🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦🎉 Giảm 20% cho đơn hàng đầu tiên!✦🚚 Miễn phí vận chuyển cho đơn trên 500.000₫✦⚡ Flash Sale — Số lượng có hạn!✦🎁 Quà tặng kèm cho đơn từ 1.000.000₫✦
Redis 缓存与脏数据:数据库已更新,客户端却还在读旧数据
Quay lại Blog

Redis 缓存与脏数据:数据库已更新,客户端却还在读旧数据

25 tháng 10, 20262 phút đọcVũ Khôi
Tiếng Trung
#Tiếng Trung
Vũ Khôi

Vũ Khôi

Tác giả tại Wotech Electronics Blog

Redis 脏缓存——缓存里的旧数据

你刚在数据库里更新了商品价格,确认记录已正确改写——可客户刷新了好几次页面,看到的仍是旧价格。这可不是"灵异故障":很可能是 Redis 缓存里存着旧数据(stale data,脏数据)——软件工程里最古老的难题之一。这篇文章帮你弄懂成因与行业通行解法。

缓存里的脏数据是什么?

缓存(Cache)是放在应用与数据库之间的一层高速存储,用来减轻负载、缩短响应时间;Redis 是当今最流行的内存缓存(in-memory,存于 RAM)软件。问题出现在数据库已变、缓存里的副本却没变的时候——那份"过期"的副本就是脏数据(stale data)。可怕之处在于:数据库是对的、服务运行正常、没有任何一行日志报错——用户却持续读到旧信息。

Phil Karlton(Netscape)曾开玩笑说:

"计算机科学只有两件难事:缓存失效(Cache Invalidation)和给变量命名。"

这句玩笑到今天依然成立:挂上缓存容易——让缓存与数据库保持一致才难。

经典场景:数据库更新完了,缓存却没删

看似简单的处理流程:

DB UPDATE → DELETE REDIS KEY(更新数据库 → 删除 Redis 中对应的 key)

下一次读取发现 key 不存在,应用从数据库加载新数据并重新写入缓存——纸面上只有两步,没有出错的空间。

但缓存失效(cache invalidation)这个问题最值钱的提问是:如果数据库已更新完毕,删除 key 的命令却没有执行、或没在正确的时机执行,会怎样? 就在那一刻,数据库与缓存变得不一致(inconsistent)——脏数据被端给用户,有时一端就是几个小时。

缓存"压货"旧数据的五大常见原因

  1. 应用在两步之间出错。 数据库已提交,但应用进程在来得及向 Redis 发送 DELETE 之前崩溃或抛出异常。横跨两个独立系统的操作天然不原子(atomic)——这是问题的技术根源。
  2. 时序竞争(race condition)。 一个请求从数据库读到旧数据,因高负载被挂起,随后才把这份旧数据写进缓存——覆盖了写请求刚删掉的 key。旧数据"复活",尽管流程每一步都没走错。
  3. 应用多实例,失效通知没送到。 横向扩容的系统有多个服务副本;若只有处理写入的副本删除了 key,其余副本仍在本地缓存(local cache)里抱着旧值,对这些变更一无所知。
  4. 数据在应用之外被改动。 手工执行的 SQL、夜间重载数据的 ETL 作业……都在向数据库写入,却对缓存一无所知。第二天一早,应用展示的还是昨天的世界。
  5. 没设 TTL。 TTL(Time-To-Live,key 的存活时间)是让 key 自动过期的"安全网"。没有 TTL 的 key 一旦落入上述四种情形之一,就会无限期地持有旧数据。

对企业的影响

脏数据直接砸到客户体验和运营上:

  • 电商: 客户看到旧价格、库存不准——下单后才发现缺货或差价。
  • 内部报表: 仪表盘显示旧数字,管理层基于过时信息做决策。
  • 客服: 员工看到最新订单状态,客户看到旧状态——双方各执一份"事实"吵成一团。
  • 信任流失: 出错几次之后,内部用户开始不信任系统——而重建信任的代价最高。

行业通行解法

不存在唯一解;实际做法是按所需的一致性程度,叠加多层防御。

1. 给所有 key 设 TTL——必配的安全网

给全部 key 都设置 TTL,即便已有主动失效机制。TTL 不会带来即时一致,但旧数据最多只能多活一个 TTL 的时长。对时效不敏感的数据,几分钟的 TTL 通常就够;敏感数据(价格、库存、订单状态)需要更短的 TTL。

2. Cache-aside 模式 + 写后删 key

最常用的模式(pattern,设计模式):应用自行管理缓存读写(cache-aside),更新数据时删除 key 而不是覆盖值。删除比覆盖更安全——既避免两个请求互相覆盖,也避免把错的数据写进缓存。

3. 延迟双删(delayed double delete)

对抗 race condition:更新数据库后先删一次 key;等一小段超过读请求可能被挂起的时长;再删第二次,清掉旧请求在两步之间把旧数据写回去的情况。

4. 直写缓存(write-through / write-behind)

应用只写一层:由缓存负责向数据库同步。Write-through 同步写——一致性高,写延迟增加;write-behind 异步写——更快,但若缓存在落库前损坏,有丢数据的风险。

5. 事件驱动失效(event-driven eviction)

数据库变更时,系统通过消息队列(message queue,如 Kafka、RabbitMQ)或 Redis Pub/Sub(Redis 的发布/订阅机制)向全部实例广播事件,删除对应 key。对多服务系统最彻底——代价是运维复杂度显著上升。

汇总表:该选哪种方案?

策略 一致性水平 复杂度 适用场景
短 TTL 最终收敛 低 数据可容忍几分钟延迟
写后删 key 高(单一写入源) 低 单体应用、实例少
延迟双删 更高,拦住 race condition 中 读流量大
Write-through 高 中 能接受写延迟
Event-driven eviction 最高 高 多服务、多实例

总原则:TTL 是所有 key 的必选项;主动失效机制再按数据的敏感程度叠加其上。

给越南企业的启示

对越南大多数中小企业系统而言:

  • 从 cache-aside + 写后删 key + 全 key 设 TTL 起步——简单,且能覆盖绝大多数场景。
  • 涉及钱的数据(价格、库存、余额),加配延迟双删,或对极端敏感的场景干脆不缓存。
  • 只在系统真正多服务、多团队时才投入 event-driven eviction——不是因为"时代潮流",而是问题确实到了需要它的规模。

快速自查:你的所有 Redis key 都设 TTL 了吗?很多项目只能回答"没有"——而那正是脏数据的温床。

结论

脏数据是缓存带来速度的代价——无法彻底消除,只能把不一致窗口收窄到可接受的范围:TTL 做安全网,主动删 key 做日常功课,事件驱动做大型系统的长期底座。吃透上面五个成因与五种解法,就抓住了缓存一致性(cache consistency)问题的核心。

资料来源:整理自 Facebook 技术分享,由 Wotech Electronics 编辑。

你的系统正遇到数据库已更新、应用却仍显示旧数据的情况?联系 Wotech,为你的企业系统做缓存架构审查与数据一致性优化。

Bài viết liên quan