一、同步双写的强一致性逻辑
1. 理论上的强一致性机制
-
原子性保证 :
若数据库与缓存的写操作在同一事务中完成(如分布式事务框架 Seata),则:
-
成功:两者均更新,数据一致。
-
失败:事务回滚,数据回退到初始状态。
@Transactional
public void updateData(Data data) {
mysql.update(data); // 更新数据库
redis.set(data.key, data.value); // 同步更新缓存
}
-
-
时序控制 :
在单线程或无并发冲突的场景下,同步双写能确保缓存与数据库的实时一致,避免脏读。
2. 适用场景
- 低并发业务:如后台管理系统、配置项更新。
- 强一致性要求:如金融账户余额、库存核心数据(需结合分布式锁)。
二、同步双写的实践局限性
1. 性能瓶颈
- 吞吐量下降 :
同步双写的延迟等于数据库与缓存操作的总和。若 Redis 写入耗时 2ms,MySQL 写入耗时 5ms,单次操作延迟为 7ms ,QPS 上限约 142(1000ms/7ms),远低于纯数据库操作的 200 QPS。 - 资源竞争 :
高并发下,线程因等待 Redis 和 MySQL 的响应而阻塞,加剧性能恶化。
2. 原子性失效风险
- 非事务性缓存 :
Redis 不支持与 MySQL 的跨系统事务(如 XA 协议),若缓存写入失败而数据库提交成功,将导致永久性不一致。 - 补偿复杂度 :
需额外实现回滚逻辑(如监听 MySQL Binlog 修复缓存),违背同步双写的初衷。
3. 并发写冲突
-
时序覆盖问题 :
若两个并发请求先后更新同一数据:- 线程A更新数据库为100,更新缓存为100。
- 线程B更新数据库为150,更新缓存为150。
- 若缓存写入顺序为 B→A,最终缓存值为100(与数据库150冲突)。
三、为何优先选择"删缓存"策略
1. 删缓存的优势
-
降低并发冲突 :
删除缓存后,后续读请求会触发缓存重建,此时直接从数据库加载最新值,避免旧数据残留。 -
简化一致性模型 :
通过 Cache-Aside 模式 (先更新数据库再删缓存)或 延迟双删 ,将一致性风险窗口控制在极短时间。写流程: 1. 删除缓存 → 2. 更新数据库 → 3. 延迟(如 500ms) → 4. 再次删除缓存
2. 删缓存的强一致性增强
-
分布式锁 :
在删缓存和更新数据库时加锁,确保同一时刻仅一个线程操作数据。
RLock lock = redisson.getLock("data_lock"); lock.lock(); try { redis.delete(key); mysql.update(data); } finally { lock.unlock(); }
-
版本号控制 :
在缓存值中嵌入版本号,更新时校验版本,防止旧数据覆盖。
{ "value": "data", "version": 3 // 与数据库版本号同步 }
3. 最终一致性的项目实践
-
异步监听 Binlog :
通过 Canal 或 Debezium 监听 MySQL 变更日志,异步更新或删除缓存,实现最终一致性。
MySQL → Binlog → MQ(如 Kafka) → 消费者更新/删除缓存
- 优势:业务代码零侵入,数据变更与缓存操作解耦。
- 延迟:通常控制在 100ms 以内,满足多数业务需求。
四、同步双写与删缓存的对比决策
维度 | 同步双写 | 删缓存策略 |
---|---|---|
一致性强度 | 理论强一致,实际受限于原子性 | 最终一致(通过延迟双删、Binlog 监听增强) |
性能影响 | 高延迟、低吞吐 | 低延迟、高吞吐 |
并发安全性 | 低(需额外锁机制) | 高(依赖缓存重建时序控制) |
适用场景 | 低频强一致需求(如金融核心数据) | 高频最终一致需求(如商品详情、社交动态) |