1.持久化
为了演示,先在虚拟机上安装 Redis,具体可以参考这篇。
1.1.RDB
1.1.1.执行时机
RDB持久化在四种情况下会执行:
-
执行save命令
-
执行bgsave命令
-
Redis停机时
-
触发RDB条件时
在 Redis 客户端执行 save 命令,会立即进行一次 RDB 备份:
[icexmoon@192 ~]$ redis-cli
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> save
OK
save 命令备份使用的是 Redis 的主线程,此时主线程是阻塞的,不能处理其他请求。
在 Redis 客户端执行 bgsave 命令同样会进行 RDB 备份:
127.0.0.1:6379> bgsave
Background saving started
与 save 命令不同的是,bgsave 是 Redis 的主进程 fork 出一个子进程来执行备份工作,子进程产生后主进程不会处于阻塞状态,依然可以处理请求。
Redis 关闭时同样会执行 RDB 备份:
...295 # User requested shutdown... ...295 * Saving the final RDB snapshot before exiting. ...296 * DB saved on disk ...296 * Removing the pid file. ...297 # Redis is now ready to exit, bye bye...
输出中明确说明了在退出前保存了 RDB 快照。
为了确保在某些非正常停机时也能够保存 RDB 快照,Redis 还支持在特定时间内满足条件就自动保存快照的功能。条件可以在 Redis 配置文件 redis.conf 中进行设置:
# 900秒内,如果至少有1个key被修改,则执行bgsave , 如果是save "" 则表示禁用RDB
save 900 1
save 300 10
save 60 10000
RDB的其它配置也可以在redis.conf文件中设置:
# 是否压缩 ,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
1.1.2.原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
-
当主进程执行读操作时,访问共享内存;
-
当主进程执行写操作时,则会拷贝一份数据,执行写操作。
1.1.3.小结
RDB方式bgsave的基本流程
-
fork主进程得到一个子进程,共享内存空间
-
子进程读取内存数据并写入新的RDB文件
-
用新RDB文件替换旧的RDB文件
RDB的缺点
-
RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
-
fork子进程、压缩、写出RDB文件都比较耗时
1.2.AOF
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
1.2.1.AOF 配置
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf文件来配:
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
三种策略对比:
一般推荐使用appendfsync everysec
1.2.2.实际演示
为了演示 AOF 备份的功能,先关闭 RDB备份:
save ""
#save 3600 1
#save 300 100
#save 60 10000
删除 RDB 备份文件:
[root@192 redis-6.2.4]# rm *.rdb
rm:是否删除普通文件 'dump.rdb'?y
重启 Redis 服务端,并写入一些 key-value:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set num 123
OK
127.0.0.1:6379> set name icexmoon
OK
1秒后,会生成一个 AOF 文件:
[icexmoon@192 redis-6.2.4]$ ls *.aof
appendonly.aof
[icexmoon@192 redis-6.2.4]$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$3
num
$3
123
*3
$3
set
$4
name
$8
icexmoon
这个 AOF 文件会记录所有在 Redis 服务端上执行过的操作,$3
表示下面的命令有3个字符长度。
关闭 Redis 服务端,可以看到:
...559 # User requested shutdown... ...559 * Calling fsync() on the AOF file.
说明在退出时也会确保 AOF 还没有记录的操作从缓冲区全部写入 AOF 文件。
相应的,在 Redis 服务端再次启动后,会输出:
591 * DB loaded from append only file: 0.000 seconds
这表示 Redis 通过加载 AOF 备份文件恢复数据:
127.0.0.1:6379> keys *
1) "name"
2) "num"
127.0.0.1:6379> get num
"123"
127.0.0.1:6379> get name
"icexmoon"
1.2.3.AOF 文件重写
虽然 AOF 备份的时效性很好,即使是appendfsync everysec
策略,最差清空也只是丢失1秒内的操作。
但是 AOF 有个明显的缺点,因为会记录所有的操作,所以某些对于数据恢复来说是无效的“中间”操作同样会被记录,比如对 Redis 执行以下操作:
127.0.0.1:6379> set num 123
OK
127.0.0.1:6379> set num 333
OK
生成的 AOP 文件:
[icexmoon@192 redis-6.2.4]$ cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$3
num
$3
123
*3
$3
set
$3
num
$3
333
实际上这里的 set num 123
操作对数据恢复是没用的操作,记录这些操作只会浪费磁盘空间。
对此,Redis 中有一个bgrewriteaof
命令,执行这个命令可以让 AOF 文件“瘦身”:
127.0.0.1:6379> BGREWRITEAOF Background append only file rewriting started
再次查看 AOF 文件:
[icexmoon@192 redis-6.2.4]$ cat appendonly.aof REDIS0009� redis-ver6.2.4� �edis-bits�@�ctime¸!eused-mem* aof-preamble���num�M���S_��
不再是可读的字符文件了,这是因为为了最大节省磁盘空间,重写后的 AOF 文件不仅去除了不必要的操作记录,还对内容进行了编码。
通常并不需要我们手动执行BGREWRITEAOF
命令,因为通过配置文件,可以设置达到某些条件时 Redis 自动执行 AOF 文件重写:
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
1.3.RDB 与 AOF 的对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
2.Redis 主从
2.1.搭建主从架构
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
具体搭建流程可以参考这篇。
2.2.主从数据同步原理
2.2.1.全量同步
主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点,流程:
这里有一个问题,master如何得知salve是第一次来连接呢??
有几个概念,可以作为判断依据:
-
Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
-
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据。
更详细的说明可以观看这个。
2.2.2.增量同步
全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。
什么是增量同步?就是只更新slave与master存在差异的部分数据。如图:
如果从 Redis 从主 Redis 上进行过全量同步,就存在 Replid 和 offset,如果再次主从同步时,replid 相同,只有 offset 不同,主 Redis 就会从 repl_backlog 中记录的 offset 有差异的部分记录提取,并传输给从 Redis 用于同步。
repl_backlog 是一个数组,如果记录达到上限,就会从头开始覆盖,可以用一个环形结构表示:
如果从 Redis 宕机时间过久,就可能出现主 Redis 的 repl_backlog 被写满,从头覆盖,并且将从 Redis 还没有同步的操作记录覆盖的情况:
此时从 Redis 从宕机中恢复后,也无法进行增量同步,只能进行全量同步。
关于增量同步更详细的说明,可以观看这个。
2.3.主从同步优化
主从同步可以保证主从数据的一致性,非常重要。
可以从以下几个方面来优化Redis主从就集群:
-
在master中配置
repl-diskless-sync yes
启用无磁盘复制(全量同步时,生成的RDB 文件不写入磁盘,写入内存),避免全量同步时的磁盘IO。 -
Redis单节点上的内存占用不要太大(限制 Redis 内存使用上限),减少RDB导致的过多磁盘IO
-
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
-
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
主从从架构图:
3.Redis 哨兵
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。
3.1.哨兵原理
哨兵的结构如图:
哨兵的作用如下:
-
监控:Sentinel 会不断检查您的master和slave是否按预期工作
-
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
-
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
关于哨兵原理的详细说明可以观看这个。
3.2.搭建哨兵集群
具体的搭建过程可以参考这篇。
3.3.RedisTemplate
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。
下面,我们通过一个测试来实现RedisTemplate集成哨兵机制。
3.3.1.导入 Demo 工程
导入示例项目
3.3.2.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3.3.配置 Redis 地址
这里使用的是哨兵模式,所以添加的也是哨兵集群的地址:
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.0.88:27001
- 192.168.0.88:27002
- 192.168.0.88:27003
3.3.4.配置读写分离
在配置类中添加一个 Bean:
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
这个bean中配置的就是读写策略,包括四种:
-
MASTER:从主节点读取
-
MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
-
REPLICA:从slave(replica)节点读取
-
REPLICA _PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master
3.3.5.测试
现在 Redis 的主从信息由 Redis-sentinel 告诉 RedisTemplate,RedisTemplate 就可以用主 Redis 写,用从 Redis 读。
先请求 http://localhost:8080/set/num/100 写入信息。
可以从日志看到:
/192.168.0.46:14777 -> /192.168.0.88:7003] writing command AsyncCommand [...] /192.168.0.46:14777 -> /192.168.0.88:7003, chid=0xc] Received: 5 bytes, 1 commands in the stack
此时我们的主 Redis 是 7003 端口的 Redis,所以 RedisTemplate 是向这个 Redis 执行命令。
再请求 http://localhost:8080/get/num 从 Redis 中读取数据。
日志:
/192.168.0.88:7002, chid=0xa] write(ctx, AsyncCommand [...] /192.168.0.88:7002] writing command AsyncCommand [...] /192.168.0.88:7002, chid=0xa] Received: 9 bytes, 1 commands in the stack
此时 7001 和 7002 端口的 Redis 都是从 Redis,都可以用于数据读取,这里使用的是 7002。
如果终止 7003 端口的主 Redis,可以看到故障转移,7001 或 7002 会变成新的主 Redis,并且 Redis-sentinel 会告知 RedisTemplate,RedisTemplate 的读写会使用新的主从。
4.Redis 分片集群
4.1.搭建分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
-
海量数据存储问题
-
高并发写的问题
使用分片集群可以解决上述问题,如图:
分片集群特征:
-
集群中有多个master,每个master保存不同数据
-
每个master都可以有多个slave节点
-
master之间通过ping监测彼此健康状态
-
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
具体搭建流程可以阅读这篇。
4.2.散列插槽
前面已经说过,分片集群中,每个 master 会保存不同的数据,为了确定在存储数据时数据保存在哪个 master 上,Redis 采用一种插槽(slot)机制。
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:
此时,7001 对应的插槽是 0-5460,7002 对应的插槽是 5461-10922,7003 对应的插槽是 10923-16383。
在存储 KEY 时,会计算出 KEY 对应的插槽,并存储到插槽所在的 Master。
KEY 存储在哪个插槽的计算方式是:用 Hash 算法(CRC16)计算 KEY 名称中的有效部分的哈希值,让哈希值对16383取余,得到的结果就是对应的插槽。
默认情况下,整个 KEY 名称就是用于 Hash 的有效部分,但如果 KEY 名称中包含 {}
,则{}
包裹的部分会作为计算哈希值时的有效部分。
可以利用这一点将同一类 KEY 存储在同一个 master 上,以提高批量读写时的性能(避免发生因为插槽在不同 Master 上时切换 Master 的性能损耗),比如对于保存学生姓名的 KEY,可以命名为
{student}name1
,{student}name2
等。
下面进行实际演示:
[root@192 redis-slaver]# redis-cli -c -p 7001
127.0.0.1:7001> set num 123
OK
127.0.0.1:7001> get num
"123"
127.0.0.1:7001> set a 111
-> Redirected to slot [15495] located at 192.168.0.88:7003
OK
192.168.0.88:7003> get a
"111"
注意,用 redis-cli 连接分片集群时,需要使用
-c
参数,代表集群(Cluster)。
可以看到,一开始客户端连接的是 7001,写入和读取 KEY 为 num 时都没有问题,但写入名称为a
的 KEY 时,客户端自动切换到了 7003,这是因为 a 的哈希值取余后,对应的插槽是 15495,该插槽位于 7003 这个 Master。
4.3.集群伸缩
前面提到的插槽机制有一个好处,就是数据存储是和插槽绑定而非直接和 Master 绑定的,因此整个集群可以很容易的进行扩容或收缩。
在分片集群中添加和删除 Master 需要使用一些命令,这些集群相关命令可以通过以下帮助命令查看:
[root@192 redis-slaver]# redis-cli --cluster help
其中向集群中添加节点的命令是:
这里new_host:new_port
表示要添加的新节点的 ip 和端口,existing_host:existing_port
表示集群中现有的一个节点的 ip 和端口(用于向集群告知并进行添加节点操作),默认情况下该命令添加的节点是 Master,如果指定--cluster-slave
,添加的节点会是 Slave,--cluster-master-id <arg>
可以指定这个 Slave 的 Master,其中<arg>
是集群中的节点 ID:
4.3.1.案例
先启动一个新的 Redis 实例。
[root@192 redis-slaver]# mkdir 7004
[root@192 redis-slaver]# cp redis.conf ./7004
[root@192 redis-slaver]# ls 7004
[root@192 redis-slaver]# sed -i s/6379/7004/g 7004/redis.conf
[root@192 redis-slaver]# redis-server 7004/redis.conf
将这个 Redis 实例作为主节点添加到集群:
[root@192 redis-slaver]# redis-cli --cluster add-node 192.168.0.88:7004 192.168.0.88:7001
需要注意的是,新添加的主节点并不会被自动分配插槽:
可以看到此时 7004 是没有插槽的。
假设我们希望某一部分插槽被分配到 7004,比如 num 这个 KEY 所在的插槽。需要先查看对应的插槽编号:
[root@192 redis-slaver]# redis-cli -c -p 7004
127.0.0.1:7004> get num
-> Redirected to slot [2765] located at 192.168.0.88:7001
"123"
目标插槽是 2765,且位于 7001。
现在我们将 7001 上 0~2999 的插槽移动到 7004:
[root@192 redis-slaver]# redis-cli --cluster reshard 192.168.0.88:7001
// ...
[OK] All 16384 slots covered.
// 在这里输入需要移动的插槽数
How many slots do you want to move (from 1 to 16384)? 3000
// 在这里输入接收的目标节点的 ID
What is the receiving node ID? 288b62bb0e96578f0d3ddf3f178b5af10d589563
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
// 在这里输入作为插槽数据源的节点 ID,这里使用 7001
Source node #1: bd91cf2cad9a3a309fcf5c0b54f05ebd48cbf32a
// 在这里输入 done 表示数据源输入完毕
Source node #2: done
// ...
// 在这里输入 yes 确认并执行操作
Do you want to proceed with the proposed reshard plan (yes/no)? yes
测试:
[root@192 redis-slaver]# redis-cli -c -p 7004
127.0.0.1:7004> get num
"123"
可以看到,没有发生节点切换。
下面删除节点 7004,在删除前必须要让该节点为空,即将其上的插槽转移到其他节点。
这里将 7004 上的插槽转移回 7001,过程与之前的类似,不再赘述。
现在 7004 上没有插槽了:
删除 7004 节点:
[root@192 redis-slaver]# redis-cli --cluster del-node 192.168.0.88:7004 288b62bb0e96578f0d3ddf3f178b5af10d589563
4.4.故障转移
4.4.1.自动故障转移
虽然分片集群没有哨兵,但 Master 与 Master 之间,以及 Master 与 Slave 之间都保持心跳检测,在某个 Master 宕机后,会自动完成故障转移,集群会让 Master 对应的 Slave 成为新的 Master。
当前集群状态:
可以看到,7002 是 Master,且对应的 Slave 是 8003。
先用 watch
命令持续监控集群状态的变化:
[root@192 redis-slaver]# watch redis-cli -p 7001 cluster nodes
关闭主节点 7002:
[root@192 icexmoon]# redis-cli -p 7002 shutdown
可以从状态监控中观察到先是 7002 节点离线,然后 8003 的 Slave 节点变成 Master 节点。
重启 7002 节点:
[root@192 redis-slaver]# redis-server 7002/redis.conf
可以从监控信息中看到,重启后 7002 变成了 8003 的从节点,它们的身份互换。
4.4.2.手动故障转移
除了自动故障转移,有时候我们还会手动执行故障转移。比如某个主节点所在的服务器需要维护,我们就可以用一台替代服务器上的 Redis 实例作为其从节点,然后手动执行故障转移。
具体方式是在主节点对应的从节点上执行 cluster failover
命令,这样对应的主节点和从节点就会完成故障转移,身份互换。
整个流程如下:
这种failover命令可以指定三种模式:
-
缺省:默认的流程,如图1~6歩
-
force:省略了对offset的一致性校验
-
takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
推荐使用默认方式。
下面我们用这种方式让已经变成从节点的 7002 恢复成主节点。
用客户端连接 7002 并执行cluster failover
命令:
[root@192 redis-slaver]# redis-cli -p 7002 127.0.0.1:7002> CLUSTER FAILOVER
可以从监控信息中发现它们的身份再次互换,7002 再次成为主节点。
4.5.RedisTemplate
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
1)引入redis的starter依赖
2)配置分片集群地址
3)配置读写分离
依赖和读写分离之前的示例中已经做过,这里只需要修改配置:
spring:
redis:
cluster:
nodes:
- 192.168.0.88:7001
- 192.168.0.88:7002
- 192.168.0.88:7003
- 192.168.0.88:8001
- 192.168.0.88:8002
- 192.168.0.88:8003
请求 http://localhost:8080/get/num
可以从日志中看到从 8002 读取数据,8002 是 7001 的从节点,而 num 正是保存在 7001 上的,所以 RedisTemplate 依然上读写分离的方式读取和写入数据。
本文的完整示例代码可以从获取。
文章评论