主从 master-slave
redis主从模式(master-slave,为保政治正确,已改名master-replica),提供了除持久化外另一种数据的热备功能,也为读写分离提供了途径;
redis主从模式通过复制功能实现,redis提供了SLAVEOF(REPLICAOF)
,让一个服务器(slave)去复制另一个服务器(master);
复制功能的开启: 通过客户端向服务器发送指令:slaveof(replicaof) host port
,或者在slave配置文件中配置 replicaof
选项;
注: slaveof(replicaof) host port
是异步命令,当服务器收到该命令后,会先将host/port保存到服务器状态(redisServer)的masterhost
及masterport
属性里,然后回复 OK,再开始执行真正的复制操作;
redis的复制功能的实现,包括 同步(SYNC/PSYNC) 和 命令传播(command propagate) 两个操作:
- 在刚开启主从同步功能或者断线重连时,使用同步命令让slave的数据状态跟master保持一致;
- 同步后,若master执行写命令,状态又将不一致,通过命令传播让slave执行同样的命令,使状态保持一致;
同步:
- SYNC命令:redis2.8之前使用,slave向master发送
sync
命令,master会执行bgsave
命令生成rdb文件,然后把rdb文件发送给slave,slave通过rdb恢复数据,达到与master一致的状态; - PSYNC命令:因为SYNC命令只能执行全量同步,对于slave断线重连后只需要执行部分重同步就可以达到一致的情况,仍使用SYNC命令有很大的浪费,因为在redis2.8中,提供了
PSYNC
命令来取代SYNC
命令
PSYNC
PSYNC提供了全量同步和部分同步两种模式,全量同步跟 SYNC
命令类似
- 为实现数据的部分重同步,redis使用了一些定义
- 复制偏移量:master和slave都会维护一个复制偏移量
- master 每发送一个字节,将偏移量+1
- slave 每收到一个字节,将偏移量+1
- 复制积压缓冲区
- master维护的一个 固定长度 的FIFO队列,默认1M;存放了一部分最近传播的写命令,并且为每个字节记录了相应的复制偏移量;
- 服务器支行ID: 每个redis服务器,不论主从,都有自己的支行ID,在启动服务时生成,由40个随机的16进制字符串组成
- 有了以上三个定义,就可以实现部分同步功能了:
- slave 向 master 发送命令
PSYNC <runid> <offset>
,其中 runid 表示上次复制的 master 的运行ID,offset 则是slave保存的复制偏移量;如果是第一次同步,则用?
和-1
表示:PSYNC ? -1
; - master 收到命令后,查检是第首次同步、收到的runid是否是自身的runid、根据偏移量检查缺失的数据是否全部在复制积压缓冲区中,然后会回复:
-
+FULLRESYNC <runid> <offset>
表示将执行全量同步; -
+CONTINUE
表示将执行增量同步,slave 只需等待 master 继续将增量数据发过来即可;注:当runid相同且缺失和数据都仍在复制积压缓冲区时才会执行部分同步;
-
-ERR
表示 master 版本低于2.8,不能识别PSYNC
命令;
心跳检测
在命令传播阶段,slave 会定时向 master 发送心跳信息,默认每秒一次,命令格式: REPLCONF ACK <offset>
- 作用:
- 检测网络通信状态;
- 辅助实现 min-slave 选项;
- 检测命令丢失: 心跳信息会带有slave的offset,master收到心跳后,可以与自己保存的offset对比,大与收到的offset,说明有命令传播失败;
- 步骤:
- slave 收到
SLAVEOF <host> <port>
命令,设置 master 的地址端口信息,然后回复OK; - 向 master 建立套接字连接;
- 发送 PING 命令;
- 身份验证,如果 slave 有 masterauth 配置;
- slave 发送自身端口信息;
- 进行同步;
- 命令传播;
注:master 和 slave 需要互为对方的客户端,因为彼此都要向对方发送命令;
哨兵 Sentinel
master-slave方案解决了数据的复制问题,但是 当 master 宕机时,slave 并不会自动切换为新的 master,以继续提供服务,于是,Sentinel System 有了用武之地;
由一个或多个 Sentinel 实例组成的 Sentinel System,可以监视任意多个 master 及其 slave,并行使以下职责:
- 监视各服务器运行状态;
- 发现异常进行通知;
- master 下线后从其 slave 中选举新 master,进行故障转移;
- 下线的 master 上线后,将其降级成新 master 的 slave;
先进行哨兵功能的总结:
- 每个 Sentinel 可监视任意多个服务器,每个服务器也可被任意多个 Sentinel 监视;
- 多个监视同一主服务器的 Sentinel 视为一个集群,在被监视主服务器下线后,该集群将选举出一个 Sentinel Leader,由该 leader 对其进行故障转移;
故障转移:
- 选举 Sentinel leader,用于执行故障转移;
- 由 Sentinel leader 从故障主服务器的所有从服务器中选一个做新的主服务器;
- 向选出的新 master 发送
slaveof no one
命令,然后一次次的发送INFO
命令查看服务器的role角色,当变成 master 说明升级成功; - 向其它slave服务器发送
slaveof
命令,让它们复制新的服务器; - 若下线的主服务器上线,则发送
slaveof
命令,让其降级为从服务器,复制新的主服务器;
Sentinel节点感知:
- Sentinel 会向被监视服务器发起两条连接,一条命令连接,一条订阅连接;
- 建立订阅频道后,会在被监视服务器上订阅
—sentinel—:hello
频道,并以 2S 一次的频率,向该频道发送消息,以向其它监视该服务器的Sentinel宣示自己的存在; - Sentinel 通过接收订阅消息并分析,获知与自己监视同一服务器的其它 Sentinel,并在被监视主服务器下线时,与其它 Sentinel 进行选举选出leader,进行故障转移;
判断下线(主观下线、客观下线):
- Sentinel 以每秒一次的频率向其它实例(包括主、从、其它Sentinel)发送PING,并根据回复判断服务器是否在线,当一个实例在指定的次数中不能返回有效回复时,会将这个服务器判断为 主观下线
- 判断一个主服务器进入主观下线后,向同样监视这个主服务器的其它Sentinel询问,看是否同意这个主服务器进入主观下线状态;
- 当足够多的 Sentinel 判断主服务器进入主观下线后,将这个主服务器判断为 客观下线
- 发现主服务器进入客观下线状态后,发起一次故障转移操作;
选举哨兵头领 Sentinel Leader:
- 所有节点都会广播一条设置信息,要求所有节点调自己为局部leader;
- 收到广播的节点,若尚未设置自己的局部leader,则按广播设置其为自己的局部leader,并回复OK,否则回复失败;
- 广播的节点按收到的回复,比对是否同一纪元,统计回复OK的数量,若超过半数,则成为全局Leader;
- 若一纪元没有一个节点获得半数以上,则休眠一个随机时间,纪元加一,再次选举,直到选出全局leader;
选出新的主服务器:
- 排除下线或者断线的从服务器;
- 排除最近5秒没有回复过 leader Sentinel 的 INFO 命令的从服务器;
- 排除与已下线服务器连接断开超过
down-after-millinseconds * 10
的从服务器; - 根据从服务器优先级,对剩余从服务器排序,选出优先级最高的;
- 若优先级最高的有多个,选出其中复制偏移量最大的;
- 若优先级最高的、偏移量最大的,仍有多个,则按
runid
,选最小的;
集群 cluster
通过主从与哨兵,redis即可实现高可用,然额,仍然存在一个问题,单台服务器的内存是有限的,不够用怎么办?redis有缓存淘汰机制,可以解决一部分问题,但业务需求是无限的,当不能过期与淘汰的数据大到一台主机不够用时,怎么办呢?SO,跟所有其它分布式系统一样,还需要横向扩展能力,幸好,自redis3.0开始,开始提供集群功能;
启动集群: 当redis 服务器以集群模式启动时,即成为一个 节点 ,默认运行在一个只包含自己的集群中,使用cluster meet <host> <port>
命令,可以让服务器把指定的节点加入到自己所在的集群中,假设向服务器 A 发送命令:cluster meet 127.0.0.1 12345
,假设监听端口12345 的服务器为B,那么节点A和节点B将首先进行 握手 :
- A 为B 创建一个clusterNode结构,并添加到自己的clusterState.nodes字典中;
- A 根据
cluster meet
命令指定的地址和端口,向节点B发送meet消息; - B 节点收到 A 的消息,为节点A创建一个clusterNode结构,并添加到自己的clusterState.nodes结构中,然后向 A 回复一条
PONG
消息; - A 收到
PONG
消息可知 B 已收到自己的握手消息,再次发一条确认消息PING
; - B 收到 A 发送的
PING
消息,握手完成; - 完成握手后,A 会通过Gossip协议向集群内的其它节点发送节点B的消息,其它服务器也会与B进行握手,最终,集群内所有服务器都将感知其它服务器,保存有其它服务的clusterNode结构,并与之建立通信;
槽指派: redis 集群内部,通过分片的方式来保存键值数据,每个分片称之为一个槽(slot),共有0-16383共计16384个槽(2048 * 8); 集群建立后,处于未上线状态,需要进行槽指派后,才上线并开始提供服务;
集群命令执行: 集群中因为数据分片存储,执行命令的过程稍微有一点差异:收到命令后,先对key进行hash映射取得该Key的槽号,然后判断该槽是否归自己处理,若是则执行命令,否则取得负责该槽的节点,返回一个MOVED
错误,并把该节点信息返回,引导客户端向正确的节点请求服务;
- 该槽归自己处理,进行处理;
- 该槽不归自己处理,返回一个
MOVED
错误,并把负责处理该槽的节点信息返回,引导客户端向正确的节点请求服务; - 该槽数据目前正在迁移:
- 首先在自己的库中查找该键,若存在,处理;
- 若不存在,返回ASK错误,并给出新节点信息,引导客户端向新节点请求服务;客户端需要先向新节点发送
asking
命令,再发送正式的命令,否则将会收到一个moved
错误
注:正在被导入的槽和数据,不算归自己管,只有导入完成后,才会集群内广播
关于集群的内部结构:
- clusterNode: 表示一个节点
- clusterLink: 表示一个节点的连接信息
- clusterState: 每个节点都保存着一个clusterState,记录了以当前节点为视角,集群目前所处的状态,如是否在线、包含多少节点、当前配置纪元等等;其中两个属性记录了所有槽指派信息:
- clusterState.slots: char数组,长度2048(16384/8),用每一个二进制位表示一个槽是否归当前节点处理,为0表示不归我管;
- clusterState.numslots:当前节点负责处理的槽的数量,即slots数组中位的值为1的数量;
- clusterState.nodes:clusterNode结构数组,长度16384
为什么是16384个槽 由记录槽指派信息的结构可知,其实是2048,redis节点维护一个长度2048的char数组,用数组元素中的每一位表示一个槽,而char的长度为8位,于是共可以存储2048*8=16384个槽;