「深入理解」缓存更新策略及缓存不一致问题解决方案

976 阅读11分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

「深入理解」缓存更新策略及缓存不一致问题解决方案

一、缓存更新策略

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。

内存淘汰

redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

超时剔除

当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除,方便咱们继续使用缓存

主动更新

我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

内存淘汰超时剔除主动更新
说明内存淘汰是Redis内部的一种淘汰机制,当内存不足的时候自动淘汰部分数据。这种机制不需要自己维护,被淘汰的数据在下次查询的时候更新缓存。给缓存数据添加TT了时间,到期后Redis自动删除缓存数据。被淘汰的数据在下次查询时候更新缓存通过业务逻辑代码,在修改数据库的数据的同时,更新缓存
一致性一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

二、数据库缓存不一致解决方案

由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有一致性问题存在

其后果是:用户使用缓存中的过时数据,就会产生类似多线程数据安全问题,从而影响业务,产品口碑等;

怎么解决呢?有如下几种方案

Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案

Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致

image-20221102221822218

Cache Aside Pattern

读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

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


下面两种模式中,应用程序将缓存作为主要的数据源,不需要感知数据库,更新数据库和从数据库的读取的任务都交给缓存来代理。

Read/Write Through Pattern

Read Through模式下,是由缓存配置一个读模块,它知道如何将数据库中的数据写入缓存。在数据被请求的时候,如果未命中,则将数据从数据库载入缓存。

Write Through模式下,缓存配置一个写模块,它知道如何将数据写入数据库。当应用要写入数据时,缓存会先存储数据,并调用写模块将数据写入数据库。

也就是说,这两种模式下,不需要应用自己去操作数据库,缓存自己就把活干完了。

Write Behind Caching Pattern

这种模式就是在更新数据的时候,只更新缓存,而不更新数据库,然后再异步的定时把缓存中的数据持久化到数据库中。

这种模式的优缺点比较明显,那就是读写速度都很快,但是会造成一定的数据丢失。

这种比较适合用在比如统计文章的访问量、点赞等场景中,允许数据少量丢失,但是速度要快。


三、数据库和缓存不一致采用什么方案

综合考虑使用方案一,但是方案一调用者如何处理呢?

操作缓存和数据库时有三个问题需要考虑:

删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存

如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来。


如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC等分布式事务方案

先操作缓存还是先操作数据库?

  • 先删除缓存,再操作数据库
  • 先操作数据库,再删除缓存

image-20221102224505763

先删除缓存,再操作数据库

在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。

后写数据库这种方式,会无形中放大前面我们提到的”读写并发”导致的数据不一致的问题。

因为这种”读写并发”问题发生的前提是读线程读缓存没读到值,而先删缓存的动作一旦发生,刚好可以让读线程就从缓存中读不到值。

所以,本来一个小概率会发生的”读写并发”问题,在先删缓存的过程中,问题发生的概率会被放大。

而且这种问题的后果也比较严重,那就是缓存中的值一直是错的,就会导致后续的所以命中缓存的查询结果都是错的!

先操作数据库,再删除缓存

因为数据库和缓存的操作是两步的,没办法做到保证原子性,所以就有可能第一步成功而第二步失败。

而一般情况下,如果把缓存的删除动作放到第二步,有一个好处,那就是缓存删除失败的概率还是比较低的,除非是网络问题或者缓存服务器宕机的问题,否则大部分情况都是可以成功的。

还有就是,先写数据库后删除缓存虽然不存在”写写并发”导致的数据一致性问题,但是会存在”读写并发”情况下的数据一致性问题。

我们知道,当我们使用了缓存之后,一个读的线程在查询数据的过程是这样的:

1、查询缓存,如果缓存中有值,则直接返回

2、查询数据库

3、把数据库的查询结果更新到缓存中

所以,对于一个读线程来说,虽然不会写数据库,但是是会更新缓存的,所以,在一些特殊的并发场景中,就会导致数据不一致的情况。

