数据丢失问题
解决:实现Redis数据持久化
并发能力问题
搭建主从集群,实现读写分离
存储能力问题
搭建分片集群,利用插槽机制实现动态扩容
故障恢复问题
利用Redis哨兵,实现健康检测和自动恢复
RDB全称Redis Database Backup file (Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
快照文件又被称为:RDB文件,默认是保存至当前运行目录
RDB有两种实现:
由Redis主进程来执行RDB,会阻塞所有命令
>save
ok
通常在该节点宕机前,或者准备停机前才进行操作
开启子进程执行RDB,避免主进程收到影响
>bgsave
Background saving started
一般使用这个,在redis运行中使用
Redis默认是有持久化的,在停机之前,默认保存的文件名为:dump.rdb
以上的持久化是被动的,有没有主动的持久化呢?Redis内部有触发RDB的机制,可以在redis.conf文件中找到
redis.conf:
save 900 1 900s内有一个key被修改,则执行bgsave
save 300 10 300s内10个key
save 60 10000 60s内10000个key
# 禁用RDB
save ""
redis.conf中的其他配置
# 是否压缩,建议不开启,因为压缩会消耗cpu,且相对而言硬盘不值钱
rdbcompression yes
# RDB文件名称
dbfilename dump.rdb
# 文件保存的路径目录
dir ./
当满足bgsave的时候,redis主进程fork得到子进程,子进程复制一份页表,通过页表将内存中的数据写入到RDB文件中,写好的RDB文件将会替换原来的旧的RDB文件。在整个子进程读取内存数据的过程中,内存空间中的数据是共享的,如果此时用户做出更新数据的操作,那么fork会将内存中的数据标记为只读,并且复制数据到副本,此后主进程的所有操作都是对副本进行操作,而子进程依旧继续读取原来的数据。
RDB缺点:
如果是save 60 1000,在60s内执行了1000次key更新,则进行RDB,那如果还没够1000次,这60s内宕机了,这999的数据就丢失了
fork子进程、压缩、写出RDB文件都比较耗时
AOF全称为Append 0nly File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文
AOF默认关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
# 标识每执行一次写命令,立刻记录到AOF文件
# 主进程来执行,就是每次接收到写命令,主进程要写数据且将数据写入磁盘才算命令结束
# 可以绝对保证安全,但是性能不好
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后标识每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统觉得何时将缓冲区内容写回磁盘
appendfsync no
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
配置项 |
刷盘时间 |
优点 |
缺点 |
always |
同步刷盘 |
可靠性高,几乎不丢失数据 |
性能影响大 |
everysec |
每秒刷盘 |
性能适中 |
最多丢失1秒数据 |
no |
操作系统控制 |
性能最好 |
可靠性差,可能丢失大量数据 |
AOF是记录命令,所以AOF文件会比RDB文件大的多,而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义,通过bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果,比如:
set num 3
set name jack
set num 6
bgrewriteaof:
mset num 3 name jack
RDB |
AOF |
|
持久化方式 |
定时对整个内存数据进行快照 |
记录每次执行的命令 |
数据完整性 |
不完整,两次备份之间数据可能会丢失 |
相对完整,取决于刷盘策略 |
文件大小 |
会有压缩,文件体积小 |
记录命令,文件体积很大 |
宕机恢复速度 |
很快 |
慢 |
数据恢复优先级 |
低,因为数据完整性不如AOF |
高,因为数据完整性更高 |
系统资源占用 |
高,大量cpu和内存消耗 |
低,主要是磁盘IO资源,但AOF重写时会占用大量CPU和内存资源 |
使用场景 |
可以容忍数分钟的数据丢失,追求更快的启动速度 |
对数据安全性要求较高 |
RDB产生的rdb文件是一个完整的数据快照文件,比较适合数据恢复
高性能:会fork独立进程,不占用主进程资源
简单:不必每次记录执行命令,只需要记录数据就行了
单节点Redis的并发能力是有上限的,且不安全,万一宕机了内存中的数据就丢失了
可以组成redis集群,且redis的读操作较多,写操作较少,可以实现读写分离,读去一台redis,写去另一台,但是这种读写分离也要保证不管从哪个节点读取数据,数据都是一样的,这就需要主节点数据同步
第一阶段:从节点带着replid向主节点发送全量数据同步请求,主节点接收到之后判断是不是第一次同步即判断replid是否一致,如果是第一次同步,则将主节点的版本信息告诉从节点
第二阶段:主节点执行bgsave,开启异步进程生成RDB文件,并将RDB发给从节点,这个时候难免会有数据更新进来,于是主节点将发送RDB文件这个过程中的新的更新命令放入repl_baklog缓冲区,从节点接收到RDB文件就加载这个文件
第三阶段:主节点将repll_baklog缓冲区中的新命令发送给从节点,从节点执行新命令
主节点如何判断从节点是不是第一次同步
Replication ld:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replidslave则会继承master节点的replid
offset:偏移量,随着记录在repl baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新
总结来说:replid是用来标记是哪个主节点的redis从节点,offset用来判断从节点数据跟主节点数据的差距,如果两边offset一样说明数据相同,主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步
其中这个repl_baklog的缓冲区的执行过程:
repl_baklog是一个环形队列,master不断向里面写入数据,其中绿色的跟红色的差值就是代表为同步的数据,如果一直往里面塞数据,有可能会塞满,但是如果塞满了,就会继续塞,替代掉一开始的绿色。
但是这样也会有问题:
如果slave宕机太久,master写入的数据量太大,占满了整个repl_baklog,就会出现slave还没同步的数据也被master新写入的数据覆盖,即offset被覆盖溜了,此时为了数据的一致性,就必须要开始全量同步
如何优化Redis主从集群?
及时恢复slave,防止宕机太久,尽量避免全量同步
在master中配置repl-disklee-sync yes启用五磁盘复制,避免全量同步时的磁盘IO,代表着不再通过磁盘传送数据,而是通过网络的形式传递(需要网络的支持,网络不能太差)
Redis单节点的内存占用不用太大,减少RDB导致的过多磁盘 IO
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从的链式结构,减少master压力
全量同步跟增量同步的区别?
全量同步需要经过RDB过程,发送RDB文件给slave。后续命令通过repl_baklog发送给slave
增量同步不需要RDB过程,只需要获取repl_baklog中的offset后续命令
什么执行全量同步?
在slave第一次连接master的时候
在slave断开连接太久,repl_baklog中的offset被覆盖被迫执行
什么时候执行增量同步?
跟master连接之后,且repl_baklog中offset没有被覆盖
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下:
监控
sentinel会不断检查master和slave是否按照预期工作
自动故障恢复
如果master故障,sentinel会将一个slave提升为master,当故障实例恢复后以新的masater为主
通知
sentinel充当redis客户端的服务发现来源,当集群发送故障转移时,会将最新信息推送到redis的客户端
客户端也不是直接访问redis,而是访问sentinel,由sentinel告诉客户端需要去哪个节点
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超
过Sentinel实例数量的一半。
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds *10)则会排除该slave节点
然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举。
如果slave-prority一样,则判断slave节点的sffset值,越大说明数据越新,优先级越高
最后是判断slave节点的运行id大小,越小优先级越高。
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
sentinel给所有其它slave发送slaveof 192.168.150.101 7002命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
主从和哨兵可以解决高可用,高并发读的问题,但是还是有两个问题没解决:
海量数据存储问题
高并发写问题
可以用分片集群解决上面的问题,分片集群有以下特征:
集群中有多个master,每个master保存不同的数据
每个master都可以由多个slave节点
master之间通过ping检测彼此的状态
客户端请求可以访问集群中任意节点,最终都会被转发到正确节点
Redis会把每一个master节点映射到0-16383个插槽(hash slot)上,查看集群信息的时候可以看到
数据key不是跟节点绑定,而是跟插槽绑定。redis会根据key的有效部分计算插槽值
key中包含**{},且{}中至少包含一个字符,“{}**”中的部分就是有效部分
key中不包含**{}**,整个key就是有效部分
举个例子:key是num,那就将num绑定到插槽上,但是如果说key是{demo}num,就是绑定demo,计算方式是利用CRC16算法得到一个hash值,将hash对16384取余,得到的就是slot
redis在分片的时候会将插槽分配给对应节点
将key的有效部分经过转化得到slot值,根据slot判断节点
同一类数据使用相同的有效部分key,例如key都以:{type}作为前缀:{type}shop:id
为什么会有固定的想法?
因为如果判断key不在当前节点上,会重定向到key所在插槽的节点上获取数据,如果数据量大反复切换会耗时。
有时候需要对集群进行扩容,那进来的节点又没有redis初始化分配插槽数,就需要对插槽进行更改
添加一个节点并启动(此处略,需要配置节点文件)
将节点添加到集群
# NewNodeIp新进来的节点,ToNodeIp提供插槽的节点
redis-cli --cluster add-node NewNodeIp:NeNodePort ToNodeIp:ToNodePort
做完就已经添加成功,但是还没有分配插槽
添加插槽
# 需要分配插槽的节点
redis-cli --cluster reshard nodeIp:nodePort
随后
1.接收资源节点id
2.提供资源节点id
当集群中有一个master宕机后,会发生什么?
首先是该实例与其他实例失去连接
然后是疑似宕机
最后是确认下线,自动提升一个slave
利用故障转移的机制实现:
利用clusterfailover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移,具体流程如下:
需要更换节点,比如更新设备:
使用clusterfailover手动让master宕机
slave告诉master要开始数据同步了,master拒绝所有客户端请求,防止同步过程中出现新数据
master返回当前数据的offset(从repl_baklog)给slave
slave等待offset跟master一致
master,slave开始故障转移
等到转移结束,slave广播故障转移结果:即:原slave变成master,原master变成slave
其中clusterfailover支持三种模式
缺省:默认的流程,上面说的流程
force:省略对offset的一致性校验
takeover:执行开始故障转移
spring:
redis:
# password: 123456
cluster:
nodes:
- 192.168.163.129:7010
- 192.168.163.129:7020
- 192.168.163.129:7030
- 192.168.163.129:8010
- 192.168.163.129:8020
- 192.168.163.129:8030
# sentinel:
# master: mymaster
# nodes:
# - 192.168.163.129:27001
# - 192.168.163.129:27002
# - 192.168.163.129:27003
@Component
public class RedisConfig {
// 对lettuce的自定义配置
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return new LettuceClientConfigurationBuilderCustomizer() {
@Override
public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
// ReadFrom.REPLICA_PREFERRED:优先从slave节点读取,所有的slave都不可用才读取master
// REPLICA:从slave节点读取
// MASTER:从主节点读取
// MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
}
};
}
}