缓存和数据库不一致性的含义:在redis缓存没有自动失效和过期的长期时间内,redis中的数据和数据库中的数据一直都是不一样的。
在更新数据库数据的这段时间,redis和数据库数据不一致,是正常情况
首先要理解下面这个流程(这是前提,如果不是这个前提,则不考虑缓存和数据库一致性问题)
不论采用哪种方式解决缓存和数据库一致性问题,我们查询数据,都是下面这个流程
(1)先查缓存,缓存查到了数据,则直接结束
(2)缓存中没有数据,去查询数据库获取数据,然后会把数据写入到缓存中,再结束
在并发情况下,这时,有一个更新数据的线程到了,还有一个查询数据的线程到了
不推荐使用修改缓存的方式,因为删除缓存的性能更高,成本更低。
我们先看前面几种方案
(1)采用方案:先更新数据库,再删缓存
初始:缓存中没有数据 x ,只有数据库中有
查询数据线程先到,到第3步,线程因为其他原因阻塞了
(2)采用方案:先删缓存,再更新数据库
初始:缓存和数据库中都有 x = 1
更新数据线程先到
以上两种方式,都有可能导致数据不一致性问题。
方案:先删缓存,再更新数据库
导致缓存和数据库数据不一致性问题的主要原因是:
更新数据库线程 删除缓存后,被另一个查询数据线程 把旧的数据 重新缓存到了 redis中
我们如果等 查询数据线程 把旧的数据 重新缓存到了 redis中(即上图的第7步)完成后, 再删除缓存,就可以了,不过由于不知道 上图的第7步 多久会完成,所以需要延迟一段时间,这个过程也不会很久。
延迟的目的 ,就是为了等 查询数据线程 把旧的数据 重新缓存到了 redis中 完成。
如果不延迟,就直接二次删除缓存, 可能 第7步还没有开始(即查询数据线程 还没开始 把旧的数据 缓存到了 redis中)。这样我们第二次删除,啥也没删到。双删的动作都做完了,第7步又开始了。
特殊情况:如果 第7步 的开始时间,大于 延迟双删的时间,仍然会造成数据不一致性
采用了延迟双删,只会在延迟删除的这一小段时间里数据不一致性。
那能不能保证这一小段时间里,数据也是一致的呢?那就是数据的强一致性,这是CP。
如果要保证强一致性,就要保证redis和数据库的这两步操作是原子性的,这就需要加一把锁。而加锁就会降低系统的吞吐量。
思考我们使用redis是为了什么?使用redis就是为了提高系统的吞吐量的,所以加锁来满足CP强一致性是不可取的。
强一致性(CP)和高性能(高可用性AP)两者只能保证一种
所以延迟双删是可以保证最终的一致性的,即AP。
我们不会采用强一致性,因为会影响系统的吞吐量。
延迟双删的延迟时间如何设定的呢?一般是设置几百毫秒
比如说延迟500ms,那在这500ms以内,那redis中就一直是脏数据,这个没有办法,我们只能保证最终的数据一致性
如何设置一个合理的值?我们需要自己去评估查询数据的线程的耗时,即从数据库读取数据,再写入缓存,这个时间为多少。
延迟的时间大于这个值即可,防止查询数据的线程覆盖掉新的数据
这里在先删缓存,再修改数据库,同时延迟双删的情况下,还是有可能删除缓存失败。
(注意:这里的删除缓存失败,是指删除了一个原本就不存在的key,即读取数据的线程太慢了,延迟双删,删除了一个空key。而不是指redis出现故障的那种删除失败)
如果删除失败了,就异步发送需要删除的key到MQ中,启动消费者线程去再次删除。
一旦消费者线程监听到redis中某个key删除失败的消息,那么就执行重试删除。
但是这样线程1还需要发送一条删除失败的消息到mq中,虽然消费者线程的代码不在这里,但是删除失败的消息还是只能线程1去发送。
这样修改数据库数据的线程1,代码就和mq的代码耦合在一起了。
如果要实现解耦的话?怎么操作呢?
可以使用另一个组件canal,通过它可以去监听mysql的binlog日志。当mysql中有数据变动时,canal(可以理解成canal是mysql的从节点)能够立刻感知到,然后mysql的从节点canal会立即通知canal的客户端
前面是因为线程1去执行的延迟双删操作,所以只有线程1知道延迟双删是否成功,所以需要线程1去发送延迟双删失败的消息给mq,这才造成了线程1代码耦合。
我们使用canal去监听mysql的binlog日志,就可以让canal客户端去执行延迟双删的操作,如果canal删除失败了,由canal去发送需要删除的key到MQ中,并由MQ的消费者去执行删除重试的操作。这样就避免了需要线程1去发送延迟双删失败的消息给mq了。
把延迟双删(第二次删除redis)的操作的交给canal客户端,并把删除失败时,异步发送需要删除的key到MQ中中这个任务交给canal(这个canal客户端可以是一个springboot应用)。(第一次删除redis仍然由修改数据线程,即线程1执行 --> 先删缓存,再修改数据库。)
整个流程如下:
所以更推荐使用先修改数据库,再删缓存的方式,来保持数据库和缓存的数据一致性。不用计较延迟双删的延迟时间。即便在 先修改数据库,再删缓存的方式中,删除了空值,也会由canal(只要数据库中的值修改了,就去删除redis)进行二次删除,如果canal失败了,也会由MQ的消息订阅者去重试删除。