redis 如何保证缓存和数据库一致性?
谢邀!!我是大明哥,一个专注 Java 技术的硬核程序员,「死磕 Java」 创始人。
回答
一般来说,Redis 和数据库的双写方案有如下四种:先更新缓存,再更新数据库先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存
由于更新缓存比删除缓存更加复杂且容易产生数据不一致的情况,所以推荐采用删除缓存策略。针对删除缓存的方案,我们可以基于如下策略做选择:业务量不大,并发不高,推荐选择先更新缓存再删除缓存,因为这种方案相对会简单些,所面对的场景也简单些,并发量不高,加上删除缓存失败原本就是一个小概率事件,所以对删除缓存失败的情况的容错性就更大。如果是在高并发的场景下,推荐采用先删除缓存,然后再更新数据库,原因有两个: 先更新数据库,再删除缓存:中间存在数据不一致的窗口期,高并发场景下,会放大这个缺陷。针对“读写并发”情况导致数据的不一致性:首先这种情况是一个非常小概率事件,因为读操作是一个非常快的情况,而在这个期间恰好遇到一个写操作耗时的场景,这种概率就会更小了。而且我们可以使用“延迟双删策略”、“分布式锁“等策略让这种方案近乎完美。
三种方案保证数据库与缓存的一致性
延迟双删策略 第一次删除缓存:在更新数据库之前先删除缓存中的旧数据。更新数据库延迟一定时间:等待一段时间,确保所有的旧数据读请求都已经穿透到数据库中。第二次删除缓存:再次删除缓存,保所有在数据库更新期间由于缓存失效而从数据库中读取并重新写入缓存的旧数据被清除。删除缓存重试机制
将第二次删除缓存的数据存储到消息队列中(如 Kafka),由消费者来执行删除操作。如果应用程序第二次删除缓存失败,则从消息队列中重新读取数据,再次删除缓存,这就是重试机制。但是我们需要限定一个重试的次数,如果超过这个限定次数,我们可以报个警。如果缓存删除成功,就把数据从消息队列中删除,避免重复操作。订阅 binlog 异步删除缓存
数据库更新记录成功后会产生一条变更日志记录在 ,我们可以订阅 日志,拿到具体的操作数据,然后再执行删除缓存。
扩展
缓存的三种经典更新策略
一般我们有三种使用缓存的经典模式,每种模式对应不同的数据同步策略::旁路缓存模式。它要求应用程序在读取数据时,首先要查询缓存,如果缓存中没有所需数据(缓存未命中),则从数据库中加载数据,然后将数据添加到缓存中。:这种模式与 差不多,只不过在这种模式中,应用程序与缓存之间多了一个中间层 ,这样应用程序就不直接与缓存和数据库交互了,由 完成交互。:异步写入模式,是一种先写入缓存,然后异步批量更新到数据库的策略。这种模式可以减少对数据库的写操作次数,适用于写频繁但对即时一致性要求不高的场景。
保证缓存和数据库的双写一致
数据库和缓存的双写方案一共有 4 种:先更新缓存,再更新数据库先更新数据库,再更新缓存先删除缓存,再更新数据库先更新数据库,再删除缓存
针对这四种方案,我们需要做出比较的是:更新缓存与删除缓存哪种方式更合适?应该先操作数据库还是先操作缓存?
更新缓存还是删除缓存?
这里,大明哥可以明确告诉你,我们要选择删除缓存而不是更新缓存。为什么呢?我们先看一个例子,假如线程 A 它需要将数据更新为 "",线程 B 需要将数据更新为 "",存在如下场景:线程 A 更新数据库数据为 ""线程 B 更新数据库数据为 ""由于某种原因,导致线程 B 先更新缓存数据为 "",然后,线程 A 再更新缓存数据为 ""
这个时候数据库的数据为 "",而缓存中的数据为 "",两者之间数据不一致。同样的道理,先更新缓存再更新数据库也是一样的。
如果选择删除缓存的话,则不会出现这种情况,因为你删除后,读请求会将数据库中的最新数据写入到缓存中来保证两者之间的一致性。
同时,删除缓存比更新缓存操作更加简单,尤其是更新缓存的数据需要经过独立的、复杂的计算,它少了繁琐的计算过程。当然,删除缓存相比更新缓存则是多了一次查询数据库,但是相比更新缓存的缺陷,大明哥更加推荐删除缓存。
先操作数据库还是先操作缓存?
在删除缓存和更新缓存之间我们选择了删除缓存,那么这里就只剩下:先更新数据库,再删除缓存先删除缓存,在更新数据库先更新数据库,再删除缓存
由于更新数据库和删除缓存是两个步骤,没有办法保证原子性,所以会存在更新数据库成功,但是删除缓存失败,这样就会导致缓存中的数据依然是旧数据,两者之间数据不一致。虽然可以根据缓存的过期时间来保证最终一致性,但是这个窗口期内应用程序读取到的数据都是旧数据,而且如果这个窗口期时间较长,还是会蛮麻烦的。
那怎么解决?先删除缓存,再更新数据库
与上面一样,存在删除缓存成功,但是更新数据库失败的场景。但是这种情况就没有数据不一致的场景产生。首先,更新数据库失败后,我们可以通过重试的机制来保证更新数据库成功。就算不通过重试的机制,我们也可以等待读请求的缓存未命中来读取数据库的数据然后再更新缓存的机制来保证两者的数据一致性。
但是,下面这个场景就是我们不能忽视的。先删除缓存后写数据库的这种方式,会无形中放大"读写并发"导致的数据不一致的问题,而且这种情况还是正常的场景,我们无法感知的。假如存在如下场景:线程 B 删除缓存数据线程 A 先读取缓存中数据,此时缓存未命中线程 A 读取数据库数据 ""线程 B 将数据库中的数据更新为 ""线程 A 将 "" 写入缓存中此时,数据库中的数据为 "",缓存中的数据为 "",两者不一致
所以,对于是先更新数据库还是先删除缓存,大明哥认为需要根据实际业务场景来确定。
如果,业务量不大,并发量不高,大明哥推荐选择先更新缓存再删除缓存,因为这种方案相对会简单些,所面对的场景也简单些,并发量不高,我们对删除缓存失败的情况的容错性就更大。
如果是在高并发的场景下,大明哥推荐采用先删除缓存,然后再更新数据库,原因有两个:先更新数据库,在删除缓存:中间存在数据不一致的窗口期,高并发场景下,会放大这个缺陷。针对“读写并发”情况导致数据的不一致性:首先这种情况是一个非常小概率事件,因为读操作是一个非常快的情况,而在这个期间恰好遇到一个写操作耗时的场景,这种概率就会更小了。而且我们可以使用“延迟双删策略” 让这种方案近乎完美。
保持一致性的四种方案
延迟双删策略
针对上面提到的先删除缓存,再更新数据库的方案因为读写并发请求而导致的数据不一致,我们可以使用延迟双删策略来解决。
延迟双删策略的主要目的是尽最大可能保证缓存和数据库之间的数据一致性。其关键点在于它解决了在更新数据库的瞬间到缓存再次被访问这段时间内可能会出现的数据不一致问题。具体步骤:删除缓存更新数据库延迟一段时间:这个步骤非常关键,它的目的是覆盖一些读请求在第一次删除缓存和更新数据库之间达到,从而读取数据库中的旧数据并更新进缓存再次删除缓存:第二次删除就是删除这些旧数据,从而使后续的读请求会再次从数据库中读取最新的数据并更新到缓存中,保证两者的数据一致性。 延迟双删策略最重要的就是我们需要评估延迟的时间,延迟太长则会影响系统性能,延迟太短又不足以覆盖所需要的场景,一般的评估是:读业务逻辑数据的耗时 + 几百毫秒。
删除缓存重试机制
不管是先更新数据库再删缓存方案还是延迟双删,第二步的删除都有可能会失败,如果失败了怎么处理?失败了就多试几次,保证缓存删除成功就可以了。
我们引入消息队列,将要删除的 key 假如到消息队列中,有消费者来操作缓存删除操作。如果缓存删除失败,则可以从消息队列中重新读取需要删除的数据,然后再次执行删除操作。但是,我们需要设定最大重试次数,如果超过这个次数,则需要向业务层告警。如果缓存删除成功,则需要将消息队列中的数据删除,避免重复删除。
订阅 binlog 异步删除缓存
删除缓存重试方案虽然可以解决问题,但是它对代码的入侵很重。其实我们还可以通过 MySQL 的 异步来删除缓存。
是一种记录数据库所有变更操作的日志文件,通过订阅该日志文件,我们可以获取数据库的变化情况。在该方案中,我们可以通过中间件(如 )订阅 日志,将 数据发送到 队列中,然后再通过删除缓存消费者将缓存数据删除。
分布式锁机制
其实还有一种使用分布式锁的机制,即我们利用分布式锁将更新数据库和删除缓存操作当做一个原子操作,使操作可以串行执行。比如下图: