Redis持久化操作
Redis 如何将数据写⼊磁盘?
持久性是指将数据写⼊持久存储,例如固态磁盘 ( SSD )。 Redis 本身提供了⼀系列持久化选项: RDB ( Redis 数据库): RDB 持久性以指定的时间间隔执⾏数据集的时间点快照。
AOF ( Append Only File ): AOF 持久化记录服务器接收到的每个写操作,在服务器启动时再次播放,重 建原始数据集。命令使⽤与 Redis 协议本身相同的格式以仅附加⽅式记录。当⽇志变得太⼤时, Redis 能 够在后台重写⽇志。
⽆持久性:如果您愿意,您可以完全禁⽤持久性,如果您希望您的数据只要服务器正在运⾏就存在。
RDB + AOF :可以在同⼀个实例中结合 AOF 和 RDB 。请注意,在这种情况下,当 Redis 重新启动时, AOF ⽂件将⽤于重建原始数据集,因为它保证是最完整的。
最重要的是要了解 RDB 和 AOF 持久性之间的不同权衡。
持久化的意义
Redis 持久化的意义,在于数据备份和故障恢复。⽐如你部署了⼀个 Redis ,作为 cache 缓存,当然也可以保 存⼀些较为重要的数据。如果没有持久化的话, Redis 遇到灾难性故障的时候,就会丢失所有的数据。如果通过 持久化将数据持久化⼀份⼉在磁盘上去,然后定期⽐如说同步和备份到⼀些云存储服务上去,那么就可以保证数据 不丢失全部,还是可以恢复⼀部分数据回来的。
Redis 持久化 + 备份:⼀般将 Redis 数据从内存存储到磁盘。然后将磁盘数据备份⼀份即将数据上传到云服务器 上即可。如果左边的 Redis 进程坏了并且磁盘也坏了,此时可以在另⼀台服务器启动该 Redis ,然后将云服务器 上的数据 copy ⼀份到磁盘上, Redis 进程在启动过程中会从磁盘加载到内存中。
持久化主要是做灾难恢复,数据恢复,也可以归类到⾼可⽤的⼀个环节⾥⾯去。⽐如你 Redis 整个挂了,然后 Redis 就不可⽤了,你要做的事情是让 Redis 变得可⽤,尽快变得可⽤。重启 Redis ,尽快让它对外提供服 务,但是就像刚说的,如果你没做数据备份,这个时候 Redis 启动了,也不可⽤啊,数据都没了。很可能说,⼤ 量的请求过来,缓存全部⽆法命中,在 Redis ⾥根本找不到数据,这个时候就死定了,缓存雪崩问题,所有请 求,没有在 Redis 命中,就会去 MySQL 数据库这种数据源头中去找,⼀下⼦ MySQL 承接⾼并发,然后就挂了。 MySQL 挂掉,你都没法去找数据恢复到 Redis ⾥⾯去, Redis 的数据从哪⼉来?从 MySQL 来。
如果你把 Redis 的持久化做好,备份和恢复⽅案做到企业级的程度,那么即使你的 Redis 故障了,也可以通过 备份数据,快速恢复,⼀旦恢复⽴即对外提供服务。 Redis 的持久化,跟⾼可⽤,是有关系的。
RDB(Redis DataBase) 机制
在指定的时间间隔内将内存中的数据集写⼊磁盘,也就是快照( Snapshot ),数据恢复是将快照⽂件直接读到内存 中。
Redis 会单独创建( fork )⼀个⼦进程来进⾏持久化,会先将数据写⼊到⼀个临时⽂件( dump.rdb )中,待持久化过程 结束后,再⽤本次的临时⽂件替换上次持久化后的⽂件。
fork 函数的作⽤是复制⼀个与当前进程⼀样的进程,新进程的所有数据数值都和原进程⼀致,但是⼀个全新的进 程,并作为原进程的⼦进程。
Redis 服务器在处理 bgsave 采⽤⼦线程进⾏ IO 写⼊,⽽主进程仍然可以接收其他请求,但创建⼦进程是同步 阻塞的,此时不接受其他请求。
RDB机制
⼿动触发:通过命令⼿动⽣成快照。
⾃动触发:通过配置参数的设置触发⾃动⽣成快照。
⼿动触发 执⾏ save 和 bgsave 命令,⼿动触发快照,⽣成 RDB ⽂件
save:该命令会阻塞当前 Redis 服务器,执⾏ save 命令期间, Redis 不能处理其他命令,直到 RDB 过程 结束为⽌(会造成⻓时间阻塞,不建议使⽤)。
bgsave:该命令执⾏后, Redis 会在后台异步进⾏快照操作,快照同时还可以响应客户端的请求,阻塞只发 ⽣在 fork 阶段,基本上 Redis 内部的所有 RDB 操作都是采⽤ bgsave 命令。
⾃动触发
⾃动触发有如下四种情况:
1. redis.conf 配置⽂件中达到 save 参数的条件,⾃动触发 bgsave 。
2. 主从复制时,从节点要从主节点进⾏全量复制时也会触发 bgsave ,⽣成快照发送到从节点。
3. 执⾏ shutdown (关闭 Redis 服务),会触发 bgsave 。
4. 执⾏ flushall (⽣成⼀个空的临时⽂件 dump.rdb )。
RDB的数据恢复
将备份⽂件( dump.rdb )移动到 Redis 路径下(可以配置⽂件的存放路径)启动服务即可, Redis 启动会将⽂件数据 加载到内存,在此期间 Redis 会处于阻塞状态,直到全部数据存⼊内存。
RDB的优缺点
优点
数据恢复快。
体积⼩。
数据备份使⽤⼦进程,对redis服务性能影响⼩。
缺点
在⼀定时间间隔进⾏备份,当 Redis 意外宕机,将会丢失最后⼀次修改的数据,⽆法做到秒级持久化。
fork 进程时,会占⽤⼀定的内存空间。
RDB ⽂件是⼆进制的没有可读性。
AOF(Append Only File) 机制
将客户端的每⼀个写操作命令以⽇志的形式记录下来,追加到 appendonly.aof 的⽂件末尾,在 Redis 服务器重 启时,会加载 aof ⽂件中的所有命令,来达到数据恢复的⽬的。
当有写命令请求时,会追加到 AOF 缓冲区内, AOF 缓冲区根据 AOF 持久化策略[ always , everysec , no ]将操作 同步到磁盘的 AOF ⽂件中,当 AOF ⽂件⼤⼩超过重写策略或⼿动重写时,会对 AOF ⽂件进⾏重写来压缩 AOF ⽂件容量, Redis 服务重启时,会重新加载 AOF ⽂件中的写操作来进⾏数据恢复。

