Redis专题——缓存一致性
本文主要叙述缓存一致性的问题以及解决方案。
1
缓存一致性
01 什么是缓存一致性
就是缓存和数据库的数据不一致导致的问题,缓存一致性分为强一致性和最终一致性。强一致性,这个比较损耗性能,比较复杂,加入之后,可能会比没加缓存更慢。最终一致性,是允许缓存数据和数据库数据一段时间内不一致,但数据最终会保持一致。这个性能较高。
02 为什么要保证缓存一致性
因为业务中存在一些写操作导致的,是要先写缓存,还是先写数据库。二者顺序的不同会导致不同的问题。单纯的读操作,是不会导致缓存一致性问题的,因为读是幂等的哈。读无数次都是不会变的,因此就不存在读操作引起缓存一致性问题。但往深一层细究,归根结底都是业务需要,如果业务需求允许缓存和数据库的不一致,那就不需要保证缓存一致性了。
03 如何保证缓存一致性(解决方案)
相信很多人都知道经典方案:cache aside pattern。
首先明确的是,读不会产生缓存一致性问题。是写操作,才会产生缓存一致性问题。
第一点,失效:请求过来时,先访问缓存,缓存不存在,再去访问数据库,更新缓存。
第二点,读:请求过来时,缓存中有数据,直接返回数据。
第三点,写:先更新数据库,后删除缓存。
关键在第三点,前提,数据库肯定是更新的。剩下的问题就是:是要更新缓存?还是要删除缓存?是先对数据库操作?还是先对缓存操作?
俩俩组合有4中可能性:
1、先更新缓存,后更新数据库
2、先更新数据库,后更新缓存
3、先删除缓存,后更新数据库
4、先更新数据库,后删除缓存
先更新缓存,后更新数据库
首先我们要明白,更新数据库或者更新缓存,都面临着更新失败的风险。但在互联网高并发的环境中和根据墨菲定律,这个事是一定会发生的。
1、先更新缓存,成功了
2、后更新数据库,失败了,当然你会说重试,好,那我就重试N次,但如果数据库彻底挂了,恢复不了了,重试也没用
导致问题:数据丢失,数据库里面的数据还是老数据
先更新数据库,后更新缓存
假设有两个请求,A请求是更新,B请求是更新,A先B后,但二者间隔很短
1、线程A更新了数据库
2、线程B更新了数据库
3、线程B更新了缓存
4、线程A更新了缓存
导致问题:缓存中是旧数据,数据库中是新数据,这就不一致了。还有就是更新后的缓存,真的会被再读取吗?如果缓存数据不再被读取,那就白白操作了一次缓存更新操作。并且还占用内存空间。
根据这个例子,可以看出,更新缓存是不可取的,那就直接删除缓存吧。接着看
先删除缓存,后更新数据库
假设有两个请求,A请求是更新,B请求是读,可能出现的问题
1、线程A删除缓存
2、线程B查询不到缓存,直接去数据库查旧值
3、线程A将新值写入数据库
4、线程B更新缓存
导致问题:缓存中的是旧值,数据库中的是新值,二者不一致。进一步,如果数据库存在读写分离,那么缓存和数据库数据不一致的情况进一步加剧。
1、线程A删除缓存
2、线程A将新值写入主数据库,但未同步数据到从数据库
3、线程B查询不到缓存,直接去从数据库查,查到旧值
4、线程B更新缓存
5、新数据同步到从数据库
导致问题:缓存是旧值,数据库是新值,二者数据不一致
先更新数据库,后删除缓存
假设有两个请求,A请求是读,B请求是更新,可能会出现的问题
1、缓存刚好失效
2、线程A查数据库,得到旧值
3、线程B更新数据库
4、线程B删除缓存
5、线程A更新缓存
导致问题:缓存是旧值,数据库是新值,二者不一致。但这种情况的可能性相对来说比较小,因为需要缓存刚好失效,并且此时有一个线程去读,且刚好又有一个写的线程。而且写的线程理论上是比读的线程慢的,因为写的线程,需要加锁。而查询不用加锁,不包括复杂的查询。
在数据库读写分离的情况下,这种情况会更加明显:
1、线程B更新主库
2、线程B删除缓存
3、线程A查询缓存,没有命中,查询从库得到旧值
4、数据同步到从库
5、线程A更新缓存
导致问题:缓存数据和数据库数据不一致
如果考虑更新数据库或者更新缓存失败的话,那么更新数据库失败的话,其实数据库和缓存都是旧数据,因此不存在数据不一致的情况。如果更新缓存失败,那么有过期时间来保证最终一致性。如果非要较真的话,可以加入重试机制。重试机制可以用线程池,也可以用MQ。MQ更加可靠。可以直接订阅MySQL的binlog,来触发缓存的删除。当然,其实MQ也会挂。但是MQ和缓存都一起挂的几率,应该很小吧。
综上所述四种情况
虽然每种方案都有各自的问题,但出现几率较小的是“先更新数据库,后删除缓存”方案。为什么先更新数据库?因为数据库的持久化能力比缓存好。上述四种情况,还可能出现缓存并发,缓存穿透,缓存雪崩的问题。这些问题,这里就不讨论了。感兴趣的话,自己去看我的相关文章。
04 如何做到强一致性
方案一:分布式事务
可以用分布式事务,分布式事务,具体的实现有2PC、3PC、消息队列等。如果要采用这个方案,架构设计中要引入很多容错、回退、兜底的措施。业务代码就增加复杂性了。还有人说用分布式一致性算法paxos和raft,这就更复杂了。
方案二:分布式读写锁
首先,我们回到“先更新数据库,后删除缓存” ,要明白什么时候会出现脏数据?出现脏数据:更新数据库后,删除缓存之前。这时候二者数据是不一致的。如果实现更新数据库时,所有读请求都被阻塞。这就解决了数据不一致的问题,这其实是串行化思路。但后果,当然是性能下滑。
总结:其实选择数据的强一致性和数据的最终一致性。得看具体需求,我好像说了一句废话。但是放弃强一致性,意味着我们系统的性能得到一定程度的提升。相反,如果我们追求强一致性,那就会巨复杂,而且可能得不偿失,可能性能比不加缓存时还低。缓存这东西,想要用得好,就需要好好琢磨,比如过期时间的设置,持久化,故障恢复,空间和时间的平衡,一致性的选择,这些都要好好斟酌,没有最好的方案,只有合适的方案。