总结
为确保Redis与数据库双写一致性,常用方法包括:先更新数据库再删缓存(Cache-Aside)、延迟双删(更新前后均删缓存并延迟二次删除)、基于消息队列异步同步、监听数据库binlog触发缓存更新,以及设置缓存过期时间兜底。需结合业务场景选择策略,配合重试机制确保最终一致性。
详细解析
保证缓存与数据库的双写一致性是分布式系统设计中的核心挑战,需结合 业务场景 和 技术特性 选择合适策略。以下是主流解决方案及原理分析:
一、核心策略分类
策略 | 核心思想 | 适用场景 | 一致性级别 |
---|---|---|---|
Cache Aside | 读时优先读缓存,写时先更新数据库再删除缓存 | 通用场景(读多写少) | 最终一致性 |
延迟双删 | 更新数据库后,延迟一段时间再次删除缓存 | 高并发读写、热点数据 | 最终一致性 |
消息队列 异步同步 | 通过消息队列异步更新缓存,保证最终一致性 | 高吞吐、允许短暂延迟 | 最终一致性 |
分布式锁 | 写操作时加锁,确保同一数据串行更新 | 强一致性要求的高并发场景 | 强一致性 |
TCC 模式 |
业务层分阶段提交(Try-Confirm-Cancel),保证数据 库和缓存操作的原子性 | 金融级强一致性场景 | 强一致性 |
二、主流方案详解
- Cache Aside 模式(旁路缓存模式)
• 操作流程:
• 读请求:先读缓存 → 缓存未命中则读数据库 → 回填缓存。
• 写请求:先更新数据库 → 删除缓存(而非更新缓存)。
• 原理:
• 删除缓存避免旧值回填(若先更新缓存,其他线程可能读取旧数据库值并覆盖新缓存)。
• 依赖 读操作重建缓存 的惰性机制。
• 代码示例:
• 优点:实现简单,适合大多数读多写少场景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 读操作
public
Data getData(String key) {
Data data = cache.get(key);
if
(data ==
null
) {
data = db.query(key);
// 从数据库加载
cache.put(key, data);
// 回填缓存
}
return
data;
}
// 写操作
public
void
updateData(String key, Data newData) {
db.update(key, newData);
// 先更新数据库
cache.delete(key);
// 再删除缓存
}
• 缺点:存在短暂不一致窗口(如写操作后,新读请求可能命中旧缓存)。
• 优化:对热点数据采用 读写锁 或 互斥锁(如 Redis 的SETNX)。 - 延迟双删策略
• 操作流程:
• 第一次删除缓存:更新数据库后立即删除。
• 延迟删除:通过定时任务或异步线程,延迟一段时间后再次删除缓存。
• 原理:
• 解决 主从同步延迟 导致的脏数据问题(如从库读取旧数据回填缓存)。
• 延迟时间需大于主从同步耗时(通常 100-500ms)。
• 代码示例:
• 优点:降低脏数据概率。1
2
3
4
5
6
7
8
9
// 写操作
public
void
updateWithDoubleDelete(String key, Data newData) {
db.update(key, newData);
cache.delete(key);
// 第一次删除
scheduleDelete(key,
300
);
// 延迟 300ms 后再次删除
}
private
void
scheduleDelete(String key,
long
delay) {
executor.schedule(() -> cache.delete(key), delay, TimeUnit.MILLISECONDS);
}
• 缺点:需维护定时任务,可能增加系统复杂度。 - 消息队列异步同步
• 操作流程:
• 写数据库成功后,发送消息到 MQ。
• 消费 MQ 消息,更新或删除缓存。
• 原理:
• 通过 MQ 解耦数据库和缓存操作,保证最终一致性。
• 支持异步重试(如 RabbitMQ 的死信队列)。
• 代码示例:
• 优点:高吞吐、解耦系统。1
2
3
4
5
6
7
// 写操作
public
void
updateWithMQ(String key, Data newData) {
db.update(key, newData);
mq.send(
"cache-update"
, key);
// 发送删除缓存指令
}
// MQ 消费者
mq.consume(
"cache-update"
, key -> cache.delete(key));
• 缺点:依赖 MQ 可靠性,需处理消息积压和重复消费。 - 分布式锁控制
• 操作流程:
• 写操作前获取分布式锁(如 Redis 的 RedLock)。
• 更新数据库 → 删除缓存 → 释放锁。
• 原理:
• 串行化写操作,避免并发冲突。
• 代码示例:
• 优点:强一致性保障。1
2
3
4
5
6
7
8
9
10
11
public
void
updateWithLock(String key, Data newData) {
String lockKey =
"lock:"
+ key;
if
(redis.tryLock(lockKey,
10
, TimeUnit.SECONDS)) {
try
{
db.update(key, newData);
cache.delete(key);
}
finally
{
redis.unlock(lockKey);
}
}
}
• 缺点:性能瓶颈明显,锁粒度控制困难。 - TCC 模式(Try-Confirm-Cancel)
• 操作流程:
• Try 阶段:预留资源(如冻结账户余额)。
• Confirm 阶段:确认操作(如扣减余额并更新缓存)。
• Cancel 阶段:释放预留资源(如回滚余额并恢复缓存)。
• 原理:
• 业务层控制事务边界,保证数据库和缓存操作的原子性。
• 代码示例:
• 优点:强一致性,适合金融场景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Try 阶段
public
boolean
tryUpdate(String key, Data newData) {
// 冻结资源(如预扣库存)
return
db.tryUpdate(key, newData);
}
// Confirm 阶段
public
void
confirmUpdate(String key) {
db.commit(key);
cache.put(key, getFromDb(key));
// 更新缓存
}
// Cancel 阶段
public
void
cancelUpdate(String key) {
db.rollback(key);
cache.delete(key);
// 恢复旧缓存
}
• 缺点:业务侵入性强,需设计补偿逻辑。
三、选型建议
场景 | 推荐方案 | 原因 |
---|---|---|
通用业务(读多写少) | Cache Aside | 实现简单,性能高,适合大多数场景。 |
高并发写+热点数据 | 延迟双删 + 互斥锁 | 减少脏数据概率,控制重建并发。 |
异步解耦 | 消息队列 | 削峰填谷,适合最终一致性要求。 |
强一致性需求 | TCC 或分布式锁 | 保证操作原子性,适用于金融级场景。 |
四、总结
没有绝对完美的方案,需根据业务特点选择:
• 强一致性:TCC 或分布式锁(牺牲部分性能)。
• 最终一致性:Cache Aside 或消息队列(平衡性能与可靠性)。
• 特殊场景:延迟双删解决主从同步问题,多级缓存应对高并发。