AOF的触发⽅式
⼿动触发
通过 bgrewriteaof 命令:重新 AOF 持久化⽣成 aof ⽂件(触发重写)。
⾃动触发
默认情况, Redis 是没有开启 AOF (默认使⽤ RDB 持久化),需要通过配置⽂件开启。

AOF的持久化策略有三种:
always:把每个写命令⽴即同步到 AOF ⽂件,很慢但安全。
everysec:每秒同步⼀次,默认配置。
no:Redis 不执⾏写⼊磁盘,交给 OS 系统处理,很快但不安全。
AOF 重写机制
AOF 持久化,会把每次写命令都追加到 appendonly.aof ⽂件中,当⽂件过⼤, Redis 的数据恢复时间就会变 ⻓,因此加⼊重写策略对 aof ⽂件进⾏重写,⽣成⼀个恢复当前数据的最少命令集。
#⽐如对同⼀个key进⾏多次写命令
set key 5
incr key
incrby key 500
#重写后就变为 set key 506
AOF 重写流程
主进程 fork 出⼀个⼦进程进⾏ AOF ⽂件的重写,⼦进程重写完毕后,主进程把⼦进程重写期间,其他客户端产 ⽣的写请求,追加到 AOF ⽂件中,替换旧⽂件。
AOF 的 rewirte 重写和 RDB 的 bgsave 都是由⽗进程 fork 出⼀个⼦进程来执⾏的。重写是直接把当前内存的 数据⽣成对应的命令,⽽不是读取旧 AOF ⽂件进⾏命令合并。
AOF的优缺点
优点
数据安全性⾼,不易丢数据。
AOF ⽂件有序保存了所有写操作,可读性强。
缺点
AOF ⽅式⽣成⽂件体积⼤。
数据恢复速度⽐ RDB 慢。
Redis 的事务
由于 Redis 在执⾏多条命令时,可能会被其他命令插队,从⽽影响预期结果。所以 Redis 中为保证完成某个功 能的⼀系列指令串在执⾏的过程中不被其他指令串所影响,提供了事务处理机制。
同传统数据库的区别
传统的事务指: 在⼀个事务中的多个操作不能分割,要么同时成功要么同时失败,如果执⾏成功通过 commit 进 ⾏事务提交,如果失败通过 rollback 进⾏事务回滚,具有:原⼦性、⼀致性、隔离性、持久性四个特性。
Redis 的事务指:⼀个指令执⾏队列,将⼀系列预定义的指令包装为⼀个整体(队列),当执⾏时,将队列中的指 令,按照既定的顺序执⾏,在执⾏过程中不允许其他命令执⾏,具有排他性。 Redis 的事务没有回滚的概念。
基本操作
开启事务
multi 指令,该指令⽤于设置事务的开始位置,该指令后的所有指令都将加⼊到 Redis 的事务中,形成⼀个指令 队列。
执⾏事务(结束事务)
exec 指令,该指令⽤于执⾏事务,表示事务结束并执⾏事务队列中的指令,它与 multi 成对使⽤。 注意:加⼊事务的指令并没有⽴即执⾏,⽽是加⼊到了⼀个执⾏队列中,当执⾏ exec 指令时才会被执⾏。
取消事务
discard 指令,该指令必须在 multi 之后, exec 之前执⾏,该指令⽤于取消当前的事务。

