1 说法一
因为CRC16会输出16bit的结果,可以看作是一个分布在 0~2^16-1 之间的数,redis的作者测试发现这个数对 2^14 求模的会将key在 0-2^14-1 之间分布得很均匀, 2^14 即16384
2 说法二
为了节省存储空间,每个节点用一个Bitmap记录节点对应的槽,2k = 210248 = 16384,也就是说,每个节点用2k的内存空间,总共16384个比特位,就可以存储该结点对应了哪些槽。
Bitmap记录节点对应的槽有两个作用:
- 判断key是不是归于当前节点,当一个客户端发送来一个key时,通过CRC(16) / 16384 计算出该key归属的槽solt,然后计算检查 (myslots[slot>>3] >> (slot&7)) & 1,一条位运算指令判断key是不是归于当前节点。[只需要了解即可,不用追究位运算细节]
- redis Cluster 节点之间通过 Gossip 消息同步集群状态。每个节点需要定期广播 自己负责的槽位集合,以便其他节点更新全局槽映射表。如果直接发送槽号列表(如 [0,1,2,…5000])需要成千上万个整数(几 KB 到几十 KB),而发送一个 2KB 的位图 就能完整表示所有槽的归属,网络开销小且固定。
补充:
- 全局槽映射表:它是一个长度为16384的数组,数组的下标是槽号(0~16383),对应的值是该槽归属的主节点信息(通常是指向节点结构体的指针,或者节点ID)。用途:客户端请求到达任意节点时,节点查这个表知道该把请求转发给谁(或返回 MOVED)。
问:已经有全局槽映射表为什么还需要bitmap表示节点自身有哪些槽位?
很好的问题!表面上看确实有冗余,但两者服务不同的场景和需求,同时存在是为了性能、消息传输和代码逻辑的清晰。
1. 本质上的信息重叠与用途分离
- 全局槽映射表(
slots[16384]):记录每个槽由哪个节点负责。
用途:客户端请求到达任意节点时,节点查这个表知道该把请求转发给谁(或返回 MOVED)。 - 每个节点的位图(
myslots,2KB):记录当前节点负责哪些槽。
用途:快速判断“这个槽是不是我管的”,以及集群内部通信时高效地告诉别人我管哪些槽。
虽然slots[slot] == myself也能判断是否自己负责,但位图有独特优势。
2. 性能:位操作比指针比较更轻量
判断一个槽是否属于当前节点:
- 用位图:检查
(myslots[slot>>3] >> (slot&7)) & 1,一条位运算指令。 - 用全局表:读取
slots[slot](一个指针),然后与myself指针比较,需要两次内存访问和一次比较。
在极高并发(如每秒几十万请求)下,位图的微秒级优势会被放大。更重要的是,Redis 常需要批量判断(如计算自己负责的槽总数、遍历所有属于自己的槽进行迁移),用位图可以用memcpy、位计数(popcount)等高效操作,比循环检查 16384 个数组元素快得多。
3. 消息传输:位图是 Gossip 协议的压缩载体
Redis Cluster 节点之间通过 Gossip 消息同步集群状态。每个节点需要定期广播自己负责的槽位集合,以便其他节点更新全局表。如果直接发送槽号列表(如[0,1,2,...5000])需要成千上万个整数(几 KB 到几十 KB),而发送一个2KB 的位图就能完整表示所有槽的归属,网络开销小且固定。
其他节点收到位图后,只需按位解析就能重构该节点的槽信息,并更新自己的全局映射表。
4. 容错和独立性
全局映射表是一个逻辑汇总视图,可能因为消息延迟或丢包暂时不一致。而每个节点自身维护的位图是权威数据(节点自己知道确切负责哪些槽)。当节点发现全局表中的某些槽与自己位图不一致时,会触发修复(如通过CLUSTER BUMPEPOCH或配置更新)。
5. 历史与代码组织
Redis 最早实现集群时就设计了clusterNode->slots位图(每个节点负责的槽),后来为了方便全局路由才增加了server.cluster->slots数组。两个结构并存,各有调用场景:
- 需要“给定槽 → 节点”时用全局数组。
- 需要“给定节点 → 它负责的槽集合”时用该节点的位图。
总结
不是“有了全局表就不需要位图”,而是两者从不同维度建模数据:
- 全局表是槽为中心(槽 → 节点),加速路由。
- 位图是节点为中心(节点 → 槽集合),加速节点自身的判断、迁移和 Gossip 广播。
内存占用上,每个节点多 2KB 完全可接受,带来的性能、网络和代码清晰度收益却很大。如果去掉位图,许多操作会变得笨拙且低效。