缓存更新和数据库更新先后顺序

本文探讨了四种缓存更新策略,包括先更新缓存再更新数据库、先删除缓存再更新数据库等,分析了各种策略可能导致的数据不一致性和解决方案。推荐采用先更新数据库再删除缓存的方式,以降低数据不一致的概率,并强调理解基础技术的重要性。同时,提到了缓存淘汰与更新的成本比较,以及分布式系统事务处理的相关内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、先更新缓存再更新数据库

1)读请求先查询缓存,缓存击中,查询数据库返回数据;
2)写请求更新数据库,删除缓存;
3)读请求回写缓存

2、先删除缓存再更新数据库

1)写请求删除缓存后;
2)读请求没有命中缓存,取数据库读到旧数据,回写到缓存;
3)写请求更新数据库,
4)如果没有写操作,在缓存中的数据则一直是老的数据。

3、先更新数据库再更新缓存

1)写请求1更新数据库;
2)写请求2更新数据库,写请求2更新缓存;
3)写请求1更新缓存
4)数据库和缓存数据不一致

4、先更新数据库再删除缓存

1)读请求先查询缓存,缓存未击中,查询数据库返回数据;
2)写请求更新数据库,删除缓存;
3)读请求回写缓存

虽然缓存和数据库数据不一致,但是发生概率极低,因为数据库更新操作比内存操作耗时大很多,最后一步回写缓存速度超级快,通常会在更新数据前完成,所以可以不考虑。可以给缓存数据设置过期时间,这样只是再极低概率下短时间内数据不一致,不影响后续查询。

5、淘汰缓存还是更新缓存:大部分情况,修改value成本会高于“增加一次cache miss”,因此应该淘汰缓存

结论:
推荐采用先更新数据库,后删除缓存,

引用篇尾原文链接里的话,原文:缓存更新的套路

云计算中的很多虚拟化技术的原理,和传统的虚拟内存不是很像么?Unix下的那些I/O模型,也放大到了架构里的同步异步的模型,还有Unix发明的管道不就是数据流式计算架构吗?TCP的好些设计也用在不同系统间的通讯中,仔细看看这些微观层面,你会发现有很多设计都非常精妙……所以,请允许我在这里放句观点鲜明的话——如果你要做好架构,首先你得把计算机体系结构以及很多老古董的基础技术吃透了。

看看相应的guideline,best practice或design pattern,吃透了已有的这些东西,再决定是否要重新发明轮子

也可参考:CANAL
参考链接:
很全面的对原文做了标注
原文:缓存更新的套路
分布式系统的事务处理