注意事项
注意1:在 Redis 事务中如果出现指令语法错误,则会⾃动取消当前事务
127.0.0.1:6380> multi
OK
127.0.0.1:6380> set name abc
QUEUED
127.0.0.1:6380> sets age 20 #语法错误,取消事务
(error) ERR unknown command 'sets'
127.0.0.1:6380> exec
(error) EXECABORT Transaction discarded because of previous errors.
注意2:在 Redis 事务执⾏过程中如果
127.0.0.1:6380> multi
OK
127.0.0.1:6380> set name admin
QUEUED
127.0.0.1:6380> get name
QUEUED
127.0.0.1:6380> set age 20
QUEUED
127.0.0.1:6380> get age
QUEUED
127.0.0.1:6380> lpush age aaa bbb ccc
QUEUED
127.0.0.1:6380> get age
QUEUED
127.0.0.1:6380> exec
1) OK
2) "admin"
3) OK
4) "20"
5) (error) WRONGTYPE Operation against a key holding the wrong kind of value #执
⾏错误
6) "20"
出现执⾏错误,则事务不会停⽌也不会回滚,依然执⾏
Redis 中的锁
watch 监控锁
当⼀个数据需要改变时,可能会出现多⼈同时去改变该数据,如果其中有⼀⼈改变了,则其他⼈就不能再改变(数据 只能被改变⼀次),此时我们就需要使⽤监控锁,对要修改的数据监控起来,⼀旦数据发⽣改变(已被其他⼈修改),则 ⾃动终⽌事务的执⾏。
对指定 key 添加监控锁,在执⾏事务前如果 key 的值发⽣改变,⾃动终⽌事务的执⾏
watch key[key1 key2 .....]
取消对所有可以的监控
unwatch key[key1 key2 .....]
注意: watch 监控锁只是监控,并不能像 java 中锁⼀样将数据锁定,只在事务中有作⽤。
分布式锁
当多个线程需要同时操作⼀个数据时,为避免出现数据异常,我们要将数据锁起来,使⽤结束后再将锁打开,此时 其他线程才可以继续访问该数据, Redis 中使⽤分布式锁实现此场景
Redis 中并没有分布式锁的实现,我们可以通过 setnx 来设计⼀个分布式锁
# 添加⼀个key,该key为分布式锁,我们知道setnx在设置数据时如果数据存在则返回0
# 设置该数据为锁,其他客户端要操作数据前先通过该指令的返回值检测如果返回值为0则表示当前数据已被锁定不能操
作,如果返回值为1表示加锁,然后操作
setnx lock-num 1
# 对加锁的数据使⽤后要解锁,通过del lock-num移除数据的⽅式实现解锁过程
del lock-num
死锁
通过分布式锁的机制可以实现对数据操作的排他性,但数据使⽤结束后必须解锁(谁加锁,谁解 锁),如果数据使⽤ 后忘记解锁或由于意外的发⽣⽆法解锁,就形成了死锁。
在设计分布式锁时不允许出现死锁。 在设置分布式锁时就需要使⽤失效时间进⾏设置,当时间到达后⾃动解锁:
# 设置分布式锁
set key value [ex seconds] [px milliseconds] [nx]
SpringBoot 中使⽤ Redis
SpringBoot 使⽤ Spring Data 与 Redis 进⾏整合,在 SpringData 中提供了⼀个模板类 RedisTemplate 来 实现 redis 的相关操作。
引⼊依赖
org.springframework.boot
spring-boot-starter-data-redis
2.7.0
配置 Redis
# Redis配置
spring:
redis:
host: 127.0.0.1 # 配置服务器地址
port: 6380 # 配置redis端⼝号
jedis:
# 连接池配置
pool:
max-active: 10 # 最⼤活动连接
min-idle: 2 # 最⼩空闲连接
max-idle: 5 # 最⼤空闲连接
max-wait: 1000 # 最⼤等待时间
操作 Redis
操作 Redis 主要通过两种⽅式来完成:
RedisTemplate 模版类
注解⽅式
RedisTemplate 模版类
RedisTemplate 是简化 Redis 数据访问代码的⼯具类。在给定对象的基础⼆进制数据之间执⾏⾃动序列化/反序 列化。中⼼⽅法是 execute ,⽀持实现 X 接⼝的 Redis 访问代码,它提供了 RedisConnection 处理,使得 RedisCallback 实现和调⽤代码都不需要显式关⼼检索/关闭 Redis 连接,或处理连接⽣命周期异常。对于典型 的单步动作,有各种⽅便的⽅法。⼀旦配置好,这个类就是线程安全的。这是 Redis ⽀持的核⼼类。⼀般我们使 ⽤它的⼦类 StringRedisTemplate 类。
SpringBoot 提供的 Redis 数据结构的操作类
ValueOperations 类,提供 Redis String API 操作
ListOperations 类,提供 Redis List API 操作
SetOperations 类,提供 Redis Set API 操作
ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作
HashOperations 类,提供 Redis Hash API 操作
下来我们分别看看各个数据结构操作类的使⽤⽅式:
ValueOperations 类
@RestController
@RequestMapping("/string")
public class StringController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/a")
public String a() {
ValueOperations value = stringRedisTemplate.opsForValue();
value.set("A", "aaa"); // set A aaavalue.set("B", "bbb", 10, TimeUnit.SECONDS); // set B bbb ex 10
value.setIfAbsent("A", "aaa2"); // setnx A aaa2
value.setIfAbsent("C", "ccc");
value.set("D", "1");
value.set("E","eee");
value.set("F","fff");
return "";
}
@GetMapping("/b")
public String b() {
ValueOperations value = stringRedisTemplate.opsForValue();
String v1 = value.get("A");
String v2 = value.get("B");
String v3 = value.get("C");
return v1 + ".." + v2 + ".." + v3;
}
@GetMapping("/c")
public String c() {
ValueOperations value = stringRedisTemplate.opsForValue();
value.append("A", "999888");
value.increment("D"); // d+1
value.increment("D", 3); // d+3
value.decrement("D"); //d-1
value.decrement("D", 3);// d-3
value.getAndExpire("C",1,TimeUnit.MINUTES); // 给C设置1min
// 删除e和f
String v1 = value.getAndDelete("E");
value.set("F","",1,TimeUnit.MILLISECONDS);
return "";
}
}
ListOperations 类
@RestController
@RequestMapping("/list")
public class ListController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/a")
public String a() {
ListOperations list = stringRedisTemplate.opsForList();
list.leftPush("A", "aa");
list.leftPushAll("A", "bb", "cc", "dd", "ee", "ff", "gg");
list.rightPush("A", "11");
list.rightPushAll("A", "22", "33", "44", "55", "66", "77", "88");
return "";
}SetOperations 类
@GetMapping("/b")
public String b() {
ListOperations list = stringRedisTemplate.opsForList();
List allList1 = list.range("A", 0, -1);
String l1 = list.leftPop("A");
String l2 = list.rightPop("A");
List lefts = list.leftPop("A", 3);
List rights = list.rightPop("A", 2);
List allList2 = list.range("A", 0, -1);
String v1 = list.index("A", 3);
return "";
}
@GetMapping("/c")
public String c() {
ListOperations list = stringRedisTemplate.opsForList();
list.remove("A", 2, "dd");
list.set("A", 3, "MMMMM");
long size = list.size("A");
System.out.println(list+".....");
Long index1 = list.indexOf("A", "a");
Object index2 = list.indexOf("A", "aa");
return "";
}
}
SetOperations 类
package com.dailyblue.java.spring.boot.redis.controller;
@RequestMapping("/set")
@RestController
public class SetController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/a")
public String a() {
SetOperations set = stringRedisTemplate.opsForSet();
set.add("A", "aa", "bb", "cc", "11", "22", "33", "44", "55");
set.add("B","aa","bb","cc","dd","ee");
set.add("C","bb","22","33","ee","44");
return "";
}
@GetMapping("/b")
public String b() {
SetOperations set = stringRedisTemplate.opsForSet();
Set all = set.members("A");
String v1 = set.randomMember("A");
List list = set.randomMembers("A",4);
Set set1 = set.distinctRandomMembers("A",4);ZSetOperations 类
return "";
}
@GetMapping("/c")
public String c() {
SetOperations set = stringRedisTemplate.opsForSet();
Set set1 = set.difference("B", "C");
Set set2 = set.union("B", "C");
Set set3 = set.intersect("B", "C");
return "";
}
}
ZSetOperations 类
package com.dailyblue.java.spring.boot.redis.controller;
@RestController
@RequestMapping("/zset")
public class ZSetController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/a")
public String a() {
ZSetOperations zset = stringRedisTemplate.opsForZSet();
zset.add("A", "aa", 9.8);
zset.add("A", "bb", 5.7);
zset.add("A", "cc", 1.5);
zset.add("A", "dd", 11);
zset.add("A", "ee", 2);
zset.add("A", "bb", 2);
zset.addIfAbsent("A", "ff", 4.7);
zset.addIfAbsent("A", "cc", 4.23);
Set set1 = Set.of("aa", "bb", "cc", "dd", "ee", "ff");
Set set2 = Set.of("aa", "mm", "33", "cc", "ff", "kk");
for (String s : set1) {
zset.add("B", s, Math.random() * 100);
}
for (String s : set2) {
zset.add("C", s, Math.random() * 100);
}
return "";
}
@GetMapping("/b")
public String b() {
ZSetOperations zset = stringRedisTemplate.opsForZSet();
Set set1 = zset.range("A", 0, -1);
Long size = zset.count("A", 3, 7);
String v1 = zset.randomMember("A");
List set2 = zset.randomMembers("A", 3);HashOperations 类
Set set3 = zset.distinctRandomMembers("A", 3);
return "";
}
@GetMapping("/c")
public String c() {
ZSetOperations zset = stringRedisTemplate.opsForZSet();
Set set1 = zset.difference("B", "C");
Set set2 = zset.union("B", "C");
Set set3 = zset.intersect("B", "C");
return "";
}
}
HashOperations 类
package com.dailyblue.java.spring.boot.redis.controller;
@RestController
@RequestMapping("/hash")
public class HashController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/a")
public String a() {
HashOperations hash = stringRedisTemplate.opsForHash();
hash.put("A","A","aaa");
hash.put("A","B","bbb");
hash.put("A","C","ccc");
hash.put("A","D","ddd");
hash.putIfAbsent("A","A","ddd");
Map map = new HashMap<>();
map.put("M1","mmm1");
map.put("M2","mmm2");
map.put("M3","mmm3");
map.put("M4","mmm4");
hash.putAll("A",map);
return "";
}
@GetMapping("/b")
public String b(){
HashOperations hash = stringRedisTemplate.opsForHash();
String v1 = hash.get("A","B");
Map map = hash.entries("A");
Set keys = hash.keys("A");
Collection values = hash.values("A");
String randomKey = hash.randomKey("A");
List list = hash.randomKeys("A",3);
return null;
}
@GetMapping("/c")
public String c(){
HashOperations hash = stringRedisTemplate.opsForHash();
hash.delete("A","A","M2");
long len = hash.size("A");
return null;
}
}
注解⽅式
刚才的模版类可以处理 SpringBoot 对 Redis 的操作,但是⼜略显麻烦,有没有⼀种简单⽅式呢? 答案:注解,我们可以使⽤提供的注解来对 Redis 进⾏操作。Spring 从 3.1 开始就引⼊了对 Cache 的⽀持。定义 了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接⼝来统⼀不同的缓 存技术。并⽀持使⽤注解简化我们的开发。
@CacheConfig 注解
@Cacheable() ⾥⾯都有⼀个 value=“xxx” 的属性,这显然如果⽅法多了,写起来也是挺累的,如果可以⼀次 性声明完 那就省事了, 所以,有了 @CacheConfig 这个配置, @CacheConfig is a class-level annotation that allows to share the cache names ,如果你在你的⽅法写别的名字,那么依然以⽅法的名字为准。
@Cacheable 注解
@Cacheable 注解在⽅法上,表示该⽅法的返回结果是可以缓存的。也就是说,该⽅法的返回结果会放在缓存中, 以便于以后使⽤相同的参数调⽤该⽅法时,会返回缓存中的值,⽽不会实际执⾏该⽅法。


