强一致性 vs 最终一致性
在缓存一致性问题中,延迟双删、消息队列、加锁、版本号等策略各有优劣,具体选择需结合业务场景和一致性要求。以下是详细对比分析:
1. 强一致性 vs 最终一致性
- 强一致性:要求缓存与数据库的数据实时一致(如每次查询都能返回最新值)。
- 最终一致性:允许缓存与数据库存在短暂不一致,但最终会趋于一致。
版本号 + 缓存校验
- 一致性级别:强一致性。
- 原理:通过版本号校验,每次查询时强制验证缓存与数据库的版本是否一致。如果版本不一致,则更新缓存。
- 优点:
- 精确控制:确保每次查询都能获取最新数据。
- 无需额外锁或延迟:直接通过版本号判断,避免线程等待。
- 缺点:
- 增加数据库查询次数:每次查询都需要验证版本号,可能增加数据库压力。
- 实现复杂度高:需要维护版本号字段,并在每次更新时递增版本号。
加锁(分布式锁)
- 一致性级别:强一致性。
- 原理:通过锁机制强制串行化操作,确保同一时间只有一个线程读写缓存和数据库。
- 优点:
- 严格串行化:避免并发导致的脏数据。
- 简单直观:逻辑清晰,容易实现。
- 缺点:
- 性能瓶颈:锁竞争会导致吞吐量下降,尤其在高并发场景。
- 死锁风险:需谨慎设计锁的释放逻辑。
延迟双删
- 一致性级别:最终一致性。
- 原理:在数据库操作后立即删除缓存,再延迟一段时间后再次删除缓存,降低并发读写导致的脏数据概率。
- 优点:
- 低侵入性:无需修改数据库结构,仅需调整缓存删除逻辑。
- 性能较高:避免锁或版本号校验带来的开销。
- 缺点:
- 依赖延迟时间估算:若延迟时间不足,仍可能出现脏数据。
- 无法完全避免不一致:仅能降低概率,无法保证强一致性。
消息队列异步更新
- 一致性级别:最终一致性。
- 原理:通过消息队列异步通知缓存更新,解耦数据库和缓存操作。
- 优点:
- 解耦系统:降低数据库和缓存的耦合度,提升系统可维护性。
- 高吞吐量:适合高并发场景。
- 缺点:
- 实现复杂度高:需引入消息队列,处理消息丢失、重复消费等问题。
- 延迟不可控:消息处理可能存在延迟,导致缓存与数据库短暂不一致。
2. 场景对比与推荐方案
3. 综合推荐
最优方案:版本号 + 缓存校验 + 延迟双删
- 组合优势:
- 版本号校验:确保每次查询时缓存与数据库版本一致,解决强一致性需求。
- 延迟双删:在更新/删除操作后,通过延迟双删进一步降低并发导致的脏数据概率。
- 适用场景:对一致性要求高且已有版本号字段的系统(如大多数表已有版本号字段的项目)。
- 示例代码:
public Object queryPO(String eqpName) { Object cachedData = redis.get(eqpName); int cachedVersion = redis.getVersion(eqpName); if (cachedData == null) { // 缓存未命中,查库并更新缓存 Object data = db.query(eqpName); int dbVersion = db.getVersion(eqpName); redis.set(eqpName, data, dbVersion); return data; } // 校验版本号 int dbVersion = db.getVersion(eqpName); if (cachedVersion != dbVersion) { // 版本不一致,更新缓存 Object data = db.query(eqpName); redis.set(eqpName, data, dbVersion); return data; } return cachedData;}@Transactionalpublic void deletePO(String eqpName) { // 1. 删除数据库数据 db.delete(eqpName); // 2. 第一次删除缓存 redis.del(eqpName); // 3. 延迟后再次删除缓存(应对主从延迟或查询延迟) new Thread(() -> { try { Thread.sleep(1000); // 延迟时间根据查询耗时评估 redis.del(eqpName); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();}
4. 注意事项
- 版本号字段的原子性:
- 确保数据库更新操作(如
UPDATE ... SET version = version + 1
)是原子的,避免并发更新导致版本号冲突。
- 确保数据库更新操作(如
- 延迟时间估算:
- 延迟双删的延迟时间需根据实际查询耗时(包括数据库查询、网络传输、业务逻辑)合理设置(如 1s)。
- 缓存穿透与雪崩:
- 对不存在的数据(如删除后),可缓存空值并设置短 TTL,避免频繁查库。
- 为缓存设置随机过期时间,避免集中失效导致的雪崩。
5. 总结
- 强一致性需求:优先选择 版本号 + 缓存校验,结合 延迟双删 进一步优化。
- 高并发场景:使用 延迟双删 或 消息队列异步更新,降低系统耦合度。
- 已有基础设施:根据现有技术栈(如是否已有消息队列)选择最合适的方案。
通过合理组合上述策略,可以在性能和一致性之间取得平衡,满足不同业务场景的需求。