关于缓存一致性的问题总结
对于解决这个问题我采取了两种策略,一种是超时剔除策略,一种是主动更新的策略。
1.超时剔除策略
超时剔除的策略更多的可以依赖于redis对存储的数据设置TTL过期时间,当超时之后就剔除。适用于数据变更较小的低一致性的场景。
2.主动更新策略
对于主动更新策略更多是程序员根据编码来实现数据一致性的问题,针对于对数据一致性要求较高且经常发生变更的数据。主动更新策略又分为两个场景,一种是读操作(读缓存),一种是写操作(写缓存)。
(1)读操作:
缓存命中直接返回结果,未命中查库然后更新缓存。
(2)写操作:
再说下写操作吧,对于写操作我一开始是考虑到底是更新缓存呢还是删除缓存呢?得出结论我选的是删除缓存,因为更新缓存可能会存在过多的冗余数据,对缓存利用率也不高,redis是基于内存的,内存资源是很宝贵的。
删除缓存呢也有两个方案,一个是先删除缓存,后更新数据库,一个是先更新数据库,后删除缓存。这两种方案在不同的场景下是有不同的并发问题的,但是触发的几率是不同的。
先删除缓存,后更新数据库
这种场景更容易出现数据不一致性的问题。解决方案可以采用延迟双删。(不要说到这里就往下解释延迟双删,不太合适,面试官肯定是知道的,可以告诉他我知道这个,我可以说很多,有需要的话我可以展开细说。)
比如线程1先删除了缓存,此时线程2进行了读操作来查询缓存,缓存未命中接着去查库,然后将旧的数据写入缓存,随后线程1再更新数据库,此时缓存中始终是旧的数据,而数据库中是新的数据。
延迟双删:
延迟就是等一会儿,在更新数据库之后进行sleep操作,让当前线程睡一会儿,然后再次删除缓存,为什么再删一次呢,因为这个场景下数据库里是新的数据,但是缓存里是旧的数据,若缓存一直不失效那么用户来拿到的数据一直是旧数据。
sleep睡眠时间:
为线程2读数据的时间+写入缓存的时间,因为必须要等这两个操作执行完之后我才能删掉这个脏的缓存数据。
延迟双删也只能尽可能保证一致性,极端情况下也会出现问题。
先更新数据库,再删除缓存
后者也会出现数据不一定问题,但是出现的概率太低了,条件是除非在写入缓存之前就已经完成了更新数据库+删除缓存的操作,这是几乎不可能的。因为写数据库一般会先加锁,通常是比读数据库时间更长的。
比如一开始缓存失效,线程1查询缓存未命中去查数据库,在他写入缓存前,线程2来了,执行更新数据库(涉及加锁),要想在线程1写入缓存前完成update是几乎不可能的。
因此该方案是可以保证数据一致性的。
两个操作的一致性问题(一个操作成功,一个操作失败怎么办?)
比如数据库更新成功,但是缓存删除失败。
这样讲,对此问题我有两个想法,一个是消息队列,一个是订阅数据库变更日志binlog,这俩本质其实都是异步重试的机制,在细节上是有一定区别的。
消息队列:
这样会把代码弄脏,要在代码中间加逻辑。
订阅数据库变更的binlog日志:
使用canal组件监听数据库binlog日志,一旦数据库信息发生变更,binlog就会发生变更,canal会自动投递消息到mq中,然后更新缓存。本质就是异步重试的机制来保证,更新数据库和删除缓存的操作一定同时成功或同时失败。
所以,总结一下,我最后采用的是「更新数据库+删除缓存」的操作,并配合消息队列来解决缓存一致性的问题。