直接用 Redis SET 存 URL 会因内存爆炸而崩溃:上亿 URL 占 5GB+ 键内存,加 Redis 开销易致内存溢出、主从卡死、RDB 失败;布隆过滤器需用 Redis 位图手写实现,避免序列化本地 BloomFilter 或依赖非原生命令。

为什么直接用 Redis 的 SET 存 URL 会崩
URL 数量上亿后,每个 URL 平均 50 字节,光存键就要占 5GB+ 内存,更别说 Redis 自身的内存开销。用 SET 或 HASH 做去重,不是慢,是根本撑不住——内存爆掉、主从同步卡死、RDB 生成失败都可能发生。
用 redis-py + pybloom_live 组合不现实
有人想把本地布隆过滤器(比如 BloomFilter)序列化后塞进 Redis,这路走不通:pybloom_live 的实例不能直接 pickle,且跨进程/实例无法共享状态;Redis 又不支持原生布隆指令(除非你用 Redis 7.0+ 的 BF.RESERVE,但 Python 客户端得手动发命令)。
真正可行的路径只有一条:用 Redis 的位图(BIT 操作)模拟布隆过滤器逻辑,自己算哈希、自己打位、自己查位。
-
redis-py必须用 4.0+,支持execute_command发送原生命令 - 选 3–5 个独立哈希函数(推荐
mmh3.hash+ 移位扰动,比hashlib.md5快 10 倍) - 总位数
m至少设为预期 URL 总量 × 12(误判率 ≈ 0.1%),别省 - 所有哈希结果必须对
m取模,否则SETBIT会报ERR bit offset is not an integer or out of range
手写布隆过滤器类要绕开三个坑
下面这个类能跑通,但漏掉任意一点都会导致误判飙升或崩溃:
立即学习“Python免费学习笔记(深入)”;
import mmh3
import redis
<p>class RedisBloom:
def <strong>init</strong>(self, client: redis.Redis, key: str, capacity: int = 10_000_000, error_rate: float = 0.001):
self.r = client
self.key = key
self.m = int(-capacity * math.log(error_rate) / (math.log(2) *<em> 2)) # 总位数
self.k = int((self.m / capacity) </em> math.log(2)) # 哈希函数个数,通常 6~8</p><pre class='brush:python;toolbar:false;'>def _hashes(self, url: str) -> list[int]:
h1 = mmh3.hash(url, 0)
h2 = mmh3.hash(url, 1)
return [(h1 + i * h2) % self.m for i in range(self.k)]
def add(self, url: str) -> bool:
bits = self._hashes(url)
# 用 pipeline 批量 SETBIT,避免网络往返放大延迟
pipe = self.r.pipeline()
for b in bits:
pipe.setbit(self.key, b, 1)
pipe.execute()
return True
def exists(self, url: str) -> bool:
bits = self._hashes(url)
# 用 BITFIELD 一次读多个位(Redis 6.0+),兼容旧版就用 GET + 位运算模拟
resp = self.r.get(self.key)
if not resp:
return False
# 简化版:逐个 GETBIT(适合调试,线上建议改用 BITFIELD)
for b in bits:
if not self.r.getbit(self.key, b):
return False
return True- 别在
exists()里用GET读整块数据再本地算——URL 超过 100MB 时,单次GET就超时 -
setbit的 offset 参数必须是int,Python 的float或numpy.int64都会触发ResponseError: wrong number of arguments - Redis 默认最大内存 512MB,布隆位图占满后,
setbit不报错但返回 0,得靠INFO memory监控used_memory_peak_human
上线前必须压测的两个边界
本地跑通不等于线上可用。重点看这两点:
- 用
redis-benchmark -n 100000 -r 1000000 -t setbit,getbit --csv测单 key 下万级SETBIT吞吐,低于 2 万 QPS 就得拆分分片(比如按 URL 域名哈希到不同 key) - 当
exists()返回True但实际 URL 未插入过,说明位图已饱和——此时bf.debug(Redis 7.0+)显示elements接近capacity,就得重建新 key 并迁移流量
布隆过滤器的“不可删除”特性决定了它不适合生命周期混杂的场景;如果 URL 有明确过期时间,得配合 EXPIRE 整 key 失效,而不是给每个 URL 单独设 TTL。


















