缓存“延时双删”真的有必要吗?我觉得没有

本文分析了缓存的常见读写模式,包括Cache Aside Pattern、Read/Write Through Pattern和Write Behind Caching Pattern。讨论了缓存更新时先删除后更新的策略,以及延时双删的优缺点。尽管延时双删能减少数据不一致,但增加了系统复杂性,实际应用中需权衡利弊。

前言

我们在实际的项目中经常会使用到缓存,本文我们主要来总结下缓存的几种常见的读写模式。

缓存的读写模式

Cache Aside Pattern(常用)

Cache Aside Pattern(旁路缓存),是最经典的缓存+数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

image-20210308153152098

更新的时候,先更新数据库,然后再删除缓存 。

image-20210308153239381

**为什么是删除缓存,而不是更新缓存呢? **

原因有两点:

  1. 如果缓存的结构是一个hash或list等,更新数据时需要遍历,比较耗时
  2. 我们对于缓存的要求是懒加载,也就是使用的时候才更新即可。我们可以采用异步的方式填充缓存,比如开启一个线程,定时将DB(数据库)中的数据刷到缓存中去。

高并发脏读的三种情况
1、先更新数据库,再更新缓存
update与commit之间,更新缓存,commit失败
则DB与缓存数据不一致。

再举一例:

1、A更新数据库

2、B更新数据库

3、B写入缓存

4、A写入缓存

本来最终结果应该是B写入缓存才对,而由于网络延迟等原因,导致A与B写入缓存的顺序颠倒,造成数据不一致。

2、先删除缓存,再更新数据库
update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据
commit后 DB为新数据
则DB与缓存数据不一致
3、先更新数据库,再删除缓存(推荐)
update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据
commit后 DB为新数据
则DB与缓存数据不一致
解决方法:采用延时双删策略

延时双删

为什么采用延时双删?

  1. 假如只先删缓存,会出现在第一个事务更新数据库之前,另一个事务又将旧数据放入缓存的问题,如下图所示:

image-20210309095726117

  1. 假如只后删缓存,那么在更新完数据库之后、更新缓存之前的这段时间内,其他事务的查询都拿到的是旧数据,如下图所示:

image-20210309101009500

  1. 普通双删

存在的问题:第一次清空缓存后,更新数据库前的这段时间内,其他事务查询了数据库的数据,第二次清空缓存后,刚才查询数据库 的那个线程又更新了缓存,此时又会将旧数据更新到缓存。

如下图所示:

image-20210309141557606

  1. 延时双删

在3中,第二次清空缓存之前,多延时一会,等B更新缓存结束了,再删除缓存,这样缓存就不存在了,其他事务查到的就是新缓存。

延时操作是为了确保 修改数据库——>清空缓存前,这段时间内,其他事务的更新缓存操作已完成。

延时删

采用延时删最后一次缓存,但这其中难免还是会大量的查询到旧缓存数据的,如下图所示:

延时双删

这时候可以通过加锁来解决,一次性不让太多的线程都来请求,另外从图上看,我们可以尽量缩短第一次删除缓存和更新数据库的时间差,这样可以使得其他事务第一时间获取到更新数据库后的数据。

Read/Write Through Pattern

Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入
缓存。(guavacache采用此种方式)

Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。

该种模式需要提供数据库的handler,开发较为复杂。

Write Behind Caching Pattern

描述:应用程序只更新缓存。缓存通过异步的方式将数据批量或合并后更新到DB中
存在的问题:不能时时同步,甚至会丢数据

总结

本文主要学习了缓存的几种读写模式,常用的旁路缓存模式,重点研究了延时双删的策略来减少访问缓存出现的问题。
最后说一句,缓存双删在实际场景中并不常用,因为增加了系统的复杂度,很多时候得不偿失,还是得根据具体的场景来考虑对应的解决方案,看你要具体解决什么问题,分清楚主次,不要一上来就延时双删,只能贻笑大方。

更多我亲身经历的面试真题,还有想要内推大厂的小伙伴可以联系我,请关注微信公众号:【程序员资料站】,回复关键字 “面试” 获取更多面试资料,回复“内推”,我帮你内推大厂。

