Sunday 的面试指南

Redis 和数据库如何保持一致?为什么更新数据库后删缓存,仍然可能读到旧数据?

🧑‍💻 面试官:任务状态缓存在 Redis 中,任务完成以后,你会怎样更新?

🙋‍♂️ 我:先把数据库改成完成,再删除缓存。下次读不到缓存,就去查数据库。

🧑‍💻 面试官:删除成功了,是不是之后每个请求都能读到最新状态?

🙋‍♂️ 我:如果有其他请求正在查询,可能还需要考虑并发。

🧑‍💻 面试官:一个请求已经从数据库读到了旧状态,但还没来得及写缓存。这时更新和删除都完成了,它再把旧状态写进去,会发生什么?

这道题的关键是:「旧数据还会不会被写回来」。删掉已有缓存,不等于取消了所有正在执行的读请求。

面试速答(60 秒版)

Redis 和数据库使用 Cache-Aside 时,通常先读缓存,未命中再查数据库并回填;修改数据时,先提交数据库更新,再删除对应缓存。

但是,这个顺序不能保证每个并发请求都读到最新值。一个请求可能先读到了数据库旧值,另一个请求随后更新数据库并删除缓存,前一个请求最后又把旧值写回缓存。

因此,还要根据业务能接受多旧的数据,设计过期时间、失效重试和必要的并发控制。删除失败不能直接忽略,可以保存可靠的失效任务,在失败后补做。

如果是余额、权限等必须准确判断的数据,就不应该只依赖缓存中的旧值。关键操作需要读取权威数据并校验。延迟双删可以缓解部分竞态,但不能单独证明 Redis 和数据库已经强一致。

CacheAside读缓存miss查DB回填,写者commitDB后del缓存,跨系统并非原子事务

知识点详解:删缓存成功,旧状态怎样又回来了?

先顺着一次普通查询,看看缓存怎么产生

假设咱们的 Agent 任务有两个状态:running 和 done。用户打开任务详情页,应用需要显示当前状态。

使用 Cache-Aside 时,应用先查询 Redis。如果命中,就使用缓存中的状态;如果没有命中,应用查询数据库,再把查询结果写入 Redis,同时设置过期时间。

任务完成时,工作进程先在数据库中把状态改成 done,提交成功以后,再删除对应缓存。这样,后面的缓存未命中请求就可以重新读取数据库。

Redis 的 Cache-Aside 说明介绍的就是这种由应用负责查询、回填和失效的方式。

注意,这里不是数据库和 Redis 自动一起提交事务。数据库提交成功,并不代表 Redis 删除也一定成功;回填请求也不会因为数据库更新了,就自动停止。

删除没有失败,为什么还会读到旧值?

咱们假设缓存一开始没有这条任务的数据。两个请求按照下面的顺序执行:

顺序读请求 A工作进程 B
1缓存未命中,查询数据库,拿到 running—
2暂时还没有回填缓存把数据库改成 done,提交
3仍未回填删除缓存成功,此时缓存中没有该键
4把之前拿到的 running 写入缓存—
5新请求命中缓存,读到 running数据库其实已经是 done

这里没有必要假设 Redis 出故障,也不需要让删除操作失败。

问题在于,A 拿到旧值以后,B 完成了更新和删除,而 A 并不知道这些变化,继续按原来的读流程回填。

因此,删除缓存只能处理当时已经存在的内容,不能阻止后来到达的旧回填。先更新数据库再删缓存,是常见的失效顺序,但不是强一致的证明。

把顺序改成先删缓存再改数据库,也不是直接解决。两个操作之间,其他请求仍然可能读到旧数据库值并回填。分析这类问题,要把读、写和回填都画出来,不能只排列两个写操作。

A缓存miss读running暂停,B更新donecommit然后DEL,A再SET running,数据库done缓存running

过期、重试和延迟双删,各自补什么问题?

过期时间可以减少旧值长期残留的风险。不过,TTL 从某次写入缓存时开始计时;晚到的旧回填,仍然可能产生新的过期窗口。它不会保证数据库一更新,所有读取立即变新。

可靠失效重试主要处理删除失败。例如,可以把需要失效的键记录成持久化任务,失败后继续补做。

如果要求数据库更新后一定留下这项任务,可以在同一数据库事务中保存业务变更和 Outbox 记录,再由后台投递失效事件。Debezium 的 Outbox 介绍展示了这类事件传播结构。这里仍要处理重复事件、积压和删除失败;它也不会自动阻止前面 A 的晚到回填。

延迟双删是在更新后删除一次,等待一段时间,再删除一次,希望把期间回填的旧值清掉。

但这个等待时间没有办法天然覆盖所有读请求。A 如果停顿得更久,可能在第二次删除以后才写入旧值。因此,延迟双删可以缓解特定时序,不能凭一个固定延迟承诺“绝不会读旧”。

这三种做法处理的问题不同,不应该把其中一种写成所有一致性问题的答案。

什么业务,不能只靠这些补救?

任务进度页可能允许短时间显示 running,用户稍后刷新即可。但即使如此,也应该定义允许延迟多久、异常时如何回源,以及怎样避免完成状态反复倒退。

换成退款额度、支付余额或操作权限,就不能只拿缓存值作最终决策。页面可以显示缓存信息,但实际执行前,要由可信服务根据权威数据重新检查,并在需要时使用事务等方式保证业务条件。

如果还要使用版本号防止旧回填,也必须设计完整比较规则。例如,在缓存里附带版本号,但缓存已经被删除,旧请求照样可能把旧版本写进去。单独多加一个字段,并不能阻止这个时序。

需要更严格的保证时,读和写双方都要遵守相应的版本屏障或协调协议,并处理失效和恢复。这会增加复杂度。因此,应该先说清业务的一致性要求,再选择合适方案,而不是给所有字段都套上同一种缓存策略。

面试官继续追问

删除缓存失败,直接回滚数据库可以吗?

通常不能这样处理。数据库已经提交后,不是简单执行一个回滚,就能撤销这个已完成事务。

需要把缓存失效当成另一个可能失败的步骤,记录并补做。若业务需要补偿,也要按业务规则设计,不能把补偿更新说成原事务回滚。

用分布式锁保护回填,就一定安全吗?

要看所有相关读写是否遵守同一规则,锁是否过期,以及旧请求是否还能在失去锁以后写入。

只锁住某一个代码入口,其他写入路径不配合,就无法证明一致性。还需要考虑故障恢复和旧持有者的动作。

用户更新后,立刻读取自己的结果怎么办?

可以让这次读取走权威数据,或使用已经明确校验的版本,让应用知道返回内容是否至少包含本次更新。

具体选择要看性能和业务要求。不能只说“TTL 很短”,因为短暂读到旧值仍然可能违背这次操作的预期。

面试速记卡

  • Cache-Aside:缓存未命中读库回填,写库提交后使缓存失效。
  • 并发漏洞:旧读可以在删除成功以后,重新写回旧值。
  • TTL:控制缓存残留,不保证更新后每次读取立即变新。
  • 可靠重试:处理失效失败,不自动解决全部回填竞态。
  • 延迟双删:缓解部分时序,固定等待不能证明强一致。
  • 关键决策:余额、权限等操作读取权威数据并重新校验。
简历汪永久免费在线制作简历,模板直接套用、导出无水印,永久免费、下载免费,不需要付费解锁任何功能。去写简历