redis
Redis
Redis
Redis Cluster
redis cluster的实现原理主要就是三点:分片,Gossip协议和去中心化
数据怎么存?
redis cluster**引入了哈希槽(Hash Slot)**的概念。
它把整个数据集群划分为 16384 个槽。集群里的每个主节点负责维护一部分槽。当我们要存一个 Key 时,Redis 会先对 Key 算一个 CRC16 值,然后对 16384 取模,算出它属于哪个槽,最后把数据存到负责这个槽的节点上。这样做的好处是,扩容缩容非常方便。如果我要加一个新节点,只需要把其他节点身上的一部分槽“过户”给新节点就行了,不需要像传统哈希那样重新计算所有数据的位置。
需要注意的是在redis cluster中每个slot中可以包含很多个key,每个slot只会归属给一个master结点。所以结构是:
slot 0 -> key1, key2, key3, ...
slot 1 -> keyA, keyB, ...
...
slot 16383 -> ...
同时slot也是redis cluster中迁移和负载均衡的最小单位,扩容/缩容都是操作的slot
节点直接如何联系?
集群里的节点是去中心化的,没有所谓的“中心大脑”。它们之间通过 Gossip 协议(八卦协议)互相通信。每个节点都会定期随机选几个邻居,交换彼此的状态信息(比如“我负责哪些槽”、“我是否还活着”)。这样只需要很短的时间,集群的所有节点就能达成一致,知道整个集群的拓扑结构。
客户端如何找数据?
客户端可以连接集群的任意一个主节点。
- 如果客户端要找的 Key 刚好在这个节点上,那就直接返回
- 如果不在,这个节点不会充当代理去转发请求,而是会返回一个 MOVED 错误,告诉客户端:“这个 Key 不归我管,你去连 IP:Port 这个节点吧。”然后客户端会重定向到对应Port的结点回去数据
**详细过程:**如果客户端连接的是集群的 Node1,但需要访问存储在 Node3 的键 user:1001,查询过程如下:
查询过程
- 计算哈希槽:客户端使用 CRC16 算法计算
user:1001的哈希值(假设为 12345)。计算哈希槽:12345 % 16384 = 12345。 - 查询请求:因为客户端连接的是集群中的 node1,所以客户端发送查询命令
GET user:1001到 Node1。 - Node1 响应:Node1 检测到请求的键
user:1001属于 Node3,返回一个MOVED错误,指示客户端请求的键在另一个节点上。MOVED错误会中返回目标节点的信息(例如,Node3 的 IP 和端口) - 重新连接:客户端根据返回的目标节点信息,建立与 Node3 的连接。
- 再次发送查询请求:客户端向 Node3 发送
GET user:1001。 - 获取结果:Node3 查询到
user:1001的值(假设为{"name": "面试鸭", "age": 18}),并返回结果。
Moved和ASK
MOVED 和 ASK 是 Redis Cluster 里两种“重定向机制”,本质都是:
客户端访问错节点了,服务端告诉你该去哪。
正常来说,像Jedis,Lettuce,Redisson这类Redis客户端在启动时会拉取一份slots的映射关系,执行类似CLUSTER SLOTS , CLUSTER NODES的命令,拿到16384个slot -> redis节点的完整映射表,并缓存起来
查询的时候客户端就会通过CRC16(key) % 16384计算出key对应的slot,然后查缓存,看当前slot映射在哪个节点上。再把请求发给对应的redis节点。
但是Redis Cluster支持动态扩容/缩容,导致slot的映射关系并非一成不变。
当映射关系发生变化时再查询时,就会发生两种情况:
- 当前slot已经完全不在这个节点上了,节点会返回一个MOVED重定向指令,告诉客户端应该去查哪个节点,即
MOVED slot ip:port,由于slot已经不在当前节点了,因此客户端要更新本地缓存,以后直接访问新节点 - 当前slot正在迁移,部分key已经搬走,部分key还没搬。如果当前查询的key还在这个节点中,直接返回。如果当前key已经迁移了,会返回一个ASK重定向指令
ASK slot ip:port,直接去新节点查,不用更新缓存
对于ASK,若节点返回如ASK 1000 192.168.1.2:6379时,客户端并不是直接查,而是先发一个ASKING,再查
发ASKING的目的在于:目标节点默认不接受“非自己负责的 slot 请求”,因为当前slot还只是在迁移,目标节点理论上还没正式接管这个槽。这个ASKING就是在告诉他“我知道你还没完全接管这个 slot,但这次让我读一下。”
为什么ASK不需要更新缓存?
因为ASK代表当前slot正在迁移到另一个节点中,slot中部分key完成迁移,部分key仍在当前slot中,仍应该交给当前节点管理。如果此时更新了slot,那么会导致还未迁移过来的这部分key直接查不到了。
并且当前slot理论上还不归目标节点管理,你直接查询,节点会拒绝。
集群里的主从是干嘛的?
redis cluster里的主节点负责处理读写请求,每个主节点负责一部分的slot。而从节点这是只复制对应主结点的数据,主要的作用是故障转移和高可用。
当某个master发生故障的时候,Cluster节点通过Gossip协议发现后达到半数以上master投票,该master下的某个slave自动升级为master,继续负责原来的slot
当然slave节点通过客户端的配置,可以让slave处理读请求,减少读多写少场景下master的压力,提升qps。但可能会读到旧数据。
集群下的自愈功能是如何实施的?
- 首先在Redis Cluster中所有节点基于Gossip协议,会周期性地相互发送PING,收到后回复PONG(master和master,master和slave)
- 如果某个节点在cluster-node-timeout内多次PING未收到对应的PONG,那么当前节点会被认为是主观下线
- 当一个master发现有半数以上的master都将同一个master标记未PFAIL的时候,那么该master就会被认定为客观下线
- 一旦被标记为FAIL(客观下线),那么他负责的slot就会不可用,他的slave开始准备晋升
- slave只有满足一下条件才有可能被选举为新master
- replication offset 足够新(数据不落后太多)
- 与原 Master 断开时间未超过阈值
- 自身状态是
connected - 没有被标记 FAIL
- 每一个合格的slave都会“自荐”,即向所有 Master 发送 FAILOVER_AUTH_REQUEST
- 然后master开始投票,每个master在一次故障转移中只能投一票,会优先投给replication offset 最大的 Slave以及请求最早到达的。当某个节点获得半数以上master的投票则晋升为master
- 晋升为master的slave会接管原master的slot
没有出现获得超过半数的 master 节点投票 的slave不能晋升。防止出现“脑裂”。会在一段时间后重试。如果多次重试仍没有,就需要人工介入
如何理解Cluster的去中心化?
Cluster的去中心化指的是:没有统一负责调度/管理的中心节点,也就是:
- 每个节点地位是平等的
- 任何节点都知道集群拓扑和 slot 分布
- 没有 Sentinel、没有独立的调度中心
master和slave的观念,主要是用于master的故障转移以及高可用,防止master宕机后数据直接不可用,slot直接丢失,集群不可恢复
集群下slave节点是如何同步master的数据的?
第一次同步(全量复制)
当一个 Slave 第一次连上 Master,或者复制中断太久时:
流程是:
- Slave 发送
PSYNC ? -1 - Master 判断无法部分复制
- Master fork 子进程
- 子进程生成 RDB 快照
- RDB 通过网络发送给 Slave
- Slave 加载 RDB 到内存
- 同时:
- Master 把新的写命令缓存到 replication buffer
- RDB 传完后:
- Master 把这段时间的写命令流发给 Slave
- Slave 重放命令,追平数据
核心:RDB + 命令传播
正常运行(增量复制)
之后进入稳定阶段:
- Master 每次写命令
- 直接通过复制连接
- 实时发给 Slave
- Slave 顺序执行
这一步 既不是 RDB,也不是 AOF 文件
为什么是16384个slot?
首先redis cluster采用的是Gossip协议,每个节点需要维护一份slot -> node的映射表
如果slot过大,那么每次 gossip 同步要传大量 slot 信息,使得心跳包变大,网络 IO 暴涨
并且slot信息是用的bitmap存的,1 个 bit 表示 1 个 slot。1代表slot属于我,0代表不属于我。
那么16384 个 slot = 16384 个 bit,按照bit -> byte -> kb的换算就是16384 / 8 / 1024得到2kb。那么这个大小在频繁的gossip下就很舒服,不吃带宽。并且redis的作者也表示:Slot 太多会导致 gossip 协议中的消息变大,Slot 太少又会导致迁移粒度过粗,16384 在实践中是一个非常合理的平衡点
Gossip 协议
Gossip 协议是一种非常像“八卦传播”一样的分布式系统通信协议。
它的核心思想就是:像人传闲话一样,随机找几个节点聊聊天,把消息扩散出去,最后整个集群都会知道这个消息。
想象一下一个村子里有人发现了八卦:
- 他不会挨家挨户去通知所有人(那样太慢太累)。
- 他只告诉了3个朋友。
- 这3个朋友又各自告诉另外3个朋友……
- 没几轮,整个村子就都知道了。
这就是 Gossip 协议的精髓。
基本原理(三种常见模式)
1)Push 模式(主动推送) 节点 A 知道了新消息,就随机挑几个节点,把消息直接推过去。被推的节点再随机推给其他人。
像病毒一样主动传播。
2)Pull 模式(主动拉取) 节点定期互相打招呼:“喂,你最近有什么新八卦吗?”如果对方有自己没有的消息,就拉过来。
像大家定期聚会交换小道消息。
3)Push+Pull 混合模式(最常用)
既主动推,也定期拉,传播速度最快,也最可靠。
Gossip 协议的优缺点
| 方面 | Gossip 协议的表现 | 说明与实际影响 |
|---|---|---|
| 去中心化 | ★★★★★ 完全无中心 | 无单点故障,所有节点对等 |
| 容错能力 | ★★★★★ 极强 | 节点批量宕机、网络分区、抖动都不影响最终收敛,网络恢复后自动愈合 |
| 可扩展性 | ★★★★★ 几乎线性扩展 | 从 10 台到 10 000 台都不需要改架构,几千节点集群通常 3–10 秒全网同步 |
| 实现与维护难度 | ★★★★★ 极其简单 | 核心代码通常几十行,容易理解、调试和修改 |
| 负载均衡 | ★★★★★ 天然均衡 | 每个节点发收消息量基本一致,不会出现热点 |
| 传播延迟 | ★★★☆☆ 中等(最终一致性) | 几秒到几十秒收敛,不适合毫秒级强一致性场景 |
| 消息冗余/带宽消耗 | ★★☆☆☆ 较高 | 同一条更新会被多次传播,带宽利用率不是最优(现代 10G+ 网络基本无压力) |
| 一致性强度 | ★★☆☆☆ 只能保证最终一致性 | 不提供强一致性保证,不能用于需要顺序性或即时性的关键操作(如扣款、锁) |
| 故障检测速度 | ★★★★☆ 较快 | 通常几秒就能检测到节点失联,比传统中心化心跳更快更准 |
| 适用场景匹配度 | 集群成员管理、故障检测、服务发现、配置广播、元数据同步:★★★★★ 金融交易、分布式锁、顺序操作:★☆☆☆☆ | 几乎是“集群八卦与状态同步”这一类场景的首选方案 |
Redis Sentinel
简介:Redis 的哨兵机制(Sentinel) 是一种高可用性解决方案,用于监控 Redis 主从集群,自动完成主从切换,以实现故障自动恢复和通知。
主要功能包括:
- 监控:哨兵不断监控 Redis 主节点和从节点的运行状态,定期发送 PING 请求检查节点是否正常。
- 自动故障转移:当主节点发生故障时,哨兵会选举一个从节点提升为新的主节点,并通知客户端更新主节点的地址,从而实现高可用。
- 通知:哨兵可以向系统管理员或其他服务发送通知,以便快速处理 Redis 实例的状态变化。
哨兵机制的由来
主从架构中,如果采用读写分离的模式,即主节点负责写请求,从节点负责读请求。假设这个时候主节点宕机了,没有新的主节点顶替上来的话,就会出现很长一段时间写请求没响应的情况。
针对这个情况,便出现了哨兵这个机制。它主要进行监控作用,如果主节点挂了,将从节点切换成主节点,从而最大限度地减少停机时间和数据丢失。
- 哨兵节点(Sentinel): 主要作用是对 Redis 的主从服务节点进行监控,当主节点发生故障的时候,哨兵节点会选择一个合适的从节点升级为主节点,并通知其他从节点和客户端进行更新操作。
- Redis 节点:主要包括 master 以及 slave 节点,就是 Redis 提供服务的实例。
一般哨兵需要集群部署,至少三台哨兵组成哨兵集群。
主观下线/客观下线
哨兵是如何判断 Redis 中主节点挂了的呢?主要涉及到了两个机制:主观下线以及客观下线。
1)主观下线
Sentinel 每隔 1s 会发送 ping 命令给所有的节点。如果 Sentinel 在down-after-milliseconds时间内还未收到对应节点的 pong 回复,就会认为这个节点主观下线SDOWN(Subjectively Down)。
2)客观下线
注意,只有主节点才有客观下线,从节点没有。
假设目前有个主节点被一个 sentinel 的判断主观下线了,但可能主节点并没问题,只是因为网络抖动导致了一台哨兵的误判。所以此时哨兵需要问问它的队友,来确定这个主节点是不是真的出了问题!
Sentinel会通过 __sentinel__:hello 频道,定期广播:自己的存在和自己看到的Master状态
当某个Sentinel发现有 达到 quorum 数量的 Sentinel都认为同一个 Master 是 SDOWN时那么该master则会被标记为:ODOWN(Objectively Down)
认为下线的总投票数大于 quorum(一般为集群总数/2 + 1,假设哨兵集群有 3 台实例,那么 3 / 2 + 1 = 2),则判定该主节点客观下线,此时就需要进行主从切换,而只有哨兵的 leader 才能操作主从切换。
Sentinel leader 是如何选举出来的?
Sentinel leader 节点的选举实际上涉及到分布式算法 raft,这里主要简单说一下哨兵集群选择 leader 的方式:
判断主节点主观下线的 sentinel 就是候选者,此时它想成为 leader。如果同时有两个 sentinel 判断主观下线,那么它们都是候选人,一起竞争成为 leader。
候选者们会先投自己一票,然后向其他 sentinel 发送命令让它们给自己投票。每个哨兵手里只有一票,投了一个之后就不能投别人了。
最后,如果某个候选者拿到哨兵集群半数及以上的赞成票,就会成为 leader。这里有一个注意的点,为了保证 sentinel 选举的时候尽量避免出现平票的情况,sentinel 的节点个数一般都会是奇数,比如 3,5,7 这样。
Redis 主节点选举
选出哨兵 leader 之后,需要选出 Redis 主从集群中的新 master 节点。
首先需要把一些已经下线的节点全部剔除,然后从正常的从节点中选择主节点,其主要经过以下三个流程:
- 根据从节点的优先级进行选择,优先选择优先级的值比较小的节点(优先级的值越小优先级越高,优先级可通过
slave-priority配置)。 - 如果节点的优先级相同,则查看进行主从复制的 offset 的值,即复制的偏移量,偏移量越大则表示其同步的数据越多,优先级越高。
- 如果 offset 也相同了,那只能比较 ID 号,选择 ID 号比较小的那个作为主节点(每个实例 ID 不同)。
选好主节点之后,哨兵 leader 会让其他从节点全部成为新 master 节点的 slave 节点。
最后利用 redis 的发布/订阅机制,把新主节点的 IP 和端口信息推送给客户端,此时主从切换就结束了。
旧主节点恢复了怎么办?实际上哨兵会继续监视旧的主节点,如果它上线了,哨兵集群会向它发送 slaveof 命令,让它成为新主节点的从节点。
Redis Cluster和Redis Sentinel的区别
1)Redis Cluster 是 Redis 集群,提供自动分片功能,将数据自动分布在多个节点上,支持自动故障转移。如果一个节点失败,集群会自动重新配置和平衡,不需要外部介入,因为它内置了哨兵逻辑。
2)Sentinel 是哨兵,主要用于管理多个 Redis 服务器实例来提高数据的高可用性。当主节点宕机,哨兵会将从节点提升为主节点,它并不提供数据分片功能。
如果需要处理大量数据并进行数据分片,应选择 Redis Cluster,它支持水平扩展,适用于大规模数据、高吞吐量场景。
如果只是为了提高 Redis 实例的可用性,并不需要数据分片,应选择主从 + Sentinel,它主要关注故障转移和实例高可用,适用于高可用性、读写分离场景。
脑裂
脑裂是指在分布式系统中,由于网络分区或其他问题导致系统中的多个节点(特别是主节点)误以为自己是唯一的主节点。这种情况会导致多个主节点同时提供写入服务,从而引起数据不一致。
分布式系统就像一个团队在干活,如果发生了脑裂,就好比这个团队突然因为某些原因,比如通信出了问题,分成了几个小团体。
每个小团体都以为自己是整个团队,都在按自己的方式工作,各自为政,对同一件事有不同的决策和做法,就像有的说要这么干,有的说要那么干。
这样一来,整个系统就乱套了,数据也可能变得不一致,服务也变得不正常了,这就是分布式系统中的脑裂。
导致脑裂出现原因主要是网络分区。
redis里的脑裂主要指的是Sentinel模式下,在网络分区时,Sentinel联系不上master,然后发起选举,选了新的主节点,此时 Redis 就出现了两个主节点。此时客户端写哪都会导致数据不一致。
为什么Redis Cluster下不容易发生脑裂呢?
- slot只会被一个master合法持有
- 少数派 Master 会自动拒绝写请求,当一个 Master发现自己联系不到多数 Master,会进入cluster_state: fail,直接拒绝写。
- Cluster 的“自我熔断”机制,所以即使发生网络分区,只有多数派那一侧还能写,少数派 Master 不会对外写,少数派 = 自动降级为只读