1.数据丢失问题
大家可以设想一下这个场景,假如我们现在只有一个Redis,即单点Redis,我们在往Redis中添加数据的时候突然宕机了,那数据怎么办,如果是一条还好,我在敲一遍就行,那我敲了一万行都没保存,难道我要再敲一遍吗?这是不现实的,所以就出现了单点Redis的时第一个问题:数据丢失问题。那他的解决办法就是:实现Redis的数据持久化
1.1实现Redis的数据持久化
RDB
Redis Database Backup file,又称Redis数据快照,就是把内存中的数据全部记录到磁盘中,当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前目录,Redis停机时会执行一次RDB
save 由Redis主进程执行RDB,会阻塞所有命令 bgsave 开启子进程执行RDB,避免主进程收到影响
RDB的原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
-
当主进程执行读操作时,访问共享内存;
-
当主进程执行写操作时,则会拷贝一份数据,执行写操作。
RDB方式save的基本流程
-
fork主进程得到一个子进程,共享内存空间
-
子进程读取内存数据并写入新的RDB文件
-
用新的RDB文件替换旧的RDB文件
RDB会在什么时候执行?
-
默认是服务停止时
save 60 1000代表什么含义?
-
代表60秒内至少执行1000次修改则触发RDB
RDB方式的缺点
-
RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
-
fork子进程,压缩,写出RDB文件都比较耗时
AOF
Append Only File,即追加文件,Redis处理的每一个写命令都会记录在AOF文件中,看一看做命令日志文件。因为AOF为记录命令,所以AOF文件会比RDB文件大得多,而且会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果。Redis也会在触发阈值时,自动去重写AOF命令。
RDB和AOF区别
二者各有千秋,实际开发中往往会结合使用。
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间可会有丢失 | 相对完整,取决于刷盘策略 |
数据恢复优先级 | 低,数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量的CPU和内存消耗 | 低,主要是磁盘IO资源,但AOF重写时会占用大量的CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
文件大小 | 会有压缩,文件体积小 | 记录命令,体积大 |
宕机恢复速度 | 快 | 慢 |
2.并发能力问题
现在我们解决了数据丢失的问题,那么还是在一样的场景下,我们现在只有一个Redis,既要读又要写,既当爹又当妈,那么此时此刻就是双11的零点,无数的读写请求在同一时刻打到redis上,那么即使一个Redis再快,也忙活不过来啊,因为读写的请求实在是太多了,这就是并发能力的问题,解决方法也很简单,搭建主从集群,实现读写分离
2.1主从集群实现读写分离
搭建主从架构:单点的Redis的并发能力是有上限的,要进一步提高Redis的并发能力,则要搭建Redis主从集群,实现读写分离。
假设现有A,B两个节点,那么只需要在B节点执行:slaveof A.IP A.port,B就可以成为A的slave节点
那么如何实现主从节点间的数据同步呢?
数据同步原理
主从第一次同步是全量同步
Replicatio ID:简称replID,是数据集的标记,id一致则说明是同一个数据集,每一个master都有唯一的replid,salve则会继承master的replid
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前的eoffset。若slave的offset小于master的offset,说明master的新数据slave还未同步,需要更新。
slave做数据同步,必须向master声明自己的replid和offset,master才可以判断哪些数据需要同步
全量同步的流程
-
slave节点请求增量同步
-
master判断replid是否一致,不一致则拒绝
-
master将完整的内存数据生成RDB,发送到slave
-
slave请求本地数据,加载master的RDB
-
master将RDB期间的命令记录在repl_baklog上,并持续将log中的命令发送给salve
-
slave执行接收的命令,保持与master之间同步
若slave重启后同步,则执行增量同步,repl_baklog大小有上限,写满后会覆盖最早的数据,如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步
全量同步和增量同步的区别
-
全量同步:master将完整的内存数据生成RDB文件,发送个slave,后续的命令则记录在repl_baklog中,逐步发送给slave
-
增量同步:slave提交自己的offset到master,master获取repl_baklog中offset之后的命令个slave
什么时候执行全量同步?
-
slave第一次连接master节点时
-
slave节点断开太久,repl_baklog中的节点又被覆盖时
什么时候执行增量同步?
-
slave节点断开又恢复,并且在repl_baklog中能找到offset时
3.故障恢复问题
在第一个问题中,我么解决了数据持久化的问题,那么假如又出现一种新的状况(不要嫌多,实际环境的问题可能更多),那就是连我们数据持久化的RDB,AOF文件也跟着没了,此时我们该怎么办呢?不要担心,如果是slave节点没了,找master节点再进行数据同步就好了,那如果是master节点宕机怎么办呢?使用哨兵机制来实现主从集群的自动故障恢复。
3.1哨兵机制实现主从集群的自动故障恢复
哨兵集群结构
哨兵的作用
在上述的结构中,我们可以清晰的看到哨兵集群的三个作用
-
监控:sentinal会不断检查master和slave是否按预期工作
-
通知:哨兵会充当redis客户端的服务来发现来源,当集群发生故障转移时,会将最新消息推送给redis客户端
-
自动故障恢复:如果master故障,哨兵会提拔一个slave作为新的master,当故障实例恢复后,也已新的master为主
服务状态监控
哨兵基于心跳机制检测服务状态,每隔一秒向集群的每一个实例发送ping命令
主观下线:如果某个哨兵发现某实例在规定时间内未作出响应,则认为该实例主观下线
客观下线:若超过指定数量的哨兵都以为该实例主管下线,则该实例客观下线,指定数量值最好超过哨兵数量的一半
自动故障恢复的过程
-
首先选定一个新的slave作为新的master,执行slave of no one
-
然后让所有节点执行slaveof 新master
-
修改故障节点,修改好后,执行slaveof 新master
4.存储能力问题
经过上述的学习,我们已经通过搭建主从集群,哨兵集群解决了高可用,高并发写的问题。但依然有两个问题未解决:
-
海量数据存储问题
-
高并发写的问题
所以使用分片集群解决上述问题
4.1分片集群
分片集群的结构
分片集群的特征
-
集群有多个master,每个master保存不同的数据
-
每个master节点都可以有多个slave
-
master节点通过心跳机制监测批次的健康状态
-
客户端请求可以访问任意节点,最终都会被转发到正确节点
我们提到每个master保存不同的数据,那这些数据如何选择自己要保存的节点呢?这就要提到散列插槽
散列插槽
Redis会把每一个master节点映射到0--16383,共16384个插槽(hash slot)上,数据key不是与节点绑定,而是与插槽绑定,redis会根据key的有效部分计算插槽值,分两种情况:
-
key中包含
{ }
,{ }
中至少有一个字符,{ }
中的为有效值 -
key中不包含{},整个key都是有效值
Redis如何判断某个key在哪个实例
-
将16384个插槽分配到不同的实例
-
根据key的有效部分计算哈希值,对16384取余
-
余数作为插槽,寻找插槽所在的实例即可
如何将同一类数据固定的保存在同一个Redis实例中
这一类数据使用相同的有效部分,例如key都已{typeID}为前缀
总结与反思
-
这四个问题及解决方法的核心,其实都是围绕在高并发的实际环境中展开的,这也就是如何应对日益增长的物质需求。
-
在这些集群中所使用到的方式,简直就是社会结构的缩影,在分片集群中甚至连哨兵的开销都省了。
-
笔落至此,我唯一顿感精妙之处在散列插槽,其利用了散列表键值对的特点,使算法复杂度降低为O(1),利用key计算哈希值,从而确定value,使本就基于内存的查找速度又更进一步,但是文中并未谈论到如果发生哈希碰撞,会怎么办呢?对插槽数值取余,这看起来像是采用了除法散列法,作为哈希函数的构造函方法,解决哈希碰撞可采用链接法,开放寻址法等,本文是讲解Redis的,就不赘述了。