1.Redis主从
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
1.1.主从集群结构
下图就是一个简单的Redis主从集群结构:
编辑
如图所示,集群中有一个master节点、两个slave节点(现在叫replica)。当我们通过Redis的Java客户端访问主从集群时,应该做好路由:
- 如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点
- 如果是读操作,建议访问各个slave节点,从而分担并发压力
1.2.主从同步原理
1.2.1.全量同步
主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点,流程:
编辑
这里有一个问题,master如何得知salve是否是第一次来同步呢??
有几个概念,可以作为判断依据:
Replication Id:简称replid,是数据集的标记,replid一致则是同一数据集。每个master都有唯一的replid,slave则会继承master节点的replidoffset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据。
由于我们在执行slaveof命令之前,所有redis节点都是master,有自己的replid和offset。
当我们第一次执行slaveof命令,与master建立主从关系时,发送的replid和offset是自己的,与master肯定不一致。
master判断发现slave发送来的replid与自己的不一致,说明这是一个全新的slave,就知道要做全量同步了。
master会将自己的replid和offset都发送给这个slave,slave保存这些信息到本地。自此以后slave的replid就与master一致了。
因此,master 判断一个节点是否是第一次同步的依据,就是看replid是否一致。流程如图:
编辑
完整流程描述:
slave节点请求增量同步master节点判断replid,发现不一致,拒绝增量同步master将完整内存数据生成RDB,发送RDB到slaveslave清空本地数据,加载master的RDBmaster将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slaveslave执行接收到的命令,保持与master之间的同步
1.2.2.增量同步
全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步。
什么是增量同步?就是只更新slave与master存在差异的部分数据。如图:
编辑
那么master怎么知道slave与自己的数据差异在哪里呢?
1.2.3.repl_baklog原理
master怎么知道slave与自己的数据差异在哪里呢?
这就要说到全量同步时的repl_baklog文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_baklog中会记录Redis处理过的命令及offset,包括master当前的offset,和slave已经拷贝到的offset:
编辑
slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。
随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset:
编辑
直到数组被填满。
此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分:
编辑
但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset:
编辑
如果master继续写入新数据,master的offset就会覆盖repl_baklog中旧的数据,直到将slave现在的offset也覆盖:
编辑
棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。
repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于repl_baklog做增量同步,只能再次全量同步。
1.3.主从同步优化
主从同步可以保证主从数据的一致性,非常重要。
可以从以下几个方面来优化Redis主从就集群:
- 在master中配置
repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。 - Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
- 适当提高
repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步 - 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用
主-从-从链式结构,减少master压力
主-从-从架构图:
编辑
简述全量同步和增量同步区别?
- 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
- 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么时候执行全量同步?
- slave节点第一次连接master节点时
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时
什么时候执行增量同步?
- slave节点断开又恢复,并且在
repl_baklog中能找到offset时
2.Redis哨兵
主从结构中master节点的作用非常重要,一旦故障就会导致集群不可用。那么有什么办法能保证主从集群的高可用性呢?
2.1.哨兵工作原理
Redis提供了哨兵(Sentinel)机制来监控主从集群监控状态,确保集群的高可用性。
2.1.1.哨兵作用
哨兵集群作用原理图:
编辑
哨兵的作用如下:
- 状态监控:
Sentinel会不断检查您的master和slave是否按预期工作 - 故障恢复(failover) :如果
master故障,Sentinel会将一个slave提升为master。当故障实例恢复后会成为slave - 状态通知:
Sentinel充当Redis客户端的服务发现来源,当集群发生failover时,会将最新集群信息推送给Redis的客户端
那么问题来了,Sentinel怎么知道一个Redis节点是否宕机呢?
2.1.2.状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个节点发送ping命令,并通过实例的响应结果来做出判断:
- 主观下线(sdown) :如果某sentinel节点发现某Redis节点未在规定时间响应,则认为该节点主观下线。
- 客观下线(odown) :若超过指定数量(通过
quorum设置)的sentinel都认为该节点主观下线,则该节点客观下线。quorum值最好超过Sentinel节点数量的一半,Sentinel节点数量至少3台。
如图:
编辑
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短,如果超过
down-after-milliseconds * 10则会排除该slave节点 - 然后判断slave节点的
slave-priority值,越小优先级越高,如果是0则永不参与选举(默认都是1)。 - 如果
slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高 - 最后是判断slave节点的
run_id大小,越小优先级越高(通过info server可以查看run_id)。
对应的官方文档如下:
问题来了,当选出一个新的master后,该如何实现身份切换呢?
大概分为两步:
- 在多个
sentinel中选举一个leader - 由
leader执行failover
2.1.3.选举leader
首先,Sentinel集群要选出一个执行failover的Sentinel节点,可以成为leader。要成为leader要满足两个条件:
- 最先获得超过半数的投票
- 获得的投票数不小于
quorum值
而sentinel投票的原则有两条:
- 优先投票给目前得票最多的
- 如果目前没有任何节点的票,就投给自己
比如有3个sentinel节点,s1、s2、s3,假如s2先投票:
- 此时发现没有任何人在投票,那就投给自己。
s2得1票 - 接着
s1和s3开始投票,发现目前s2票最多,于是也投给s2,s2得3票 s2称为leader,开始故障转移
不难看出,谁先 投票 ,谁就会称为 leader,那什么时候会触发投票呢?
答案是第一个确认 master 客观下线的人 会立刻发起 投票 ,一定会成为 leader。
OK,sentinel找到leader以后,该如何完成failover呢?
2.1.4.failover
我们举个例子,有一个集群,初始状态下7001为master,7002和7003为slave:
编辑
假如master发生故障,slave1当选。则故障转移的流程如下:
1)sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
编辑
2)sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些节点成为新master,也就是7002的slave节点,开始从新的master上同步数据。
编辑
3)最后,当故障节点恢复后会接收到哨兵信号,执行slaveof 192.168.150.101 7002命令,成为slave:
编辑
2.2.总结
Sentinel的三个作用是什么?
- 集群监控
- 故障恢复
- 状态通知
Sentinel如何判断一个redis实例是否健康?
- 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线(
sdown) - 如果大多数sentinel都认为实例主观下线,则判定服务客观下线(
odown)
故障转移步骤有哪些?
- 首先要在
sentinel中选出一个leader,由leader执行failover - 选定一个
slave作为新的master,执行slaveof noone,切换到master模式 - 然后让所有节点都执行
slaveof新master - 修改故障节点配置,添加
slaveof新master
sentinel选举leader的依据是什么?
- 票数超过sentinel节点数量1半
- 票数超过quorum数量
- 一般情况下最先发起failover的节点会当选
sentinel从slave中选取master的依据是什么?
- 首先会判断slave节点与master节点断开时间长短,如果超过
down-after-milliseconds`` * 10则会排除该slave节点 - 然后判断slave节点的
slave-priority值,越小优先级越高,如果是0则永不参与选举(默认都是1)。 - 如果
slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高 - 最后是判断slave节点的
run_id大小,越小优先级越高(通过info server可以查看run_id)。
3.Redis分片集群
主从模式可以解决高可用、高并发读的问题。但依然有两个问题没有解决:
- 海量数据存储
- 高并发写
要解决这两个问题就需要用到分片集群了。分片的意思,就是把数据拆分存储到不同节点,这样整个集群的存储数据量就更大了。
Redis分片集群的结构如图:
编辑
分片集群特征:
- 集群中有多个master,每个master保存不同分片数据 ,解决海量数据存储问题
- 每个master都可以有多个slave节点 ,确保高可用
- master之间通过ping监测彼此健康状态 ,类似哨兵作用
- 客户端请求可以访问集群任意节点,最终都会被转发到数据所在节点
3.1.散列插槽
数据要分片存储到不同的Redis节点,肯定需要有分片的依据,这样下次查询的时候才能知道去哪个节点查询。很多数据分片都会采用一致性hash算法。而Redis则是利用散列插槽(hash slot)的方式实现数据分片。
详见官方文档:
在Redis集群中,共有16384个hash slots,集群中的每一个master节点都会分配一定数量的hash slots。具体的分配在集群创建时就已经指定了:
编辑
如图中所示:
- Master[0],本例中就是7001节点,分配到的插槽是0~5460
- Master[1],本例中就是7002节点,分配到的插槽是5461~10922
- Master[2],本例中就是7003节点,分配到的插槽是10923~16383
当我们读写数据时,Redis基于CRC16 算法对key做hash运算,得到的结果与16384取余,就计算出了这个key的slot值。然后到slot所在的Redis节点执行读写操作。
不过hash slot的计算也分两种情况:
- 当
key中包含{}时,根据{}之间的字符串计算hash slot - 当
key中不包含{}时,则根据整个key字符串计算hash slot
例如:
- key是
user,则根据user来计算hash slot - key是
user:{age},则根据age来计算hash slot
3.2.故障转移
分片集群的节点之间会互相通过ping的方式做心跳检测,超时未回应的节点会被标记为下线状态。当发现master下线时,会将这个master的某个slave提升为master。
这就相当于分片集群的master兼职了哨兵
3.3.总结
Redis分片集群如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例
- 根据key计算哈希值,对16384取余
- 余数作为插槽,寻找插槽所在实例即可
如何将同一类数据固定的保存在同一个Redis实例?
- Redis计算key的插槽值时会判断key中是否包含
{},如果有则基于{}内的字符计算插槽 - 数据的key中可以加入
{类型},例如key都以{typeId}为前缀,这样同类型数据计算的插槽一定相同