一道我曾经答不好的架构问题,今天我来重答

一道我曾经答不好的架构问题,今天我来重答
踱鸽&水晶蟹一道我曾经答不好的架构问题,今天我来重答
一、那道题是什么,当时的背景
2019 年参加一次技术面试,面试官问了这么一道题:
「你们系统的缓存和数据库是怎么保持一致的?」
当时我的回答大概是:「用 Redis 缓存,查询先查缓存,没有再查库,更新时先更新库再更新缓存,或者让缓存过期。」
面试官追问:「如果先更新了数据库,还没来得及更新缓存,另一个线程来读,读到了脏数据,怎么处理?」
我有点慌,说「设置缓存过期时间,等它自动失效」。面试官没有继续追问,面试最后也没有通过。
那道题我当时就觉得答浅了,但不知道浅在哪。
二、当时我的回答,为什么没答好
回头看,当时的答案有两个问题:
第一,没有意识到「先更新库还是先更新缓存」是一个有具体权衡的选择。 我把它当成了一个无所谓的实现细节,其实两种顺序各有不同的风险,需要根据业务容忍程度来选。
第二,「过期时间兜底」是一个工程策略,不是一个解答。面试官在问「并发情况下的数据不一致窗口怎么控制」,我用一个「最终一致性兜底」直接跳过了这个问题。
三、后来哪些经历让我真正理解了这个问题
两次经历。
第一次是 2021 年,我们的商品详情页缓存出了一个问题:商品价格改了,但部分用户看到的价格还是旧的,持续了大概 3 分钟。追查下来,是更新代码里先删了缓存、再更新数据库,但这两步之间有一个请求进来了,读到库里的旧价格之后写回了缓存,导致缓存里又是旧数据。
当时为了修这个问题,我第一次系统地搜索了「缓存更新策略」,看到了 Cache Aside、Read Through、Write Through 这几种模式,也第一次搞清楚了「删缓存还是更新缓存」、「先操作库还是先操作缓存」背后的推理逻辑。
第二次是 2023 年,在一次内部的方案评审里,同事提出了「延迟双删」的方案,我被追问:「延迟多久?这个时间怎么确定?如果延迟时间内还有写入怎么办?」那几个问题让我意识到,延迟双删也不是银弹,只是把不一致窗口缩小了,没有消除。
四、现在我的完整回答
核心原则:优先删缓存,不要更新缓存。
删缓存后,下一次请求会回源到数据库,重新写入一份最新的缓存。如果直接更新缓存,在并发写的场景下,两个线程的更新顺序可能和数据库的写入顺序不一致,导致缓存里留下过期值。
策略一:Cache Aside(最常用)
读:先读缓存 → 缓存 miss 则读库 → 将结果写入缓存
写:先写库,再删缓存
为什么是「先写库,再删缓存」,而不是「先删缓存,再写库」?
- 如果先删缓存再写库:删缓存后、写库之前,另一个线程来读,回源读到旧的库数据,写回缓存,此后库更新成功,但缓存里是旧值。不一致。
- 如果先写库再删缓存:写库后、删缓存之前,另一个线程来读,读到旧缓存,可接受(读到删除前的一致快照);删缓存后,下次请求回源重新写入最新值。
先写库再删缓存的方案仍有一个极小概率的问题:写库成功但删缓存失败(如网络抖动)。缓存里会留下旧值,直到过期。
对策:删缓存操作走消息队列做异步重试,确保最终删除成功;同时设置合理的缓存过期时间作为兜底。
策略二:延迟双删(在读多写多场景的补充手段)
|
延迟双删的目的:消灭写库前已经读到旧值、正在构建缓存的那个请求带来的「回写旧值」问题。延迟时间取「数据库主从延迟 + 读操作最长耗时 + buffer」的经验值,不是一个精确数字。
延迟双删不是银弹:它把不一致窗口从「两步操作之间」缩小到「延迟时间内」,但没有消除,适合「能容忍秒级不一致、不希望引入消息队列复杂度」的业务场景。
一致性边界总结:
| 方案 | 不一致窗口 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 先写库再删缓存 + 过期时间兜底 | 较小(删缓存失败时到过期) | 低 | 大多数场景 |
| 先写库再删缓存 + 消息队列重试 | 极小 | 中 | 对一致性要求较高 |
| 延迟双删 | 延迟时间内(秒级) | 低 | 读多写多、能容忍秒级不一致 |
| 分布式事务(如 Seata) | 无(强一致) | 高 | 金融级别,跨服务写操作 |
五、这类问题考察的本质是什么
不是「你背了多少种缓存更新策略」,而是:你能不能在约束条件下做合理的权衡。
面试官真正想看到的回答结构是:「这个问题的本质是一致性和可用性的权衡 → 不同业务场景对不一致的容忍度不同 → 在我们的业务里,选了 X 方案,原因是 Y,边界条件是 Z。」
一个「标准答案」背出来,和「在真实项目里踩过坑、想清楚了每个方案的适用边界」,面试官是能分辨出来的。

