Redis 6.2 修复了 LFU 计数并发溢出问题:因 lru 字段 counter 仅 6 bit(0–63),旧版 LFUIncrAndReturn 缺少上限检查,导致多线程竞争下计数越界归零;6.2 增加 if (counter < 63) 判断,确保不超限,算法逻辑与衰减机制均未改变。

Redis 6.2 并没有“新增 LFU 计数更新逻辑”,而是修复了 LFUIncrAndReturn 在高并发下因竞争导致的计数异常增长问题——本质是原子性缺失引发的计数溢出,不是算法设计变更。
LFU 计数字段只有 6 bit,最大值就是 63
Redis 的 robj.lru 字段共 8 bit,其中前 6 bit 存访问计数(counter),后 2 bit 存衰减时钟(clock)。这意味着计数值只能是 0–63。一旦写入超过 63,高位会被截断,比如 64 写进去实际存成 0,65 变成 1……造成频率统计完全失真。
问题不在于“要不要限制”,而在于:在多线程/多请求并发调用 LFUIncrAndReturn 时,counter 的读-改-写过程非原子,多个线程可能同时读到 62,各自 +1 后都写回 63,或更糟——其中一个写完 63 后,另一个仍基于旧值 62 继续递增逻辑,触发越界覆盖。
这个缺陷在 Redis 6.0 及更早版本中存在,6.2 通过在 LFUIncrAndReturn 入口加了显式上限判断来堵住漏洞:
if (counter < 63) {
if (rand() % (counter + 1) == 0) counter++;
}
注意:这行 if (counter < 63) 是关键补丁,它确保即使并发读到同一值,也不会越过 63。
为什么不用原子操作(如 atomic_fetch_add)?
Redis 主循环仍是单线程,但 LFU 计数更新发生在多个上下文中:
-
lookupKey、lookupKeyRead等键查找路径(主线程) - 后台 bio 线程执行的
LFUDecrAndReturn(用于衰减) - 某些命令(如
OBJECT FREQ)直接触发计数读取
所以严格来说,LFU 计数变量面临的是「主线程 + 后台线程」的跨线程竞争,而非纯多核并发。Redis 没有对 lru 字段做整字段原子操作,是因为:
— lru 字段还承载着 2-bit clock,原子读写需掩码+合并,开销不小;
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
— 实际观测发现,绝大多数溢出都源于 counter 接近 63 时的密集递增,而非 clock 更新冲突;
— 加一道轻量级条件判断,比引入原子指令或锁更符合 Redis “零阻塞”哲学。
LFU 增长逻辑本身没变,但并发安全性被显式加固
Redis 6.2 之前,LFUIncrAndReturn 的伪代码是:
counter = LFUGetCounter(o); if (rand() % (counter + 1) == 0) counter++; // ❌ 无上限检查 LFUSetCounter(o, counter);
6.2 之后变成:
counter = LFUGetCounter(o); if (counter < 63 && rand() % (counter + 1) == 0) counter++; // ✅ 显式兜底 LFUSetCounter(o, counter);
也就是说,对数增长策略(概率递增)、衰减机制(LFUDecrAndReturn 中的随机衰减)、clock 衰减触发逻辑,全部未改动。变的只是:当计数已达上限,就彻底关闭递增通道。
这个改动极小,但效果明确——避免了因并发写导致的 counter 字段高位溢出、归零、错位,从而保障 LFU 淘汰决策始终基于真实频次分布,尤其在热点 key 遭受秒杀级请求时不会突然“降权”被误淘汰。
真正容易被忽略的点是:这个修复只管“不溢出”,不管“不重复”。如果业务层高频调用 OBJECT FREQ 或频繁命中同一 key 的读命令,仍可能在短时间内把计数推到 63 就卡住——此时 LFU 和 LRU 的区分度会下降。需要结合 lfu-log-factor 和 lfu-decay-time 调优,而不是只依赖计数上限保护。

















