Redis HyperLogLog在稀疏存储字节数超约3000字节或基数达约16000时自动转为密集存储,转换单向不可逆,固定占用12KB内存。

稀疏存储什么时候会自动转成密集存储
Redis 的 HYPERLOGLOG 默认在基数达到 16000 时触发从稀疏(sparse)到密集(dense)的转换。这个阈值不是硬编码在客户端,而是由 Redis 服务端控制的——你无法通过配置项直接修改它,但可以通过 DEBUG OBJECT key 观察当前存储类型是否已切换。
常见错误现象是:明明只加了几百个元素,MEMORY USAGE 却突然跳到 12KB 左右。这往往不是 bug,而是稀疏结构内部因插入顺序、哈希分布不均,提前触发了“稀疏膨胀”,导致 Redis 主动升级为密集模式。
- 稀疏模式下,连续零值桶用 RLE(Run-Length Encoding)压缩,比如
00000000可能只存为0x80(表示 8 个零) - 一旦稀疏编码占用字节数超过某个隐含上限(约 3000 字节),Redis 就会强制转为固定 12KB 的密集格式
- 转换是单向的:密集 → 不会退回稀疏;哪怕后续所有元素都被
PFADD重复插入(无实际更新),也不会降级
为什么 PFADD 返回 0 后 MEMORY USAGE 还变大了
这是容易被忽略的关键点:PFADD 虽然返回 0 表示“无新元素加入”,但它仍可能触发内部重编码。例如,当稀疏结构因新增元素导致 RLE 编码效率下降(如零值段被打断),Redis 会在本次操作中完成向密集格式的迁移。
验证方式很简单:
127.0.0.1:6379> PFADD hll a b c (integer) 1 127.0.0.1:6379> MEMORY USAGE hll (integer) 240 127.0.0.1:6379> PFADD hll d e f g ... # 插入足够多元素直到触发转换 127.0.0.1:6379> MEMORY USAGE hll (integer) 12288
注意:即使某次 PFADD 返回 0,只要它参与了重编码流程,MEMORY USAGE 就可能跃升。这不是数据写入,而是结构优化的副作用。
密集存储的 12KB 是怎么算出来的
密集模式本质是一个含 16384 个桶(register)的数组,每个桶用 6 位(bit)存储,总 bit 数 = 16384 × 6 = 98304 bit ≈ 12KB。但要注意:这 12KB 是对齐后的内存块大小,实际有效数据仍是那 98304 位。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
由于 6 位不整除 8,Redis 必须做位运算访问单个桶,比如读取第 pos 个桶时:
- 计算所在字节偏移:
b0 = (6 * pos) / 8 - 计算右移位数:
fb = (6 * pos) % 8 - 从
b0和b0+1两个字节中拼出 6 位值,再与0x3f(即111111)做 AND
这种设计牺牲了随机访问速度,换来极致空间压缩——但你几乎不需要手动操作这些位,PFCOUNT 和 PFMERGE 全部封装好了。
想省内存?别依赖“等它自动转”,要主动控制
如果你明确知道日活量级长期低于 5000,又极度敏感于内存抖动,就不要把多个业务共用一个 HLL key。因为合并(PFMERGE)或跨 key 统计(PFCOUNT key1 key2)会强制创建临时密集结构,哪怕源 key 都还是稀疏的。
更稳妥的做法:
- 按业务维度拆分 key,比如
hll:page:home:20260527、hll:page:profile:20260527 - 避免用
PFCOUNT多 key 模式做日常统计,改用单 key + 定期PFMERGE到汇总 key - 监控
DEBUG OBJECT hll_key输出中的encoding字段:值为raw表示稀疏,quicklist或其他则说明已是密集(不同 Redis 版本字段名略有差异)
真正难处理的从来不是“怎么转”,而是“转完不能退”——一旦进入密集模式,12KB 就锁死了,哪怕之后半年只加一个新用户。

















