大家好,我是 31 岁、爱折腾技术、也爱讲故事的小米。
有一天,我在楼下便利店买水,老板一边扫码一边叹气。
我问他:“咋了老板?”
老板说:“我这店里有两个账本,一个是货架上的库存牌子,一个是电脑里的真实库存。
有时候货都卖光了,牌子上还写着‘还有 3 瓶’,客人骂我骗人;有时候牌子没更新,明明有货,客人又走了。”
我当场就笑了。这不就是我们天天在系统里折腾的那点事吗?
- 数据库:电脑里的真实库存
- Redis 缓存:货架上那块“库存展示牌”
你只要用了缓存,你只要缓存 + 数据库双写,那你就一定会遇到一个灵魂拷问:
数据一致性问题,怎么搞?
而且,这道题,java 社招面试几乎必问。今天这篇文章,我们就从这个“便利店”的故事,彻底把这道题讲透。
先把结论说清楚:为什么双写一定会不一致?
在讲方案之前,小米必须先泼你一盆冷水。
只要是「缓存 + 数据库」双存储
你面对的真实世界是这样的:
- 网络可能超时
- 线程可能被切走
- JVM 可能 GC
- Redis 可能刚好重启
- 数据库可能慢了一拍
所以结论只有一个:强一致性,在缓存架构下,几乎不可能
你能做的,只有两件事:
- 要么 牺牲性能,换一致性
- 要么 牺牲极小概率一致性,换性能
面试官真正想问的,从来不是“有没有完美方案”,而是:你知道自己在牺牲什么吗?
第一种方案:读写请求串行化(最硬核、最反人类)
1、类比一下:一个人记账
回到便利店的故事。
老板说:“那我以后不让员工自己记账了,所有卖货、进货,都排队找我一个人登记。”
结果是什么?
- 绝对不会出错
- 顾客排队,骂街
2、技术方案本质
这就是面试官常说的:读请求和写请求串行化,串到一个内存队列里
示意图是这样的:
3、为什么它一定一致?
因为它满足了两个条件:
- 所有操作 顺序执行
- 不存在并发写、并发读穿插
换句话说:时间线被你“锁死”了
4、伪代码示意
5、面试官追问点(非常关键)
那它的问题是什么?吞吐量直接腰斩,甚至更惨。
- 本来 8 核 CPU
- 你硬生生用成了 1 核
- 想扛住流量?多上几倍机器
6、真实结论
实话实说:这方案存在的最大意义,是用来“面试吹牛”和“对比其他方案”的。
第二种现实方案:允许极小概率不一致(主流)
讲到这里,我们回到现实。
便利店老板想了想,说:“算了,牌子偶尔错一次没事,但不能天天排长队。”
这,就是互联网系统的真实选择。
经典方案:先更新数据库,再删除缓存(重点!)
1、为什么是“删除缓存”,而不是“更新缓存”?
这是面试官最喜欢挖坑的地方。很多新手会写:
看起来很合理,对吧?但这是个坑。
2、并发下的致命问题
假设:
- 线程 A 更新数据
- 线程 B 正在读数据
时间线如下:
结果:缓存里是旧数据。
3、正确姿势:先 DB,再删 Cache
4、为什么这样更安全?
因为:
- 删除缓存是 幂等操作
- 读请求会触发 缓存重建
- 最坏情况:短时间读到旧值
但不会长期脏数据。
那为什么还会不一致?面试官继续追问
你以为到这就结束了?不,面试官会继续问你:如果 删除缓存失败 呢?
这时候,提醒你一句:这道题的精髓,不在代码,而在“系统设计思维”
删除缓存失败的解决思路(思路比方案重要)
1、重试机制
2、延迟双删(面试加分项)
- 第一次删,防止并发读
- 第二次删,兜底
3、消息队列补偿(高级)
- 更新 DB 成功
- 发送 MQ 消息
- 异步消费删除缓存
- 失败可重试
三种方案横向对比(一定要记)
这是面试必备表格,建议你背下来。
面试时的标准回答模板(直接背)
如果面试官问你:
Redis 如何保证缓存与数据库双写时的数据一致性?
你可以这样答:
“只要使用缓存,就不可避免会涉及缓存和数据库双写,只要双写,就一定存在一致性问题。
如果业务要求强一致性,可以通过读写请求串行化的方式,把所有请求串到内存队列中,但代价是吞吐量大幅下降,通常不推荐。
在绝大多数互联网场景中,采用的是最终一致性方案,即先更新数据库,再删除缓存。
即使在极端并发情况下,最多也只是短时间内读到旧数据,发生概率非常低,可以通过延迟双删、重试或消息队列进一步兜底。”
这一段,说完,面试官基本不会再追。
总结:技术和人生,其实一样
最后,回到便利店老板。他跟我说:“有时候牌子错一会儿没事,但我得保证,最终是对的。”
我突然觉得,这和我们做系统、做人,其实一模一样。
- 完美,代价极高
- 容错,才是常态
- 你要清楚,你选择牺牲什么
缓存一致性这道题,从来考的不是 Redis,而是你对 系统权衡能力的理解。
END
如果你看到这里,说明你已经把这道社招高频题,彻底拿下了。
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
好朋友们,我们下篇见。