<think>好的,用户现在想了解如何处理缓存数据库双写不一致的问题,特别是在多读多写场景下的解决方案。之前用户已经询问过关于Canalbinlog监听的问题,现在这个问题看起来是进一步的系统设计问题,特别是在缓存数据库一致性方面。 首,我需要回忆一下常见的缓存数据库双写不一致的场景解决方案。用户提到了两种场景:多读多写。在多读场景下,用户提到了更新数据库再删除缓存,并且使用消息队列或Canal+消息队列来处理缓存删除。而在多写场景下,用户提到了延迟双删的策略,结合分布式锁。 接下来,我需要确认这些方法的正确性适用性。例如,更新数据库再删除缓存是否真的能解决多读场景下的不一致问题?这里可能要考虑经典的缓存更新策略,比如Cache-Aside模式。通常,正确的做法是更新数据库后使缓存失效,而不是直接更新缓存,这样可以避免并发写导致的问题。因此,更新数据库再删除缓存是合理的,但需要处理删除失败的情况,这时候引入消息队列或者通过Canal监听binlog异步删除缓存,可以增加可靠性,确保最终一致性。 对于多写场景,延迟双删的策略确实是一个常见的方法。删除缓存,然后更新数据库,之后再次延迟删除缓存。这里的关键是延迟的时间需要足够长,以确保在数据库主从同步完成后再删除缓存,避免读到旧数据。同时,使用分布式锁来保证操作的原子性,防止多个线程同时操作导致的问题。需要确认这个延迟时间的设置,通常是根据业务的主从同步延迟来估算的,比如1-2秒。 另外,用户提到使用分布式锁,这部分需要详细说明,比如锁的范围、如何避免死锁,以及锁的粒度。比如,锁应该基于数据的主键或其他唯一标识,确保同一数据在同一时间只有一个线程在处理。同时,锁的释放时机也需要考虑,确保在数据库更新第二次缓存删除之后释放,避免其他线程在中间状态介入。 还需要考虑异常处理的情况。比如,如果第一次删除缓存失败,或者数据库更新失败,或者第二次删除缓存失败,系统应该如何应对。这时候可能需要重试机制,或者将操作记录到消息队列中,由消费者负责重试,直到成功为止。此外,是否需要引入版本号或者时间戳来避免旧数据覆盖新数据,这也是需要考虑的点。 用户可能在实际应用中遇到了缓存不一致的问题,特别是在高并发场景下,比如电商秒杀、社交网络热点数据等。他们可能需要一个可靠的解决方案来保证数据的一致性,同时不影响系统性能。因此,除了理论上的方案,还需要给出实践中的注意事项,比如监控、日志记录、重试策略、超时设置等。 另外,用户可能对Canal的应用场景比较熟悉,所以他们可能会倾向于使用Canal来监听数据库变更,进而触发缓存删除。这种情况下,需要解释Canal如何与消息队列结合,异步处理缓存失效,从而解耦业务逻辑缓存更新操作,提高系统的可靠性扩展性。 最后,要对比不同的方案,比如更新数据库再删除缓存与延迟双删的适用场景,以及各自的优缺点。帮助用户理解在不同情况下如何选择最合适的策略,以及如何结合使用这些策略来达到最佳效果。</think>处理缓存数据库双写不一致的问题是高并发系统中的经典挑战。以下是针对 **多读场景** **多写场景** 的常见解决方案,结合延迟双删、分布式锁、消息队列 Canal 等技术实现最终一致性: --- ## **1. 多读场景(读多写少)** ### **核心问题** - 并发读时,缓存数据可能因数据库更新未及时同步而返回旧值。 - 直接“更新数据库,再更新缓存”可能导致脏数据(例如并发写时的操作覆盖)。 ### **解决方案:更新数据库,后删除缓存** - **操作步骤**: 1. **更新数据库**:优保证数据库写入成功。 2. **删除缓存**:使旧缓存失效,后续读请求会从数据库加载最新数据到缓存。 - **关键点**: - **删除缓存而非更新缓存**:避免并发写导致缓存数据错乱(如两个线程更新缓存,顺序可能颠倒)。 - **异步化缓存删除**:通过消息队列或 Canal 监听 binlog 异步删除,降低对主流程的性能影响。 #### **实现方式** - **方式一:业务代码同步删除缓存** ```java // 伪代码示例 public void updateData(Data data) { // 1. 更新数据库 db.update(data); // 2. 同步删除缓存(可能失败,需重试) cache.delete(data.getId()); } ``` **缺点**:缓存删除失败会导致不一致(需重试机制补偿)。 - **方式二:消息队列异步删除(最终一致性)** ```java public void updateData(Data data) { // 1. 更新数据库 db.update(data); // 2. 发送删除缓存的消息到队列(解耦) mq.send("cache_delete_topic", data.getId()); } // 消费者处理消息 mq.consume("cache_delete_topic", (id) -> { cache.delete(id); }); ``` **优点**:解耦主流程,通过消息队列保证至少一次消费(需幂等性处理)。 - **方式三:Canal + 消息队列(无侵入式)** - 使用 **Canal 监听数据库 binlog**,捕获数据变更事件。 - 将变更事件(如 UPDATE)发送到消息队列,由消费者删除缓存。 ```java // Canal 解析到数据变更后,推送消息到队列 mq.send("cache_delete_topic", updatedId); // 消费者逻辑同上 ``` **优点**:业务代码无需感知缓存逻辑,适合老旧系统改造。 --- ## **2. 多写场景(写并发高)** ### **核心问题** - 高并发写场景下,多个线程同时操作数据库缓存,可能导致缓存数据库数据错乱。 - 例如:线程 A 删除缓存 → 线程 B 读缓存未命中 → 线程 B 读数据库旧值 → 线程 B 更新缓存 → 线程 A 更新数据库,导致缓存数据库不一致。 ### **解决方案:延迟双删 + 分布式锁** - **操作步骤**: 1. **第一次删除缓存**:确保后续读请求强制回源数据库。 2. **更新数据库**:完成数据持久化。 3. **延迟后第二次删除缓存**:等待主从同步完成,确保删除的是可能的脏数据。 - **关键点**: - **延迟时间**:需覆盖主从同步延迟(通常 500ms~1s)。 - **分布式锁**:保证同一数据的更新操作串行化,避免并发写干扰。 #### **实现示例** ```java public void updateDataWithLock(Data data) { String lockKey = "LOCK_DATA_" + data.getId(); // 1. 获取分布式锁(如 Redis RedLock) boolean locked = redis.lock(lockKey, 10s); if (!locked) throw new RetryException(); try { // 2. 第一次删除缓存 cache.delete(data.getId()); // 3. 更新数据库 db.update(data); // 4. 延迟后再次删除缓存(异步执行) scheduleTask(() -> { cache.delete(data.getId()); }, 1s); // 延迟时间根据主从同步时间调整 } finally { // 释放锁 redis.unlock(lockKey); } } ``` #### **优化点** - **异步延迟删除**:通过消息队列延迟消息或线程池调度实现,避免阻塞主线程。 ```java // 发送延迟消息(如 RocketMQ 延迟消息) mq.sendDelayMessage("cache_delete_topic", data.getId(), 1s); ``` - **锁粒度细化**:锁的 Key 应基于数据唯一标识(如主键),减少锁竞争。 --- ## **3. 通用设计原则** ### **保证最终一致性** - **重试机制**:缓存删除失败时,通过消息队列重试或定时任务补偿。 - **数据版本号**:在缓存数据库中添加版本号,仅允许高版本数据覆盖低版本。 - **读操作兜底**:读缓存未命中时,加分布式锁防止并发回源(缓存击穿)。 ### **监控与告警** - **缓存一致性对比**:定期对比缓存数据库数据,触发告警。 - **同步延迟监控**:关注 Canal 或主从同步延迟,避免因延迟导致双删失效。 --- ## **方案对比与选型** | 场景 | 方案 | 优点 | 缺点 | |--------------|--------------------|-------------------------------|-------------------------------| | **多读场景** | 更新DB + 删除缓存 | 简单,适合读多写少 | 需处理缓存删除失败 | | **多读场景** | Canal + 消息队列 | 业务无侵入,适合复杂系统 | 依赖中间件,运维成本高 | | **多写场景** | 延迟双删 + 分布式锁 | 有效降低并发写不一致概率 | 需合理设置延迟时间,锁影响性能 | --- ## **总结** - **多读场景**:优选择 **更新数据库,后异步删除缓存**,结合消息队列或 Canal 保证最终一致性。 - **多写场景**:使用 **延迟双删 + 分布式锁** 串行化写操作,确保缓存最终与数据库一致。 - **兜底措施**:始终设计重试、监控对比机制,应对极端场景下的不一致问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值