Redis缓存一致性解决方案
背景
使用缓存,涉及数据库和缓存两部分数据的维护,既然是两个组件的数据,那么必然有数据一致性问题。
缓存利用率
我们的内存是稀缺资源,在业务数据比较少的时候我们可以直接把所有的数据直接存进缓存中,这时候访问的时候就不需要访问数据库,但是当我们数据量非常大的时候,这时候如果把所有的数据都放进缓存中是不现实的事情,这时候我们一般会在缓存中放入热点数据
具体的实现步骤:
1.写请求只写数据库
2.读请求先读缓存,没有命中则读取数据库,并重建该缓存
3.设置缓存的过期时间,并配合缓存的过期策略和内存淘汰策略,则可以提高缓存利用率
这样一来,缓存中放置的数据内容一般都是热点数据,即使有冷数据被访问到,也会在过期策略和内存淘汰策略下,最后达到优化的目的
数据一致性
数据库和缓存属于不同的数据存储方式,两者之间必然会出现数据不一致的问题,并且这样的问题是无法百分之百从根源上消除它
无法消除的原因:
1.两者数据更新时间差之间出现的问题(加锁)
2.更新成功与更新失败(消息队列,订阅数据库变更日志)
所以我们只能对其进行优化,因此出现很多不同的优化方式。
解决方案
方式一:更新数据库,删除缓存(推荐)
实现思路
Case:
1.修改信息前:数据库与缓存中用数据都保持1
2.用户请求,将值修改为2
3.进行step1,更新数据库1->2,结果为true
4.进行step2,删除缓存,结果为true
5.请求访问,缓存中数据没有,访问数据库,更新缓存为2
问题延伸
Question:
1.step1成功,但是step2失败,导致缓存删除失效,用户访问时还是之前的老的数据,需要等到key过期才能进行更新,用户视角:修改了,没生效,过一段时间又莫名其妙变成自己改的数据了。
2.并发问题,A、B双方同时操作,此时,数据库中是新值,缓存中是旧值,又会等到缓存过期才会更新到新数据,但是这样的概率是非常非常低的,并且一般在写操作时,会加锁,一般不会考虑到这个问题。
Question1的解决方案:异步重试机制
重试机制目前有两套比较好的解决方案
问题解决
方案一:消息队列
删除缓存不直接删除,采用消息队列的方式进行缓存删除,由消费者来操作删除,消息队列正好满足我们现在的场景
- 失败重试机制,下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的需求)
- 消息可靠性,写到队列中的消息,成功消费之前不会丢失
方案二:订阅数据库变更日志,操作缓存
拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。
订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:无需考虑写消息队列失败情况,只要写 MySQL 成功,Binlog 肯定会有,并且这样的方式不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用
自动投递到下游队列:canal 自动把数据库变更日志「投递」给下游的消息队列
当然,与此同时,我们需要投入精力去维护 canal 的高可用和稳定性。
方式二:删除缓存,更新数据库
此方式并不推荐使用,这里只做简单介绍
优点:
1.能够实现缓存一致性,也做了缓存删除,实时性也较高
2.代码第一步就删缓存,如果这个时候没有线程查询数据,直到更新完数据库才查询,那么会触发其它查询线程查询数据库,得到最新数据,实时性要更高一点(相比方案2)
缺点:
1.代码复杂度比较高,使用了异步做延迟二次删除缓存
2.代码第一步就删缓存,如果紧接着有其它线程查询,那么这次删除缓存意义不大,依然缓存被填充了旧数据,而且等延迟双删,还需要延迟时间后才能看到新数据
3.假设网络抖动,缓存延迟删除失败,依然和方案1一样的实时性不高,但是可以做一些 重试的补偿措施