<think> 首先,用户的问题是:“我想了解延时策略中再次缓存的原因”。 回顾之前的对话,系统级指令要求所有回答使用中文,并且遵循特定的格式规则:行内数学表达式用$...$,独立公式用$$...$$,并生成相关问题。 用户提到了延时策略,并引用了几个来源: - 引用[1]:延迟的正确流程是先缓存,再更新数据库,最后延迟后再次缓存;但更推荐先更新数据库,再缓存,最后延迟后再次缓存。 - 引用[2]:解释了延迟的目的是让其他请求填充缓存后再除,确保下次填充时使用新数据。 - 引用[3]:给出了延迟的代码框架,包括两次缓存和线程睡眠。 - 引用[4]:描述了并发场景,展示了用户A更新数据库缓存可能导致一致的问题。 核心问题是:为什么需要再次缓存? 从引用中提取关键点: - 延迟的主要目的是解决缓存数据库在并发环境下的一致问题。 - 第一次缓存是为了防止旧数据被读取后写入缓存。 - 第二次缓存(延迟后)是为了清除在第一次除后可能被其他请求填充的旧数据。 原因: 1. **并发问题**:在更新数据库时,多个请求可能同时读写缓存,导致缓存中存储旧数据。 - 引用[4]举例:用户A更新数据库缓存,但用户B可能在缓存后读取数据库旧数据并写入缓存;用户C可能在除前就读取了旧数据并写入缓存。 2. **延迟的作用**:延迟一段时间是为了让当前正在进行的请求(如用户B读取旧数据并写入缓存)完成操作,然后再缓存,确保缓存被清除后,下一次读取能从数据库获取新数据。 3. **确保一致性**:第二次缓存是为了捕捉在数据库更新过程中可能漏掉的缓存写入,确保缓存最终与数据库一致。 推荐流程(先更新数据库缓存)能降低一致概率,因为如果先缓存,别的请求可能立即填充旧数据到缓存。 回答结构: - 解释原因:使用中文,步骤清晰。 - 可能添加代码示例(如引用[3])。 - 末尾添加相关问题(根据指令)。 - 引用来源:在末尾添加引用标识[^1], [^2], 等。 生成相关问题:指令要求“生成相关问题”,所以在回答结束后添加相关问题。 最终回答框架: 1. 概述延迟策略。 2. 解释为什么需要再次缓存。 3. 提供代码或简单示例。 4. 添加相关问题。 </think> ### 延时策略中再次缓存的原因 延时策略的核心目标是解决缓存数据库在并发场景下的数据一致问题。再次缓存(即第二次除)是策略的关键步骤,其主要原因如下: 1. **清除在高并发下可能残留的旧数据缓存**: - 在数据库更新过程中,如果第一次缓存后,其他并发请求(如用户B)可能在这段时间内读取了数据库中的旧数据,并将其重新写入缓存。 - 例如,用户A更新数据库后立即缓存,但用户B在缓存除后立即读取数据库(此时缓存为空),并将旧数据填充到缓存中(引用[2]; 引用[4])。 - 第二次除(延迟后执行)专门清除这类被错误填充的旧数据,确保下次请求读取缓存时能触发从数据库加载新数据。 2. **应对“读-写”并发冲突**: - 在更新操作(如用户A)期间,读操作(如用户C)可能在第一次缓存前就读取了旧数据并写入缓存(引用[4])。 - 延迟时间(如100ms)允许这些读操作完成缓存填充,然后通过第二次除强制清除缓存,防止旧数据长时间残留(引用[3])。 3. **降低数据一致的概率**: - 第一次缓存可能无法覆盖所有并发场景,第二次除作为“兜底”机制,确保缓存最终与数据库一致。 - 尤其在使用推荐流程(先更新数据库缓存)时,第二次除进一步减少了因网络延迟或并发写入导致的一致风险(引用[1]; 引用[2])。 ### 简单示例说明 以引用[3]的代码框架为例: ```java public void write(String key, Object data) { redisUtils.del(key); // 第一次缓存(推荐流程中可省略) db.update(data); // 更新数据库(推荐流程优先) Thread.sleep(100); // 延迟等待,让并发读请求完成缓存填充 redisUtils.del(key); // 第二次缓存,清除可能的旧数据 } ``` - **延迟时间的作用**:例如100ms的延迟,是为了让之前的并发请求(如用户B和用户C)完成对旧数据的缓存写入(引用[2])。 - **为什么需要第二次除**:第一次除后,用户B可能已将旧数据写入缓存($ \text{缓存}_{\text{旧}} $);第二次除后,下一次读取会触发缓存重建($ \text{缓存}_{\text{新}} $),确保数据一致性[^4]。 ### 总结 再次缓存的根本原因是**解决第一次除后并发请求填充旧数据的风险**,通过延迟机制让这些操作完成,再强制清除缓存,使系统最终达到一致性(引用[1]; 引用[2])。实际应用中,需合理设置延迟时间(通常基于系统平均响应时间),以平衡性能和一致性(参考引用[3])。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员资料站

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值