前言

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

缓存的读写模式

Cache Aside Pattern(常用)

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

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

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

原因有两点:

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

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

  1. 普通双删

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

如下图所示:

  1. 延时双删

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

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

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

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

Read/Write Through Pattern

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

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

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

Write Behind Caching Pattern

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

总结

本文主要学习了缓存的几种读写模式,常用的旁路缓存模式,重点研究了延时双删的策略来减少访问缓存出现的问题。