也就是说,假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询,但是如果自查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就导致最终缓存会被重新用一个”旧值”覆盖掉。

这也就导致了缓存和数据库的不一致的现象

但是这种现象其实发生的概率比较低,因为一般一个读操作是很快的,数据库+缓存的读操作基本在十几毫秒左右就可以完成了。

而在这期间,更好另一个线程执行了一个比较耗时的写操作的概率确实比较低。

延迟双删

那么,虽然先写数据后删除缓存的这种情况,可以大大的降低并发问题的概率,但是,根据墨菲定律,只要有可能发生的坏事,那就基本上会发生。越是庞大的系统发生的概率越高。

那么,有没有什么办法可以来解决一下这种情况带来的不一致的问题呢?

其实是有一个比较常见的方案的,在很多公司内用的也比较多,那就是延迟双删

因为”读写并发”的问题会导致并发发生后,缓存中的数被读线程写进去脏数据,那么就只需要在写线程在写数据库、删缓存之后,延迟一段时间,在执行一把删除动作就行了。

这样就能保证缓存中的脏数据被清理掉,避免后续的读操作都读到脏数据。当然,这个延迟的时长也很久讲究,到底多久来删除呢?一般建议设置1-2s就可以了。

当然,这种方案也是有一个弊端的,那就是可能会导致缓存中准确的数据被删除掉。当然这也问题不大,就像我们前面说过的,只是增加一次cache miss罢了。


四、如何选择

前面介绍了几种情况的具体问题和解决方案,那么实际工作中应该如何选择呢?

我觉得主要还是根据实际的业务情况来分析。

比如,如果业务量不大,并发不高的情况,可以选择先删除缓存,后更新数据库的方式,因为这种方案更加简单。

但是,如果是业务量比较大,并发度很高的话,那么建议选择先更新数据库,后删除缓存的方式,因为这种方式并发问题更少一些。但是可能会引入加锁、延迟双删等更多机制,使得整个方案会更加复杂。

其实,先操作数据库,后操作缓存,是一种比较典型的设计模式——Cache Aside Pattern

这种模式的主要方案就是先写数据库,后删缓存,而且缓存的删除是可以在旁路异步执行的。

这种模式的优点就是我们说的,他可以解决”写写并发”导致的数据不一致问题,并且可以大大降低”读写并发”的问题,所以这也是Facebook比较推崇的一种模式。


五、优化方案

Cache Aside Pattern 这种模式中,我们可以异步的在旁路处理缓存。其实这种方案在大厂中确实有的还蛮多的。

主要的方式就是借助数据库的binlog或者基于异步消息订阅的方式。

也就是说,在代码的主要逻辑中,先操作数据库就行了,然后数据库操作完,可以发一个异步消息出来。

然后再由一个监听者在接到消息之后,异步的把缓存中的数据删除掉。

或者干脆借助数据库的binlog,订阅到数据库变更之后,异步的清除缓存。

这两种方式都会有一定的延时,通常在毫秒级别,一般用于在可接受秒级延迟的业务场景中。


六、总结

《人月神话》的作者Fred Brooks在早年有一篇很著名文章《No Silver Bullet》 ,他提到:

在软件开发过程里是没有万能的终杀性武器的,只有各种方法综合运用,才是解决之道。而各种声称如何如何神奇的理论或方法,都不是能杀死“软件危机”这头人狼的银弹。

也就是说,没有哪种技术手段或者方案,是放之四海皆准的。如果有的话,我们这些工程师也就没有存在的必要了。

所以,任何的技术方案,都是一个权衡的过程,要权衡的问题有很多,业务的具体情况,实现的复杂度、实现的成本,团队成员的接受度、可维护性、容易理解的程度等等。

所以,没有一个”完美”的方案,只有”适合”的方案。

但是,如何能选出一个适合的方案,这里面就需要有很多的输入来做支撑了。希望本文的内容可以为你日后的决策提供一点参考!


参考

Hollis

黑马程序员