大家好,我是锋哥。今天分享关于【高频面试题:Java项目高并发下如何保证数据的一致性和可靠性?】面试题。希望对大家有帮助;
高频面试题:Java项目高并发下如何保证数据的一致性和可靠性?
下面按常见场景和解决方案强度来系统性回答(从弱→强排序),建议你按这个结构组织回答,既逻辑清晰又有层次感。
1. 最常见的场景:缓存 + 数据库(读多写少/写少读多)
这是面试官最爱问的场景,占比70%以上。
| 方案 | 更新顺序 | 是否推荐 | 一致性强度 | 并发安全度 | 主要问题/风险 | 适用场景 | 兜底/补偿手段 |
|---|---|---|---|---|---|---|---|
| 先更新缓存,再更新DB | Cache → DB | ×强烈不推荐 | 弱 | 很差 | 极端情况下永久不一致 | 几乎不用 | — |
| 先更新DB,再更新缓存 | DB → Cache | △偶尔用 | 弱-中 | 中等 | 写成功但更新缓存失败 | 一致性要求不高 | 设置过期时间兜底 |
| 先删缓存,再更新DB | del Cache → DB | △部分场景 | 弱-中 | 中等 | 读-写并发导致脏数据 | 一般场景 | 延迟双删 + 过期 |
| 先更新DB,再删缓存(主流方案) | DB → del Cache | √强烈推荐 | 最终一致性 | 较高 | 极小概率脏数据窗口 | 绝大多数业务 | 1. 缓存设过期时间<br>2. 延迟双删<br>3. 读到空值回源DB并重建 |
| 先更新DB,再删缓存 +延迟双删 | DB → del → sleep(几百ms~2s) → del | √高频使用 | 最终一致性 | 高 | 基本杜绝脏数据 | 高并发读写场景 | + 缓存过期兜底 |
| 先删缓存 + 异步更新(Canal/ binlog订阅) | del Cache → MQ → 消费更新Cache | √ | 最终一致性 | 很高 | 依赖MQ可靠性 | 超高并发写 | Canal + MQ重试 |
| 读写分离 + 先删后写 + 延迟双删 | del → DB主 → sleep → del | √ | 最终一致性 | 高 | 从库延迟导致短暂不一致 | 读写分离严重场景 | 延迟时间要大于主从延迟 |
面试最推荐的回答路径(性价比最高):
“我们项目中绝大部分场景采用『先更新数据库,再删除缓存』这个方案,结合延迟双删策略+缓存设置合理的过期时间作为兜底。
为什么要先更新DB再删缓存而不是反过来? 因为如果先删缓存再更新DB,存在一个经典的脏数据时间窗口:
T1:请求A 删除缓存 T2:请求B 读缓存miss → 查老数据 → 回写旧值到缓存 T3:请求A 更新DB成功 → 此时缓存里是旧数据,永久脏数据直到过期而先更新DB再删缓存,就算有并发读,也只是短暂读到旧值(最终会过期),不会永久脏数据。
为了进一步压缩不一致窗口,我们会在更新DB成功后:
- 立即删除缓存
- 等待几百毫秒 ~ 2秒(大于主从延迟、读修复时间)再删除一次(延迟双删)
极端情况下仍然不一致怎么办?我们给所有业务key设置合理的过期时间(1小时~7天不等),作为最终一致性兜底。
2. 写多/并发写冲突严重的场景(库存扣减、余额扣款、秒杀)
这类场景不能接受任何超卖/负值,一致性要求更高。
常用组合方案(强度递增):
数据库乐观锁(version / 条件更新)
→ 最常用、最稳分布式锁(Redisson / zookeeper)
→ 锁粒度要细(推荐按用户ID/商品ID/订单号等维度锁)悲观锁 + 行锁(for update)
→ 适用于写少、并发冲突集中的热点商品消息队列削峰 + 最终一致性
→ 下单 → 进MQ → 消费者串行扣库存/余额扣减成功后发MQ做下游(典型削峰方案)
TCC / Seata AT / 可靠消息(MQ)(金融级强一致性)
一句话总结常用答案:
“秒杀/库存扣减我们采用Redis + 分布式锁(Redisson) + 数据库乐观锁双重防护的组合方案:
- 先在Redis里使用Lua脚本做原子扣减(setnx + decrby)
- 扣减成功后再进数据库,使用version字段或条件更新做最终落库
- 如果Redis扣减成功但DB失败,则补偿回滚Redis(或发MQ异步补偿)
- 整个过程设置分布式锁防止并发穿透到DB”
3. 分布式事务场景(跨库、跨服务)
| 一致性要求 | 推荐方案 | 是否高并发友好 | 备注 |
|---|---|---|---|
| 强一致性 | Seata-AT / XA | 一般 | TPS低 |
| 强一致性 | 2PC / 3PC | 差 | 基本不用 |
| 强一致性 | TCC | 中等 | 业务侵入大 |
| 最终一致性 | 可靠消息(RocketMQ事务消息 / Kafka) | 高 | 主流互联网方案 |
| 最终一致性 | 本地消息表 + 定时任务 | 高 | 可靠但较重 |
| 最终一致性 | 最大努力通知 | 高 | 对时效不敏感 |
一句话标准回答:
“我们项目中跨服务的事务一致性主要采用**『基于MQ的可靠消息最终一致性』**方案:
- 本地事务执行成功后,发送事务消息(RocketMQ事务消息 / Kafka + 事务表)
- MQ回调确认本地事务状态
- 下游服务消费消息执行业务,失败支持本地重试 + 死信兜底
- 配合定时对账任务做最终数据对齐
这种方案在高并发下性能最好,也比较成熟。”
4. 总结 - 面试回答的黄金结构(推荐背诵)
- 先说业务场景决定了采用哪种一致性(强一致 / 最终一致)
- 主流方案:先更新DB再删/更新缓存 + 延迟双删 + 过期兜底
- 高并发写冲突:Redis Lua + 分布式锁 + DB乐观锁
- 跨服务/分布式事务:MQ可靠消息 + 本地事务表 + 定时对账
- 兜底手段:过期时间 / Canal binlog订阅 / 定时对账任务
- 监控:一致性监控 / 脏数据告警 / 业务对账差异告警