Redis高级之分布式缓存
1.单点Redis的问题
2.Redis持久化
Redis 提供了不同级别
的持久化方式:
- RDB持久化方式能够在指定的时间间隔内对你的数据进行
快照存储
。 - AOF持久化方式
记录
每次对服务器的写操作
,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。Redis还能对AOF文件进行后台重写
,使得AOF文件的体积不至于过大。 - 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
- 你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会
优先载入AOF文件
来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整
。
2.1RDB
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
1 | 指定的时间间隔内至少发生指定次数的写操作,则触发RDB后台持久化 |
你也可以通过调用SAVE
或者BGSAVE
,手动让Redis进行数据保存操作。
RDB的优点
- RDB是一个非常
紧凑
的文件,它保存了某个时间点的数据集,非常适用于数据集的备份
,比如你可以在每个小时保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集。 - RDB是一个紧凑的单一文件,很方便传送到另一个远端数据中心或者亚马逊的S3(可能加密),非常适用于
灾难恢复
。 - RDB在保存RDB文件时父进程唯一需要做的就是
fork
出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能。 - 与AOF相比,在恢复大的数据集的时候,RDB方式会
更快
一些。
RDB的缺点
- 如果你希望在Redis意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么RDB不适合你。虽然你可以配置不同的save时间点(例如每隔5分钟并且对数据集有100个写的操作),但Redis要完整的保存整个数据集是一个比较繁重的工作,你通常会每隔5分钟或者更久做一次完整的保存,万一在Redis意外宕机,你可能会
丢失几分钟的数据
。 - RDB需要经常fork子进程来保存数据集到硬盘上,当
数据集比较大
的时候,fork
的过程是非常耗时
的,可能会导致Redis在一些毫秒级内不能响应客户端的请求。如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒,AOF也需要fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度。
2.2AOF
快照功能并不是非常耐久(durable):如果Redis因为某些原因而造成故障停机
,那么服务器将丢失最近写入且仍未保存到快照
中的那些数据。从1.1版本开始,Redis增加了一种完全耐久的持久化方式:AOF持久化
。
AOF全称为Append Only File,Redis处理的每一个写命令
都会记录在AOF文件,可以看作是命令日志文件。从现在开始,每当Redis执行一个改变数据集的命令时(比如SET),这个命令就会被追加
到AOF文件的末尾。这样的话,当Redis重启时,程序就可以通过重新执行AOF文件中的命令来达到重建数据集
的目的。
因为AOF的运作方式是不断地将命令追加到文件的末尾,所以随着写入命令的不断增加,AOF文件的体积
也会变得越来越大
。举个例子,如果你对一个计数器调用了100次INCR,那么仅仅是为了保存这个计数器的当前值,AOF文件就需要使用100条记录(entry)。然而在实际上,只使用一条SET命令已经足以保存计数器的当前值了,其余99条记录实际上都是多余的。
为了处理这种情况,Redis支持一种有趣的特性:可以在不打断服务客户端的情况下,对AOF文件进行重建(rebuild)。执行bgwriteaof
命令,Redis将生成一个新的AOF文件,这个文件包含重建当前数据集所需的最少命令
。Redis2.2需要自己手动执行bgwriteaof
命令;Redis2.4则可以自动触发AOF重写。
触发阈值
可以在配置文件redis.conf中指定:
1 | AOF文件比上次文件增长超过多少百分比则触发重写 |
2.3混合持久化
尽管RDB比AOF的数据恢复速度快,但是快照的频率不好把握:
- 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
- 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。
那有没有什么方法不仅有RDB恢复速度快的优点,又有AOF丢失数据少的优点呢?
当然有,那就是将RDB和AOF合体使用,这个方法是在Redis 4.0提出的,该方法叫混合使用AOF日志和内存快照,也叫混合持久化。
如果想要开启混合持久化功能,可以在Redis配置文件将下面这个配置项设置成yes:
1 | aof-use-rdb-preamble yes |
混合持久化工作在AOF日志重写
过程。
当开启了混合持久化时,在AOF重写日志时,fork出来的重写子进程会先将与主进程共享的内存数据以RDB方式写入到AOF文件,然后主进程处理的操作命令会被记录在重写缓冲区
里,重写缓冲区里的增量命令会以AOF方式写入到AOF文件,写入完成后通知主进程将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件。
也就是说,使用了混合持久化,AOF文件的前半部分是RDB格式的全量数据,后半部分是AOF格式的增量数据。这样的好处在于,重启Redis加载数据的时候,由于前半部分是RDB内容,这样加载的时候速度会很快。加载完RDB的内容后,才会加载后半部分的AOF内容,这里的内容是Redis后台子进程重写AOF期间,主进程处理的操作命令,可以使得数据更少的丢失。
2.4大Key对持久化的影响
AOF重写机制和RDB快照(bgsave命令)的过程,都会分别通过fork函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程:
- 创建子进程的途中,由于要复制父进程的页表等数据结构,
阻塞的时间跟页表的大小有关
,页表越大,阻塞的时间也越长; - 创建完子进程后,如果
父进程修改了共享数据中的大Key,就会发生写时复制
,这期间会拷贝物理内存,由于大Key占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
大Key除了会影响持久化之外,还会有以下的影响:
- 客户端超时阻塞。由于Redis执行命令是单线程处理,然后在操作大Key时会比较耗时,那么就会阻塞Redis,从客户端这一视角看,就是很久很久都没有响应。
- 引发网络阻塞。每次获取大Key产生的网络流量较大,如果一个Key的大小是1MB,每秒访问量为1000,那么每秒会产生1000MB的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程。如果使用del删除大Key时,会阻塞工作线程,这样就没办法处理后续的命令。
- 内存分布不均。集群模型在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大Key的Redis节点占用内存多。
2.5总结
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用。
比较项 | RDB | AOF |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源,但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
有很多用户都只使用AOF持久化,但并不推荐这种方式:因为定时生成RDB快照(snapshot)非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。
3.Redis主从架构
3.1搭建主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
我们开始搭建一主两从的Redis集群:
(1)创建目录
我们创建三个文件夹,名字分别叫7001、7002、7003:
1 | 进入/tmp目录 |
(2)恢复原始配置
修改redis-6.2.4/redis.conf文件,将其中的持久化模式改为默认的RDB模式,AOF保持关闭状态:
1 | 开启RDB |
(3)拷贝配置文件到每个实例目录
然后将redis-6.2.4/redis.conf文件拷贝到三个目录中(在/tmp目录执行下列命令):
1 | 方式一:逐个拷贝 |
(4)修改每个实例的端口、工作目录
修改每个文件夹内的配置文件,将端口分别修改为7001、7002、7003,将RDB文件保存位置都修改为自己所在目录(在/tmp目录执行下列命令):
1 | sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/temp\/7001\//g' 7001/redis.conf |
(5)修改每个实例的声明IP
虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定IP信息,格式如下:
1 | redis实例的声明IP |
每个目录都要改,我们一键完成修改(在/tmp目录执行下列命令):
1 | 逐一执行 |
现在三个实例还没有任何关系,要配置主从可以使用replicaof
或者slaveof
(5.0以前)命令。
有临时和永久两种模式:
修改配置文件(永久生效)
- 在redis.conf中添加一行配置:
slaveof <masterip> <masterport>
- 在redis.conf中添加一行配置:
使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):
1
slaveof <masterip> <masterport>
注意:在5.0以后新增命令replicaof,与salveof效果一致。
3.2数据同步原理
主从同步之间的第一次同步属于全量同步
,此时Master节点需要生成内存数据的RDB
快照文件并发放给Slave节点,由于生成RDB文件是子进程异步完成的,主进程依然正常处理客户端请求,因此主从全量同步期间Master节点可能会有新数据的写入
,这时Master节点需要一块内存缓冲区repl_backlog
记录所有写入命令,并增量同步
给从节点(重放命令实现落库)。
Master如何判断Slave是不是第一次来同步数据?这里会用到两个很重要的概念:
- Replication Id:简称replid,是数据集的标记,
replid一致则说明是同一数据集
。每一个Master都有唯一的replid,Slave则会继承Master节点的replid。 - offset:偏移量,随着记录在repl_backlog中的数据增多而逐渐增大。Slave完成同步时也会记录当前同步的offset,如果Slave的offset小于Master的offset,说明Slave数据落后于Master,需要更新。因此Slave做数据同步,必须向Master声明自己的replication id和offset,Master才可以判断到底需要同步哪些数据。
可以从以下几个方面来优化Redis主从就集群:
- 在Master中配置
repl-diskless-sync yes
(适合于磁盘慢网络带宽高的情况)启用无磁盘复制
,避免全量同步时的磁盘IO。 - Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO。
- 适当提高
repl_backlog
的大小,发现Slave宕机时尽快
实现故障恢复,尽可能避免全量同步。 - 限制一个Master上的Slave节点数量,如果实在是太多Slave,则可以采用主-从-从链式结构,减少Master压力。
简述全量同步和增量同步区别?
- 全量同步:Master将完整内存数据生成RDB,发送RDB到Slave。后续命令则记录在repl_backlog,逐个发送给Slave。
- 增量同步:Slave提交自己的offset到Master,Master获取repl_baklog中从offset之后的命令给Slave。
什么时候执行全量同步?
- Slave节点
第一次连接
Master节点时。 - Slave节点
断开时间太久,repl_baklog中的offset已经被覆盖时
。
什么时候执行增量同步?
- Slave节点
断开又恢复,并且在repl_baklog中能找到offset时
。
4.Redis哨兵机制
4.1哨兵的原理
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(
down-after-milliseconds*10
)则会排除该slave节点; - 然后判断slave节点的
slave-priority
值,越小优先级越高,如果是0则永不参与选举; - 如果slave-prority一样,则判断slave节点的
offset
值,越大说明数据越新,优先级越高; - 最后是判断slave节点
运行id
大小,越小优先级越高。
经过前面一系列的操作后,哨兵集群终于完成主从切换的工作,那么新主节点的信息要如何通知给客户端呢?这主要通过Redis的发布/订阅机制来实现的。每个哨兵节点提供发布/订阅机制,客户端可以从哨兵订阅消息。
哨兵提供的消息订阅频道有很多,不同频道包含了主从节点切换过程中的不同关键事件,几个常见的事件如下:
客户端和哨兵建立连接后,客户端会订阅哨兵提供的频道
。主从切换完成后,哨兵就会向+switch-master
频道发布新主节点的IP地址和端口的消息,这个时候客户端就可以收到这条信息,然后用这里面的新主节点的IP地址和端口进行通信了。通过发布/订阅机制机制,有了这些事件通知,客户端不仅可以在主从切换后得到新主节点的连接信息,还可以监控到主从节点切换过程中发生的各个重要事件。这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。
4.2搭建哨兵集群
(1)要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。我们创建三个文件夹,名字分别叫s1、s2、s3:
1 | 进入/temp目录 |
(2)然后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:
1 | port 27001 |
port 27001
:是当前sentinel实例的端口sentinel monitor mymaster 192.168.150.101 7001 2
:指定主节点信息mymaster
:主节点名称,自定义,任意写192.168.150.101 7001
:主节点的ip和端口2
:选举master时的quorum值
(3)然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/temp目录执行下列命令):
1 | 方式一:逐个拷贝 |
(4)修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:
1 | sed -i -e 's/27001/27002/g' -e 's/s1/s2/g' s2/sentinel.conf |
(5)为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:
(6)测试主节点宕机,查看故障恢复的日志:
(7)再次恢复原来的故障主节点:
4.3RedisTemplate的哨兵模式
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。
(1)在pom文件中引入redis的starter依赖:
1 | <dependency> |
(2)然后在配置文件application.yml中指定sentinel相关信息:
1 | spring: |
(3)配置主从读写分离:
1 |
|
这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:
- MASTER:从主节点读取
- MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
- REPLICA:从slave(replica)节点读取
- REPLICA_PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master
4.4脑裂问题
在Redis主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道Redis内部已经出现了问题,还在照样的向这个失联的主节点写数据,此时这些数据被主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在从节点中选举出一个leeder作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。
这时候网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点,然后从节点会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
减少脑裂的数据丢的方案:
当主节点发现「从节点下线的数量太多」,或者「网络延迟太大」的时候,那么主节点会禁止写操作,直接把错误返回给客户端。
在Redis的配置文件中有两个参数我们可以设置:
min-slaves-to-write x
,主节点必须要有至少x个从节点连接,如果小于这个数,主节点会禁止写数据。min-slaves-max-lag x
,主从数据复制和同步的延迟不能超过x秒,如果主从同步的延迟超过x秒,主节点会禁止写数据。
我们可以把min-slaves-to-write和min-slaves-max-lag这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为N和T。这两个配置项组合后的要求是,主节点连接的从节点中至少有N个从节点,并且主节点进行数据复制时的ACK消息延迟不能超过T秒
,否则,主节点就不会再接收客户端的写请求了。
即使原主节点是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从节点进行同步,自然也就无法和从节点进行ACK确认了。这样一来min-slaves-to-write和min-slaves-max-lag的组合要求就无法得到满足,原主节点就会被限制接收客户端写请求,客户端也就不能在原主节点中写入新数据了。
5.Redis分片集群
5.1搭建分片集群
分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:
IP | PORT | 角色 |
---|---|---|
192.168.139.166 | 7001 | master |
192.168.139.166 | 7002 | master |
192.168.139.166 | 7003 | master |
192.168.139.166 | 8001 | slave |
192.168.139.166 | 8002 | slave |
192.168.139.166 | 8003 | slave |
(1)删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:
1 | 进入/temp目录 |
(2)在/temp下准备一个新的redis.conf文件,内容如下:
1 | port 6379 |
(3)将这个文件拷贝到每个目录下:
1 | 进入/temp目录 |
(4)修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:
1 | 进入/tmp目录 |
(5)因为已经配置了后台启动模式,所以可以直接启动服务:
1 | 进入/tmp目录 |
(6)如果要关闭所有进程,可以执行命令:
1 | printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown |
(7)虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。
1 | redis-cli --cluster create --cluster-replicas 1 192.168.139.166:7001 192.168.139.166:7002 192.168.139.166:7003 192.168.139.166:8001 192.168.139.166:8002 192.168.139.166:8003 |
redis-cli --cluster
:代表集群操作命令create
:代表是创建集群--replicas 1
或者--cluster-replicas 1
:指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas + 1)
得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
(8)通过命令可以查看集群状态:
1 | redis-cli -p 7001 cluster nodes |
5.2散列插槽
Redis会把每一个master节点映射到0~16383共16384
个插槽(hash slot)上,查看集群信息时就能看到:
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
- key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16
算法得到一个hash值,然后对16384取余
,得到的结果就是slot值。
如何将同一类数据固定的保存在同一个Redis实例?
这一类数据使用相同的有效部分,例如key都以{type}为前缀。
5.3集群伸缩
redis-cli –cluster提供了很多操作集群的命令,可以通过下面方式查看:
需求:向集群中添加一个新的master节点,并向其中存储key1=value1
可以看到,此时加入集群的7004主节点还没有任何插槽的分配,我们需要把key1所对应的插槽(9189,在7002主节点上)迁移到7004主节点上。
此时key1所在的插槽已经转移到7004主节点上了。
5.4故障转移
当集群中有一个master宕机会发生什么呢?(自动故障转移)
- 首先是该实例与其它实例失去连接
- 然后是疑似宕机
- 最后是确定下线,自动提升一个slave为新的master
利用cluster failover
命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。其流程如下:
需求:在7001这个slave节点执行手动故障转移,重新夺回master地位