一、单机Redis存在的问题
单机Redis存在的问题
-
数据持久化问题
Redis为了提高性能,数据是在内存中存储的。而内存不能持久化存储数据,所以Redis提供了持久化机制
-
并发能力问题
单机Redis的并发能力有限,可以通过主从集群来实现读写分离,从而提高并发能力
-
故障恢复问题
在Redis集群中,一旦某个节点宕机,就必须要手动进行故障转换与恢复。Redis提供了哨兵模式来实现自动故障恢复
-
存储能力问题
无论是主从模式集群,还是哨兵的集群,都没有解决存储能力的扩展问题。Redis提供了分片集群模式,可以很方便的动态扩容
二、Redis持久化
1. 持久化介绍
所谓持久化,就是将数据永久保存。Redis是一个内存数据库,它的所有的数据都在内存中,所以有极高的读写性能,但是内存中的数据都是临时的,所以一旦Redis进程结束,所有的数据就会清空。
为了防止这样的问题,Redis提供了持久化机制:
- RDB模式:快照模式,默认是开启状态
- AOF模式:日志模式
2. RDB模式
RDB,Redis Database Backup file,称为Redis数据快照。
快照文件,也称RDB文件,默认保存在当前运行目录(在哪个目录里启动Redis服务,就保存在哪个目录)
2.1 如何持久化的?
当执行RDB持久化时,会把Redis内存中的数据全部都保存到磁盘文件上;当Redis服务重启时,就会从磁盘上加载文件,把数据恢复到内存中
2.2 什么时候持久化?
在一下情况下,会执行RDB快照持久化:
- 执行
save
命令 - 执行
bgsave
命令 - Redis服务正常关闭
- 触发自动RDB的条件
2.2.1 save命令
当执行save命令时,Redis会执行一次RDB持久化。但是save命令是一个阻塞式命令:
- 它由Redis主进程执行RDB持久化,在持久化过程中Redis将不能处理客户端的操作,知道RDB执行完毕
- 如果Redis中数据非常多,持久化将会花费比较长的时间,Redis也将会阻塞比较长的时间
- 目前,
save
命令已经很少使用了,通常使用bgsave
代替它
2.2.2 bgsave命令
当执行bgsave命令时,Redis会执行一次RDB持久化。
而bgsave是非阻塞的、异步式的:
- Redis会fork一个子进程,由子进程负责RDB持久化
- 主进程负责处理客户端的操作命令
使用redis-cli连接Redis服务之后,执行命令:bgsave
,就会执行一次RDB持久化
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379>
2.2.3 Redis服务关闭
当结束Redis服务时,Redis会先执行一次save命令,把内存中的数据进行RDB持久化。
关闭Redis服务的方式有很多,可以:
- 在redis-server服务进程中,按
ctrl + c
,立即结束redis服务 - 在redis-cli客户端中,执行命令
shutdown
,关闭redis服务 - 或者直接执行Linux命令
redis-cli shutdown
命令
2.2.4 RDB自动触发
Redis的RDB模式提供了默认的自动触发机制,相关的配置参数在配置文件redis.conf
中,默认值如下:
- 如果3600秒内有1次数据变更,就执行一次RDB
- 如果300秒内有100次数据变更,就执行一次RDB
- 如果60秒内有10000次数据变更,就执行一次RDB
save 3600 1
save 300 100
save 60 10000
提示:如果写成
save ""
,表示禁用RDB
2.3 bgsave原理
默认情况下,Redis服务只有一个进程,由这个进程负责处理客户端的一切操作请求。
当执行bgsave时:
- 会fork一个子进程,会拷贝主进程里的页表,然后子进程就可以通过页表读取物理内存里的数据,保存到磁盘文件上
- 同时,为了防止子进程RDB过程中,物理内存里的数据被修改:会把物理内存里的数据设置为read-only只读。子进程RDB时,数据就不会被修改
- 如果这时有主进程执行了新的命令要修改数据:物理内存里的数据是只读的,Redis会采用copy-on-write机制,把数据拷贝一个副本,对这个数据副本进行读写操作
- 当子进程RDB结束之后,会丢弃旧版本的数据,保留最新的副本数据
图示如下:
3. AOF模式
AOF,Append Only File。也有人称为“日志模式”。
3.1 如何持久化的?
当执行AOF持久化时,会把执行的每个数据变更的命令追加保存到磁盘文件里。当AOF恢复数据时,会读取磁盘文件,按顺序依次执行所有的命令,恢复数据。
3.2 什么时候持久化?
3.2.1 开启AOP
Redis的AOF默认是关闭状态的,如果要开启AOF模式,则需要:
-
修改
redis.conf
配置文件appendonly yes # AOF文件的名称 appendfilename "appendonly.aof"
-
appendonly
:AOF模式的开关。如果值是yes,表示开启;如果是no,表示关闭 -
appendfilename
:AOF文件的名称。默认是appendonly.aof
,可以自定义文件名
-
-
重启Redis服务
先关闭Redis服务,执行命令:
redis-cli shutdown
再重启Redis服务,执行命令:
redis-server redis.conf
3.2.2 执行时机
开启AOF模式之后,在redis.conf中配置有的AOF持久化执行时机,可通过appendfsync
参数进行配置
#appendfsync always
appendfsync everysec
#appendfsync no
- 如果值为
always
:同步刷盘,每次的写数据的命令,就立即追加到aof文件中 - 如果值为
everysec
:每次的写命令先放到AOF缓冲区,然后以每秒一次的频率,把缓冲区里的命令保存到aof文件 - 如果值为
no
:每次的写命令先放到AOF缓冲区,然后完全由操作系统决定何时 把缓冲区里命令保存到aof文件
三种时机的对比:
配置项 | 刷新时机 | 优点 | 缺点 |
---|---|---|---|
always | 同步刷盘 | 可靠性高,几乎不丢数据· | 对性能影响大 |
everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒的数据 |
no | 由操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
刷盘:把缓冲区里的数据存储到磁盘上,称为刷盘。 flush
3.3 AOF文件重写
3.3.1 AOF冗余记录
因为AOF保存的是写命令,类似于日志文件,所以通常情况下AOF日志文件都比RDB文件大,且随着使用时间的推移,AOF文件通常会越来越大,从而导致:
- 磁盘空间的占用越来越大
- 恢复数据的速度越来越慢
主要原因在于,AOF忠实的记录每次写操作的命令,就可能有大量重复的命令;比如对同一key有多次修改操作,每次修改操作都会记录到AOF文件里,而实际上只有最后一次操作命令才有效。假如先后执行以下命令:
set num 1
set num 10
set num 100
set num 1
那么AOF文件里会保存4条命令,实际上只需要最后一条命令就可以了
3.3.2 bgrewriteaof重写
手动执行AOF重写
通过在redis-cli中执行bgrewriteaof
命令,可以让AOF文件执行重写:
- 删除无效的命令。例如:
set a A
,set a B
,del a
三个命令,其实全部都可以删除掉 - 合并多余的命令。例如:
自动触发AOF重写
Redis也会在触发阈值时自动去重写AOF文件,阈值的配置在redis.conf
文件中
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-min-size
:AOF文件必须大于这个值,才可能触发AOF重写auto-aof-rewrite-percentage
:当AOF文件 与 上次重写后的文件对比,超过这个百分比后,会触发AOF重写
4. RDB和AOF对比
RDB和AOF两种持久化模式各有各的优缺点。在实际开发中往往是两者结合使用
对比项 | RDB | AOF |
---|---|---|
持久化方式 | 定时对整个内存的数据做快照 | 记录每次执行的写命令 |
数据完整性 | 两次备份之间数据可能丢失 | 丢失数据的可能性较小,取决于刷盘策略 |
文件大小 | 相对较小,且有RDB压缩 | 记录命令,文件相对较大 |
恢复速度 | 很快 | 慢 |
恢复时优先级 | 低 | 高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO较高 但AOF重写时会占用较高的CPU和内存 |
使用场景 | 可能容忍数分钟内的数据丢失, 追求更快的启动速度 | 对数据安全性要求较高时使用 |
三、Redis主从模式
1. 主从模式介绍
单节点的Redis服务支持的并发是有限的:所有客户端的请求全部冲击到仅有的一台服务器上,服务器可能因为压力过大而阻塞不能及时响应
单节点的Redis,只有一个节点,一旦节点宕机,整个服务不可用
所以可以采用主从架构,做到**读写分离**:多个Redis节点共同提供服务,所有写数据的操作都请求到主服务器,所有读数据的操作都请求到从服务器。
如上图所示,集群中有一个master节点、两个slave节点(现在叫replica)。当我们通过Redis的Java客户端访问主从集群时,应该做好路由:
- 如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点
- 如果是读操作,建议访问各个slave节点,从而分担并发压力
2. 主从数据同步原理
2.1 全量同步
发生在什么时候:一个节点第一次变成slave的时候,先进行一次全量同步
全量同步过程
- 第一阶段:建立主从关系
- slave把自己数据集的id(replication id)发送给master
- master收到以后判断 slave 给的 replication id 和自己的 replication id是否相同。不同说明slave是第一次连接master
- master会首先把自己的replication id发送给slave;slave丢弃自己的replication id,接受master replication id
- 第二阶段:开始全量同步
- master执行bgsave,生成RDB。把RDB发送给slave
- slave丢弃自己的所有的旧数据,加载master提供的RDB
- 第三阶段:后续的增量同步
- 在第二阶段全量同步期间,master所执行的新命令会存储到repl_backlog里
- master在第二阶段结束,会把repl_backlog里的命令发送给slave
- slave再执行接收到的命令。最终slave的数据和master同步了
流程图如下:
2.2 增量同步
全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。什么是增量同步?就是只更新slave与master存在差异的部分数据。
增量同步的过程
slave向master发请求,要求同步数据,会给master发送自己的replication id,和上次同步的位置offset
master判断replication id和自己的相同,再判断offset和自己master的offset
- 如果slave的offset 与 master的offset 相差超过一圈:就只能全量同步了
- 如果slave的offset 与master的offset 相差不超过一圈:就从slave的offset开始,把后边的所有命令同步给slave,是增量同步
2.3 repl_backlog原理
master怎么知道slave与自己的数据差异在哪里呢?
这就要说到全量同步时的repl_baklog
文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_backlog实现增量复制的过程
1) Master把写命令存储到repl_backlog里
repl_backlog中会记录Redis处理过的写命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset。slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。
2) Slave同步repl_backlog中的命令
随着不断有数据写入,master的offset逐渐变大。
slave也不断的读取repl_backlog中的命令,同步到slave中,追赶master的offset:
3) repl_backlog填满后环绕从头开始
直到repl_backlog数组被完全填满,如果再有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。
4) Slave落后过多后只能全量同步
但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset;如果master继续写入新数据,其offset就会覆盖旧的数据,直到将slave现在的offset也覆盖:
棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。
注意:repl_backlog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过长,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次进行全量同步
2.4 主从同步优化
主从同步可以保证主从数据的一致性,非常重要。
可以从以下几个方面来优化Redis主从就集群:
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO
- 适当提高repl_backlog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
主从从架构图:
四、Redis哨兵模式
1. 哨兵模式介绍
主从集群存在的问题:故障转移的问题,一旦Master节点宕机,就需要人工干预切换Master主节点。可以增加哨兵解决问题
2. 哨兵模式的架构原理
2.1 哨兵的作用
- 监控:监控主从节点的健康状态
- 故障转移:当主节点宕机时,由哨兵负责选出新的Master
- 通知:通知整个集群有新的Master,构建新的主从关系
2.2 集群监控原理
Sentinel基于心跳机制监测服务状态,定时(每秒1次)向集群的每个实例发送ping命令:
-
主观下线:如果某sentinel节点发现某实例未在规定时间(通过参数配置)响应,则认为该实例主观下线。
-
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
2.3 集群故障恢复原理
选举原则
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
- 然后判断slave节点的slave-priority值(通过参数配置),越小优先级越高,如果是0则永不参与选举
- 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 最后是判断slave节点的runid大小,越小优先级越高(越小表示redis实例启动的越早)。
切换Master
当选出一个新的master后,该如何实现切换呢?
流程如下:
- sentinel给备选的slave节点发送slaveof no one命令,让该节点成为master
- sentinel给所有其它slave发送slaveof 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
- 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
五、Redis分片集群
1. 分片集群介绍
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 主从集群只有一个master具备写数据的能力,存在高并写问题
- 主从集群里各节点数据相同,如果数据太多,就存不下,存在海量数据存储问题
使用分片集群可以解决上述问题
2. 分片集群的架构
分片集群特征:
-
集群里可以有多个master主节点,每个主节点存储一部分数据
-
每个master节点可以有多个slave节点,实现高可用
-
分片集群不需要再有哨兵监控和选举,而是master之间互相心跳监控、实现选举
-
连接任意一个master,master都会帮我们重定向到正确的master上存取数据
3. 散列插槽(Hash插槽)
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- 如果key里没有
{}
:crc16算法(key) % 16384
结果就是最终的hash值。根据hash值判断存储到哪个节点 - 如果key里有
{}
:crc16算法({}里的内容)%16384
结果就是最终的hash值。根据hash值判断存储到哪个节点
例如:key是num,那么就根据num计算;如果是{order}num,则根据order计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
提问:如何将一批key固定的保存在同一个Redis实例上呢?
答案:可以给这些数据的key,增加相同的
{标识}
。例如这些key都以{typeId}
为前缀查看key的slot值:
cluster keyslot key
hash插槽:数据和节点之间没有直接的关联关系,方便进行集群的伸缩与数据的转移
4. 集群伸缩
因为Redis的分片集群,数据并没有与Redis节点直接进行绑定,而是:数据与插槽绑定,插槽与Redis节点绑定。这样就可以很方便的进行集群的伸缩,增加或减少Redis节点。
我们通过下面例子,演示一下给集群增加节点,需要如何实现。要求:向集群中添加一个新的master节点,并向其中存储num = 10
- 启动一个新的Redis实例,端口为7383
- 把7383节点添加到之前的集群,并作为一个master节点
- 给7383节点分配插槽,使得这个num可以存储到7383实例
这需要有两大步:
- 添加一个节点到集群中
- 转移插槽
4.1 添加新节点到集群中
创建7383节点
#创建7383文件夹
mkdir ~/04cluster/7383
#把配置文件拷贝到7383文件夹里
cp ~/04cluster/7380/redis.conf ~/04cluster/7383
#修改配置文件
sed -i s/7380/7383/g ~/04cluster/7383/redis.conf
#启动7383实例
redis-server ~/04cluster/7383/redis.conf
添加到集群
查看redis集群操作帮助:
redis-cli --cluster help
执行命令:
#把7383节点添加到集群
redis-cli --cluster add-node 192.168.150.132:7383 192.168.150.132:7380
#查看集群状态
redis-cli -p 7380 cluster nodes
4.2 转移插槽
查看num的插槽
我们要将num存储到7383节点,因此需要先看看num的插槽是多少:redis-cli -c -p 7380 cluster keyslot 键
我们可以将0~3000插槽,从7380节点移动到7383节点上
转移插槽
转换插槽的命令,操作如下:
-
建立连接:
redis-cli --cluster reshard 192.168.200.136:7380
-
输入要移动的插槽数量,我们输入 3000
-
输入目标节点的id(哪个节点要接收这些插槽,就输入哪个节点的id)
我们从刚刚的输出结果中,找到7383节点的id设置过来
-
从哪个节点里转移出插槽?
输入
all
,表示全部。即从现有每个master节点中各转移一部分出来输入节点id,表示从指定节点中转移出来
输入
done
,表示输入完毕了我们这里从7380节点转移出来一部分,要输入7380节点的id
-
输入
yes
,确认转移
确认结果
执行命令:redis-cli -p 7380 cluster nodes
,可以看到7383节点拥有0~2999插槽,说明转移插槽成功了
5. 故障转移
当master宕机时,Redis的分片集群同样具备故障转移的能力。
接下来给大家演示一下分片集群的故障转换,先确认一下集群的初始状态:
- 7380是master节点,8382是其slave节点
- 7381是master节点,8380是其slave节点
- 7382是master节点,8381是其slave节点
- 7383是master节点,没有slave节点
5.1 自动故障转移
当master宕机时,分片集群会自动将其slave节点提升为master。
我们直接将7381节点关闭:redis-cli -p 7381 shutdown
,
然后查看集群状态 redis-cli -p 7380 cluster nodes
,发现7381是fail状态,8380已经提升成为master
再次启动7381节点:redis-server 7381/redis.conf
再次查看集群状态:redis-cli -p 7380 cluster nodes
,发现7381节点成为了slave
5.2 手动故障转移
利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。
这种failover命令可以指定三种模式:
- 缺省:默认的流程,如图1~6歩
- force:省略了对offset的一致性校验
- takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
其流程如下:
我们以7381这个slave节点为例,演示如何手动进行故障转移,让7381重新夺回master:
- 利用redis-cli连接7381这个节点:
redis-cli -p 7381
- 在7381节点上执行
cluster failover
命令 - 重新查看集群状态:
cluster nodes