缓存不一致问题:并发窗口的"定时炸弹"
一、问题场景
并发窗口的"定时炸弹"(价:10 → 20):写请求将价格从 10 改为 20,采用 先删缓存、再更新数据库 的策略。
| 步骤 | 请求 | 操作 | 说明 |
|---|---|---|---|
| 1️⃣ | 写请求(改 20) | 删缓存 | 此时数据库仍是 10 |
| 2️⃣ | 读请求(极速响应) | 缓存空,查库拿到 10 | 恰好落在删除后的空窗期 |
| 3️⃣ | 读请求 | 旧值 10 回填进缓存 | 污染发生 |
| 4️⃣ | 写请求 | 更新数据库 = 20 | 但缓存里已是脏数据 |
⚠️ 1️⃣ 和 4️⃣ 之间存在 致命微秒级缝隙,读请求恰好插入其中。
结论
💥 脏数据 10 会一直赖在缓存里,产生主动的持续污染!
- 后续读请求会一直命中缓存中的旧值 10
- 直到缓存过期或再次写操作才可能修复
二、解决方案
方案一:先更新数据库,再删缓存(Cache Aside)✅ 推荐
适合普通场景下、并发不高的业务。
把顺序反过来:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1️⃣ | 写请求 | 更新数据库 = 20 |
| 2️⃣ | 写请求 | 删除缓存 |
| 3️⃣ | 读请求 | 缓存未命中 → 查库 → 回填 20 |
为什么有效:即使删缓存前有读请求拿到旧值,它的回填发生在删缓存之后(读+回填比"更新库+删库"慢的概率极低),脏数据几乎不会留存。
这是业界最通用的方案(Facebook、各大厂默认),配合下面的兜底措施即可。
残余风险:读请求"查库(10) → 回填"若极端卡顿,晚于写请求"更新库(20) → 删缓存",仍可能回填旧值。概率极低,可用兜底方案覆盖。
方案二:延迟双删
针对"先删缓存"场景的补救:
text
1. 删除缓存
2. 更新数据库
3. 睡眠 N 毫秒(覆盖一次读请求的耗时,如 500ms)
4. 再次删除缓存 ← 把并发窗口期被回填的脏数据清掉
- 缺点:延迟时间靠估算,且降低吞吐(睡眠)
- 适合:无法改造为 Cache Aside 的存量代码
方案三:订阅 Binlog 异步删缓存(最终一致性)✅ 生产常用
text
更新数据库 → Canal/Debezium 订阅 binlog → 发送 MQ → 消费者删除缓存
实现逻辑:业务代码只负责更新 MySQL。通过中间件(如 Canal、Debezium)伪装成 MySQL 从库,监听 Binlog 日志。当解析到数据变更时,异步触发 Redis 的更新或删除操作。
优势:与业务逻辑完全解耦,保证了严格的顺序性,支持全量/增量同步,架构扩展性强。
劣势:架构复杂度较高,同步存在一定延迟,且运维成本较高。
- 业务代码与缓存删除解耦
- 可通过 MQ 重试保证删除成功
- 天然具备"再次删除"的兜底能力
方案四:加分布式锁(强一致场景)
写请求对同一 key 加锁,读请求发现锁存在时:
- 等待,或
- 直接读库、跳过回填缓存
代价是性能下降,仅在强一致要求(如库存扣减)时使用。
方案五:兜底措施(无论选哪种都建议加上)
| 措施 | 说明 |
|---|---|
| 设置过期时间 TTL | 即使脏了也能自愈,比如 30s~5min |
| 删除失败重试 | 删缓存失败时发 MQ / 记录重试表 |
| 写入带版本号 | 回填时校验版本,旧版本不回填 |
三、总结选型
text
一般业务 → 先更新库,再删缓存 + TTL (简单够用)
高一致性要求 → Cache Aside + Binlog 异步删 + 重试
强一致(库存等)→ 分布式锁 / 串行化
不能改代码 → 延迟双删
核心原则:让"删缓存"这个动作尽可能晚发生、且有机会重试,再靠 TTL 兜底,脏数据的窗口就能被压缩到几乎为零。
Redis 内存淘汰策略
Redis 内存淘汰策略是指当 Redis 的内存使用达到上限时,如何决定清理哪些数据以保证新数据的存入。Redis 提供了多种内存淘汰策略,每种策略适用于不同的场景和需求。
模拟面试:Redis 淘汰策略的拷打
以下是一个模拟的面试场景,面试官逐步深入提问,答案详细且结构化,涵盖技术细节和实际应用。
面试官:Redis 的内存淘汰策略有哪些?分别适用于什么场景?
候选人: Redis 提供了八种淘汰策略,分为两类:针对所有键(allkeys)和仅针对设置了过期时间的键(volatile)。具体如下:
- noeviction:不淘汰,内存满时拒绝写入,适合数据不可丢失的场景,如金融数据存储。
- allkeys-lru:对所有键使用 LRU 算法,适合热点数据频繁访问的缓存系统。
- allkeys-lfu:对所有键使用 LFU 算法,适合访问频率差异大的场景,如推荐系统。
- allkeys-random:随机淘汰所有键,适合无明显访问模式的测试环境。
- volatile-lru:对设置了 TTL 的键使用 LRU,适合混合数据场景,如临时缓存。
- volatile-lfu:对设置了 TTL 的键使用 LFU,适合关注频率的临时数据。
- volatile-random:随机淘汰设置了 TTL 的键,适合简单临时数据场景。
- volatile-ttl:优先淘汰 TTL 最短的键,适合短生命周期数据。
这些策略通过 maxmemory-policy 配置,默认是 noeviction。
面试官:LRU 和 LFU 的区别是什么?Redis 是如何实现 LRU 的?
候选人:
- LRU(Least Recently Used):根据最近访问时间淘汰,优先删除最久未访问的键。适合热点数据场景。
- LFU(Least Frequently Used):根据访问频率淘汰,优先删除访问次数最少的键。适合长期运行、频率差异大的场景。
Redis 的 LRU 实现:Redis 并未实现严格的 LRU,而是采用近似 LRU 算法,具体步骤:
- 采样:从键空间中随机抽取一小部分键(默认 5 个,可通过
maxmemory-samples配置)。 - 时间戳记录:每个键维护一个访问时间戳(基于 LRU_CLOCK)。
- 比较淘汰:比较采样键的访问时间,淘汰最旧的键。
- 优化:Redis 使用一个全局 LRU 时钟(精度为 1 秒),减少维护开销。
这种近似 LRU 算法在性能和精度间取得平衡,采样越多,精度越高,但开销也越大。
面试官:Fast 模式和 Slow 模式是什么?它们会影响淘汰效果吗?
候选人: Fast 模式和 Slow 模式不是淘汰策略,而是 Redis 执行淘汰算法时的运行模式,影响淘汰的效率和精度:
- Fast 模式:采样少量数据,快速执行,适合高并发场景,但精度较低。
- Slow 模式:采样更多数据,执行更精确的算法(如完整的 LRU/LFU 计算),适合数据价值高的场景。
影响:
- Fast 模式可能导致次优淘汰(误删高价值数据),但响应快。
- Slow 模式淘汰更精准,但性能开销大。
通过 maxmemory-eviction-tenacity(0 到 100)控制,0 为纯 Fast,100 为纯 Slow,中间值动态调整。
实际应用:
- 高并发场景(如电商秒杀)用 Fast 模式。
- 高精度场景(如推荐系统缓存)用 Slow 模式。
面试官:如果我要实现一个自定义淘汰策略,应该怎么做?
候选人: Redis 本身不支持直接自定义淘汰策略,但可以通过以下方式实现:
- 修改源码 :Redis 是开源的,可以修改
evict.c中的淘汰逻辑,添加自定义算法。例如,基于键的大小、业务优先级等设计新策略。缺点:需要重新编译,维护成本高。 - 外部控制:使用 Redis 的 INFO 命令监控内存使用情况。通过客户端脚本定期扫描键空间,结合业务逻辑(如优先级、访问频率)执行 DEL 命令删除键。优点:无需改源码,灵活性高。缺点:需要额外开发,可能影响性能。
- 结合 Lua 脚本:使用 Redis 的 Lua 脚本实现简单的淘汰逻辑,通过 EVAL 命令定期运行。例如,扫描特定模式的键,基于自定义规则删除。局限性:复杂逻辑可能受 Lua 脚本性能限制。
推荐方案:
- 小规模场景:使用外部控制 + Lua 脚本。
- 大规模场景:修改源码并充分测试。
面试官:实际生产环境中,你会如何选择淘汰策略?
候选人: 选择淘汰策略需要综合考虑业务场景、数据特性和服务要求:
-
分析数据特性
- 数据是否有明确的生命周期?如果有,优先考虑 volatile-* 策略。
- 数据访问是否有热点?如果有,LRU 或 LFU 更适合。
-
评估业务需求
- 数据丢失是否可接受?如果不可接受,选择 noeviction。
- 性能敏感度高?选择 Fast 模式或 allkeys-random。
-
典型场景参考
- 电商缓存:热点数据多,选 allkeys-lru + Fast 模式。
- 推荐系统:频率差异大,选 allkeys-lfu + Slow 模式。
- 临时数据:有 TTL,选 volatile-ttl 或 volatile-lru。
-
监控与调优
- 使用
INFO MEMORY监控evicted_keys和mem_used。 - 调整
maxmemory-samples和maxmemory-eviction-tenacity优化淘汰效果。
- 使用
我的经验: 在某电商项目中,我们使用 allkeys-lru 结合 Fast 模式,配合 maxmemory-samples=10,在高并发场景下保持了低延迟,同时通过监控调整 maxmemory 避免频繁淘汰。
作者:Asthenian 链接:https://juejin.cn/post/7503454000494280742 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。