前面的文章我们已经介绍过了缓存的三种异常(缓存击穿、缓存穿透和缓存雪崩)以及如何解决。这篇文章我们来讲解一下缓存一致性问题。
什么是缓存一致性问题?
一般情况下,Redis是用作应用程序和数据库之间读操作的缓存,主要目的是减少数据库的IO,还可以提升数据的IO性能。
当应用程序要去读取一个数据时,首先会尝试从Redis中读取,如果命中了就直接返回结果,如果没有命中,就从数据库中查询,查询到数据后再把这个数据缓存到Redis中并返回结果。
在这样的架构中,会出现一个问题,就是一份数据,同时保存在数据库和Redis中,当数据发生变化的时候,需要同时更新Redis和MySql,由于更新是有先后顺序的,这种两边写入的情况下,并不能像单纯数据库操作一样,可以满足ACID的特性。因此,就可能存在一方更新成功而另一方更新失败的情况,从而出现缓存和数据库中数据不一致的问题,也就是缓存一致性问题。
如何解决?
我们先将出现缓存一致性问题的场景进行分类,分别是先删除缓存,再更新数据库 和先更新数据库,再删除缓存两种场景。
先删除缓存,再更新数据库
先删除缓存,此时数据库还没有及时更新成功,此时如果有另一个线程来读取缓存中的值,由于缓存被删除,这条线程就会去数据库中进行读取,但是数据库此时还没有更新,所以读取到的还是旧值,缓存不一致问题就发生了。

解决方法------延时双删
延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再Sleep一段时间,然后再次删除缓存,具体的流程如下:

(1)线程1删除缓存,然后去更新数据库;
(2)线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取数据,这时候由于数据库还没有及时更新,所以读取到的还是旧值,然后把旧值写入了缓存;
(3)线程1,根据估算的时间来进行Sleep操作,Sleep醒来后,线程1再次删除缓存;
(4)如果此时还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值。
存在的问题
如何取Sleep的时间其实很难进行衡量,如果时间取的过短,很有可能数据库还没有更新的时候双删操作就完成了,缓存就会一直保持旧值;如果时间取的过长,就会导致更新后的缓存又被删除了,造成重复写入缓存。
先更新数据库,再删除缓存
如果将上面的场景反过来,这个产生的问题会更加明显,就是更新数据库的操作成功了,但是缓存删除操作失败了,或者没有来得及进行缓存删除操作,这个时候其他线程去访问缓存,拿到的还是原来的旧值,缓存一致性问题出现了。

解决方案------消息队列
先更新数据库,成功之后往消息队列中发送消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现缓存的删除,最终达到数据的一致性。

存在的问题
(1)引入消息中间件之后,问题更加复杂了,怎么保证消息不丢失更加麻烦;
(2)就算不考虑消息丢失的问题,由于缓存的删除不是立即进行的,所以不能保证缓存一致性的高时效性,只能保证最终的缓存一致性。
进阶版的消息队列解决方案
为了解决缓存一致性的问题单独引入一个消息队列,太复杂了。其实,一般的大公司本身都会有监听binlong消息的消息队列存在,主要是为了做一些核对的工作。
可以借助监听binlog消息的消息队列来做删除缓存的操作。这样做的好处是,不用引入一个新的消息队列到业务代码中,同时保证了高可用。

当然问题还是上面提到的问题,不过在并发量不是特别高的场景下,这种做法的实时性和一致性都还算可以接受的。
为什么是删除缓存而不是更新缓存?
就以先更新数据库,再删除缓存为例,如果不是删除缓存而是更新缓存的话,如果数据库在1个小时内更新了2000次,那么相对的缓存也要跟着更新2000次,但是在这1个小时内,缓存只被访问到了1次,那么这2000次的更新操作和1次的删除操作相比,显然是删除操作的性能更高一些。换句话说,缓存不需要实时更新,只有在用到缓存的时候在去数据库中读取新数据即可。
这篇文章我们讲解了Redis的缓存一致性问题和解决方案,大家有什么问题或者勘误的话可以在评论区留言,笔者看到都会回复的。