3.redis分布式篇

1.主从复制的原理:

1) 连接阶段

1.slave node 启动时(执行 slaveof 命令),会在自己本地保存 master node 的信息,包括 master node 的 host 和 ip。

2.slave node 内部有个定时任务 replicationCron(源码 replication.c),每隔 1秒钟检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立socket 网络连接,如果连接成功,从节点为该 socket 建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。当从节点变成了主节点的一个客户端之后,会给主节点发送 ping 请求。

2)数据同步阶段

1.全量复制:master node 第一次执行全量复制,通过 bgsave 命令在本地生成一份 RDB 快照,将 RDB 快照文件发给 slave node(如果超时会重连,可以调大 repl-timeout 的值)。slave node 首先清除自己的旧数据,然后用 RDB 文件加载数据。

2.问题:生成 RDB 期间,master 接收到的命令怎么处理?开始生成 RDB 文件时,master 会把所有新的写命令缓存在内存中。在 slave node保存了 RDB 之后,再将新的写命令复制给 slave node。

3)命令传播阶段

1.原理:master node 持续将写命令,异步复制给 slave node。

2.延迟:延迟是不可避免的,只能通过优化网络。

3.延迟的配置:repl-disable-tcp-nodelay  参数---》当设置为 yes 时,TCP 会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与 Linux 内核的配置有关,默认配置为40ms。当设置为 no 时,TCP 会立马将主节点的数据发送给从节点,带宽增加但延迟变小。

4.延迟的配置应用:一般来说,只有当应用对 Redis 数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为 yes;多数情况使用默认值 no。

5.问题:如果从节点有一段时间断开了与主节点的连接是不是要重新全量复制一遍?如果可以增量复制,怎么知道上次复制到哪里?

通过 master_repl_offset 记录的偏移量

master_repl_offset

4)主从复制的缺点:

主从模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足:1、RDB 文件过大的情况下,同步非常耗时。2、在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,这个比较费时费力,还会造成一定时间的服务不可用。

2.可用性保证之 Sentinel(哨兵)

1)原理

1.思路: 通过运行监控服务器来保证服务的可用性。

2.Sentinel流程: 为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 既监控所有的 Redis 服务,Sentinel 之间也相互监控。

Sentinel流程图

3.注意点: Sentinel 本身没有主从之分,只有 Redis 服务节点有主从之分。

2)如何判断服务下线?

1.主观下线:Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线!

主观下线图

2.客观下线: Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master!

3)故障转移

1.原理:如果 master 被标记为客观下线,就会开始故障转移流程。故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinle 通过 Raft 算法,实现 Sentinel 选举。

2.问题1怎么让一个原来的 slave 节点成为主节点? 1)选出 Sentinel Leader 之后,由 Sentinel Leader 向某个节点发送 slaveof no one命令,让它成为独立节点。2)然后向其他节点发送 slaveof x.x.x.x xxxx(本机服务),让它们成为这个节点的子节点,故障转移完成。

3.问题2:这么多从节点,选谁成为主节点? 关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程 id 最小的那个。

4)Raft算法

1.目的:通过复制的方式,使所有节点达成一致!

2.步骤:领导选举,数据复制

3.核心思想:先到先得,少数服从多数。

4.演示:http://thesecretlivesofdata.com/raft/

5.总结:1)master 客观下线触发选举,而不是过了 election timeout 时间开始选举。2)Leader 并不会把自己成为 Leader 的消息发给其他 Sentinel。其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程

5)哨兵机制的不足

1.主从切换的过程中会丢失数据,因为只有一个 master。只能单点写,没有解决水平扩容的问题。如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。数据怎么分片?分片之后,怎么实现路由?且看下一节Redis 分布式方案!

3.Redis 分布式方案

1)Redis 数据的分片的方案:

1.客户端实现相关的逻辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由。

2.做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。

3.基于服务端实现。

2)客户端-Sharding

1.代码展示:

实际应用

2.shardjedis分片的原理

1)采用一致性hash算法----》数据分布均匀的解决方案

2)关键点:实现hash环,创建虚拟结点,顺时针定位第一个结点

3)关键代码:

jedis实现

在getResource()中,获取了一个Jedis实例。它最终调用了redis.clients.util.Sharded类的initialize()方法。

initialize()

在jedis.getShard(“k”+i).getClient()获取到真正的客户端。

定位结点

Sharded用红黑树实现了哈希环。客户端从哈希环中获取Redis节点的信息,虚拟节点也是映射到对应的Redis实例。

3)代理服务-codis

4)服务端-Redis Cluster

5)如何保证数据分布均匀

1.哈希后取模:例如,hash(key)%N,根据余数,决定映射到那一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的 N 发生变化,数据需要重新分布。

2.一致性哈希:把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。因为是环形空间,0 和 2^32-1 是重叠的。

机器的名称或 者 IP 计算哈希值,然后分布到哈希环中
沿哈希环顺时针找到的第一个 Node,就是数据存储的节点


新增了一个 Node5 节点,不影响数据的分布


删除了一个节点 Node4,只影响相邻的一个节点                     

问题:一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)

引入虚拟结点之前
引入虚拟结点之后

3.Redis 虚拟槽分区

原理:Redis 创建了 16384 个槽(slot),每个节点负责一定区间的 slot。比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383。

原理图

怎么标记:Redis 的每个 master 节点维护一个 16384 位(2048bytes=2KB)的位序列,比如:序列的第 0 位是 1,就代表第一个 slot 是它负责;序列的第 1 位是 0,代表第二个 slot不归它负责。对象分布到 Redis 节点上时,对 key 用 CRC16 算法计算再%16384,得到一个 slot的值,数据落到负责这个 slot 的 Redis 节点上。

问题:怎么让相关的数据落到同一个节点上?---》比如有些 multi key 操作是不能跨节点的,如果要让某些数据分布到一个节点上,例如用户 2673 的基本信息和金融信息,怎么办?

在 key 里面加入{hash tag}即可。Redis 在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。

例:user{2673}base=...   user{2673}fin=...

你可能感兴趣的:(3.redis分布式篇)