/*
cacheNames:在缓存中的名称,和value作⽤⼀致
key:参数名,和cacheNames⼀起组成最终的缓存Key
unless:当ID⼤于5时不缓存数据
condition:当age⼩于30时才缓存数据
*/
@Cacheable(cacheNames = "dailyblue", key = "#id", unless = "#id>5", condition =
"#age<=30")
public JsonResult dailyblue(int id, int age) {
return ResultTool.success(mapper.find(id,age));
}
@CachePut 注解
@CachePut 的作⽤主要针对⽅法配置,能够根据⽅法的请求参数对其结果进⾏缓存,和 @Cacheable 不同的是, 它每次都会触发真实⽅法的调⽤。⼀般⽤于更新操作,调⽤这个注解后会访问数据库,并将数据库的内容同步到 Redis 中。
@CacheEvict 注解
@CachEvict 的作⽤主要针对⽅法配置,能够根据⼀定的条件对缓存进⾏清空。
@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
public User getUserInfo(User user){
...
return user;
}
// 清空当前cache name下的所有key
@CachEvict(allEntries = true)
@Caching 注解
@Caching 可以使注解组合使⽤,⽐如根据 id 查询⽤户信息,查询完的结果为{key = id,value = userInfo},但我们现 在为了⽅遍,想⽤⽤户的⼿机号,邮箱等缓存对应⽤户的信息,这时候我们就要使⽤ @Caching 。例:
@Caching(put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
})
public User getUserInfo(User user){
...
return user;
}
注意:如果要使⽤注解⽅式必须在启动类前引⼊ @EnableCaching 注解。

浙公网安备 33010602011771号