前言
我们在实际的项目中经常会使用到缓存,本文我们主要来总结下缓存的几种常见的读写模式。
缓存的读写模式
Cache Aside Pattern(常用)
Cache Aside Pattern(旁路缓存),是最经典的缓存+数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存 。
**为什么是删除缓存,而不是更新缓存呢? **
原因有两点:
- 如果缓存的结构是一个hash或list等,更新数据时需要遍历,比较耗时
- 我们对于缓存的要求是懒加载,也就是使用的时候才更新即可。我们可以采用异步的方式填充缓存,比如开启一个线程,定时将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与缓存数据不一致
解决方法:采用延时双删策略
延时双删:
为什么采用延时双删?
- 假如只先删缓存,会出现在第一个事务更新数据库之前,另一个事务又将旧数据放入缓存的问题,如下图所示:
- 假如只后删缓存,那么在更新完数据库之后、更新缓存之前的这段时间内,其他事务的查询都拿到的是旧数据,如下图所示:
- 普通双删
存在的问题:第一次清空缓存后,更新数据库前的这段时间内,其他事务查询了数据库的数据,第二次清空缓存后,刚才查询数据库 的那个线程又更新了缓存,此时又会将旧数据更新到缓存。
如下图所示:
- 延时双删
在3中,第二次清空缓存之前,多延时一会,等B更新缓存结束了,再删除缓存,这样缓存就不存在了,其他事务查到的就是新缓存。
延时操作是为了确保 修改数据库——>清空缓存前,这段时间内,其他事务的更新缓存操作已完成。
采用延时删最后一次缓存,但这其中难免还是会大量的查询到旧缓存数据的,如下图所示:
这时候可以通过加锁来解决,一次性不让太多的线程都来请求,另外从图上看,我们可以尽量缩短第一次删除缓存和更新数据库的时间差,这样可以使得其他事务第一时间获取到更新数据库后的数据。
Read/Write Through Pattern
Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入
缓存。(guavacache采用此种方式)
Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。
该种模式需要提供数据库的handler,开发较为复杂。
Write Behind Caching Pattern
描述:应用程序只更新缓存。缓存通过异步的方式将数据批量或合并后更新到DB中
存在的问题:不能时时同步,甚至会丢数据
总结
本文主要学习了缓存的几种读写模式,常用的旁路缓存模式,重点研究了延时双删的策略来减少访问缓存出现的问题。
最后说一句,缓存双删在实际场景中并不常用,因为增加了系统的复杂度,很多时候得不偿失,还是得根据具体的场景来考虑对应的解决方案,看你要具体解决什么问题,分清楚主次,不要一上来就延时双删,只能贻笑大方。
更多我亲身经历的面试真题,还有想要内推大厂的小伙伴可以联系我,请关注微信公众号:【程序员资料站】,回复关键字 “面试” 获取更多面试资料,回复“内推”,我帮你内推大厂。