2. 哈希分桶原理:一致性哈希、普通哈希在分桶中的应用与区别
分桶这件事,说白了就是把流量或用户均匀地扔进几个桶里。我刚开始做推荐系统时,觉得这不就是个取模吗?后来线上出了几次事故,才发现没那么简单。
今天咱们就聊聊两种主流哈希分桶方式——普通哈希和一致性哈希。它们在实时推荐实验里怎么用?区别在哪?坑在哪?我用自己的踩坑经历给你讲透。
2.1 普通哈希分桶:简单直接,但有硬伤
普通哈希分桶,最常见的做法就是 hash(user_id) % N。N 是桶数。比如你有 10 个桶,用户 1001 的 hash 值对 10 取模,结果就是桶编号。
代码实现很简单:
def normal_hash_bucket(user_id: str, bucket_num: int) -> int:
hash_val = hash(user_id)
return hash_val % bucket_num
我在项目中第一次用这个方案时,觉得稳得很。直到有一天,实验组从 5 个桶扩容到 6 个桶——结果所有用户的桶号都变了。原本在实验组的用户跑到了对照组,对照组跑到了实验组。实验数据全废了。
为什么会这样?因为取模运算依赖分母 N。N 一变,余数全变。你想想看,一个用户原本在桶 3,N 从 5 变成 6 后,他可能去了桶 0。实验组和对照组的用户群体就混了。
普通哈希适合什么场景?桶数固定不变的场景。比如你确定永远只用 10 个桶做 A/B 测试,那没问题。但现实是,实验规模经常调整,桶数说变就变。
| 特性 | 普通哈希 | 一致性哈希 |
|---|---|---|
| 桶数变化影响 | 全部重映射 | 仅影响相邻桶 |
| 实现复杂度 | 低 | 中 |
| 数据迁移量 | 100% | 约 1/N |
| 适合场景 | 桶数固定 | 桶数动态变化 |
2.2 一致性哈希:优雅解决扩容问题
一致性哈希的核心思路,是把哈希值空间组织成一个环。嗯,你可以想象成一个钟表盘,从 0 到 2^32-1 首尾相连。
具体做法是这样的:
- 把每个桶(比如实验组 A、B、C)通过哈希函数映射到环上的某个位置
- 把每个用户也通过哈希函数映射到环上
- 用户沿着环顺时针走,遇到的第一个桶就是他的归属桶
我画了一张图,你看一眼就明白了:
你看,用户 1 顺时针走,先碰到桶 B,所以归桶 B。用户 2 顺时针走,先碰到桶 D,归桶 D。用户 3 顺时针走,先碰到桶 C,归桶 C。
现在问题来了:如果我要加一个桶 E,会发生什么?
只有桶 E 在环上的位置到前一个桶之间的那部分用户会受影响。其他用户完全不动。这就是一致性哈希最牛的地方——最小化数据迁移。
2.3 虚拟节点:解决数据倾斜
一致性哈希有个问题——数据倾斜。如果桶在环上分布不均匀,某些桶会分到特别多的用户,某些桶分得特别少。
我记得有一次线上实验,三个桶,结果一个桶占了 60% 的流量,另外两个各占 20%。实验结论根本没法信。
解决方案是引入虚拟节点。每个物理桶在环上映射多个虚拟位置。比如桶 A 有 100 个虚拟节点,分散在环上各处。这样分布就均匀了。
class ConsistentHash:
def __init__(self, physical_nodes: list, virtual_nodes_per_node: int = 100):
self.ring = {}
self.virtual_nodes_per_node = virtual_nodes_per_node
for node in physical_nodes:
for i in range(virtual_nodes_per_node):
virtual_key = f"{node}_{i}"
hash_val = hash(virtual_key)
self.ring[hash_val] = node
def get_bucket(self, user_id: str) -> str:
if not self.ring:
return None
hash_val = hash(user_id)
# 找到环上第一个大于等于 hash_val 的节点
sorted_keys = sorted(self.ring.keys())
for key in sorted_keys:
if hash_val <= key:
return self.ring[key]
# 如果没找到,回到环首
return self.ring[sorted_keys[0]]
2.4 实时推荐场景下的选择建议
说了这么多,到底怎么选?我给你几个判断标准:
- 实验桶数固定不变 → 普通哈希就够了,简单高效
- 实验桶数可能动态调整 → 必须用一致性哈希
- 流量规模巨大(千万级用户) → 一致性哈希 + 虚拟节点
- 需要跨版本对比实验数据 → 一致性哈希,保证用户归属稳定
我曾经在一个日活千万的推荐系统里,一开始用了普通哈希。后来产品经理说「加两个实验组看看效果」,我一算,所有用户都得重新分桶,历史数据全废了。那次之后,我所有新项目都默认用一致性哈希。
最后说一句:哈希分桶没有银弹。普通哈希简单但脆弱,一致性哈希灵活但复杂。根据你的实验规模和变更频率来选,别盲目跟风。