缓存的使用在项目中是极其常见的,如果使用得当,缓存可谓时提升系统性能的最简单方法之一,反之则会出现一些莫名其妙的问题,在不同场景下,所使用的缓存策略也是有所变化的,下面我们就介绍一下三种常见的缓存设计模式——Cache Aside Pattern(重点)、Read/Write Through Pattern、Write Behind Caching Pattern。

一、Cache Aside Pattern

先看一下我们最经常使用的查询缓存流程:查询请求过来先去读缓存,缓存命中则直接返回,如果未命中就去查数据库,有数据则写入缓存并返回结果,没数据也直接返回,如下图

这个缓存查询流程大家都不陌生,这应该是使用最为广泛的流程,但大部分人可能不知道,这个流程有一个名字:Cache Aside Pattern(旁路缓存模式),这是缓存设计模式的一种,也是Facebook比较推崇的一种模式,上面是该模式的查询流程,而更新流程如下:

ps:到此为止就是Cache Aside Pattern的全部内容了,由于此模式比较重要,下面我们将对其进行延申。

这个大家应该也并不陌生,更新完数据库后再把缓存给删了,下次再有查询请求过来时,就会先从数据库查询出更新后的数据再写入缓存,乍一看这个流程似乎很“完美”,但还有几个点需要我们思考一下:

  • 为什么是先更新数据库再操作缓存,它俩能对调一下吗?
  • 为什么是删除缓存而不是更新缓存?
  • 读写、写写并发下会有什么问题?

1.1 删除缓存VS更新缓存

为了保证数据库和缓存的一致性,很多人会在做数据更新的时候,顺便会把缓存也一并更新了,虽然这是我们正常的思维,但是我要说的是删除缓存要优于更新缓存。

1.1.1 缓存的数据

我们先抛开并发问题,单从缓存的数据方面看。缓存中的数据大部分情况下可能不只是一个简单的字符串,可能是一个大JSON串比如我们需要通过缓存进行扣减库存的时候,可能需要从缓存中查出整个订单模型数据,把它反序列化之后,再解析其中库存的字段,修改后再序列化放进缓存中。

可以看到相比于直接删除缓存,更新缓存更为复杂,也较容易出错。下面我们再来看看这两种操作在并发情况下的表现。

1.1.2 写写并发

"写写并发"下,如果同时更新缓存和数据库,那么就会很容易出现并发问题导致数据不一致的情况。

先写数据库,再更新缓存:

 先更新缓存,再写数据库:

可以看到,如果是更新缓存的话,无论是先操作数据库还是先操作缓存,都会出现数据不一致的情况(标红的地方)。但如果是直接删除缓存的话,就不会出现数据不一致的情况。

但它有个小缺点,就是会带来一次额外的cache miss,也就是说删除缓存后的下一次查询无法命中缓存,需要再查一下数据库。

ps:如果同一个key有大量的请求过来,在某种程度上可能会导致缓存击穿,不过可以通过加锁来解决。

所以,删除缓存相较于更新缓存,方案更简单,一致性问题也更少,建议大家优先选择删除缓存。

1.2 先写数据库VS先删缓存

在确定了优先选择删除缓存后,那么先写数据库还是先删缓存呢,下面一起来看看。

1.2.1 先写数据库

在读写并发下:

正常情况下,脏数据时间范围:更新数据库后,删除缓存前。这个时间范围很小,通常不会超过几毫秒。

异常情况下,如果缓存删除失败,会导致数据库中的数据已经更新,而缓存还是旧数据,从而导致数据不一致,但这种情况除非是网络问题或者缓存服务器宕机,否则大部分情况下还是会成功的。

1.2.2 先删除缓存

首先,如果是先删除缓存,那么第二步写数据库失败是可以接收的,因为这样不会有脏数据。

但是,先删除缓存再更新数据库,会放大"读写并发"导致的数据不一致问题。我们知道查询缓存的流程是这样的:

  • 查询缓存,命中则直接返回。
  • 查询数据库。
  • 把数据库的值更新进缓存。

对于一个读线程来说,虽然不会写数据库,但是会写缓存。如下图:

脏数据时间范围:更新数据库后,下一次对该数据更新前。这个时间范围不确定性很大:

  • 如果下一次数据更新马上来,那么会失效缓存,脏数据时间就很短。
  • 如果下一次数据更新很久才到,那么这期间保存的都是脏数据。

通过上述两个方案的比较可以看出,先操作数据库和先操作缓存都会存在脏数据的情况。但相比之下,先操作数据库再操作缓存时更优的方式,即使再并发的情况下,也只会出现很小量的脏数据。

ps:由于篇幅原因,缓存和数据库一致性的解决方案下篇文章再介绍。

二、Read/Write Through Pattern

在Cache Aside中,应用层需要和缓存和数据库两个数据源打交道,这增加了应用层的复杂度,而Read/Write Through Pattern就是来解决这个问题的,在这两种模式下,应用层只需要将缓存作为主要数据源,不需要感知数据库,更新和读取的任务都交给缓存来代理。

2.1 Read Through Pattern

在Read Though模式下,是由缓存配置一个读模块,应用层查询数据时,当缓存未命中时,由缓存去查询数据库,并且将结果写入缓存中,最后返回结果给应用层:

在上述流程图中,红色框内的操作不再由应用层来处理,而是由缓存自己处理。

2.2 Write Though Pattern

在Write Though模式下,是由缓存配置一个写模块,应用层更新数据时,由缓存去更新数据库。同时,当缓存命中时,写缓存和写数据库这两个操作在一个事务中完成,保证同时成功:

 

当使用Write Though时,一般都配合使用Read Though来使用。Write Though适用的场景有:

  • 需要频繁读取相同数据。
  • 不能忍受丢失数据(相对于Write Behind而言)和数据不一致。

在使用Write-Through时要特别注意的是缓存的有效性管理,否则会导致大量的缓存占用内存资源。甚至有效的缓存数据被无效的缓存数据给清除掉。

三、Write Behind Caching Pattern

Write Behind又称为Write Back,从应用层视角来看和Write Through类似,在该模式下,应用层也只需和缓存一个数据源打交道,不同的是Write Through会立即把数据写入数据库,而Write Behind会在一段时间之后(或者被其它方式触发)把数据批量写入数据库,这个异步写操作是Write Behind的最大特点。

这样做的优点是:

  • 应用层操作只写缓存,速度非常快。
  • 缓存在异步地写数据库时,会将多个I/O操作合并为一个,减少I/O次数。

缺点是:

  • 复杂度高。
  • 更新后的数据还未写入数据库时,如果此时断电,数据将无法找回。

Write Behind缓存模式由于其复杂度比较高,所以在业务应用中适用的比较少,但是由于其性能比较好,还是有不少优秀的软件采用了该模式,比如:linux中的页缓存、Mysql中的InnoDB存储引擎。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