缓存和DB的一致性

  • Post author:
  • Post category:其他




如何保证缓存和DB的一致性问题


简单的提几个问题

  1. 一定要保证缓存和DB一致性么?
  2. 缓存一定要设置超时时间么?
  3. 更新数据,先更新缓存还是DB?
  4. 查询数据,先查缓存,没有就去DB查,然后就写入缓存就ok了?



什么时候需要使用缓存?


  1. 减少DB压力(高可用)


    为什么可以提高可用性?

    不知道你有没有想过,有一天DB挂了(压力大 或者 被其他业务/程序打垮了)缓存还能保你一命;不过也取决于你缓存多久,DB修复其实也很快,DBA就是干这个事情的,肯定有很多技术保障DB快速恢复的(扩容、限流),如果你缓存1h,平均下来如果半个小时能修复,就不会太影响你的业务

  2. 减少RT(高性能)

针对第一个问题:一定要保证缓存和DB一致性?

我的回答是:其实没必要一定保证缓存和DB的一致性,还是取决于你的业务是什么样的,大多数的时候保证最终一致性就可以了



什么时候清理缓存?


如果你使用的是不过期的缓存

针对第二个问题:缓存一定要设置超时时间?

我的回答是:不一定需要清理。

还是看具体业务;如果你缓存的东西容量可控,那么就不需要清理,比如:中国城市名词、全球国家二字码、地址库id…

不过大多数的业务还是需要设置超时时间的,不过期的缓存,需要保证修改的时候及时修改缓存,而且有内存溢出风险



定时清理缓存

现在java提供很多工具包都支持设置key的超时时间,没必要你自己去写一个定时清理的缓存。我经常用的是Guava cache。

如果你要去写(可能你的业务特殊,需要定时部分清理),一般就是两种思路

  1. 写入key的时候注册回调函数,到时间调用回调函数去清理
  2. 启动一个定时器,定时去扫描缓存



DB数据有修改

针对第三个问题:更新数据,先更新缓存还是DB?

我的回答是:都可以

有部分人肯定会不加思考的就说,

先更新db啊,不然数据有可能不一致啊!


这个上面我说过了,数据不一定要保证一致性。

案例:实际做一个业务的时候,为了更新“分拣码(不用理解)”,我就选择了先更新缓存,再去更新DB,原因主要有4点:

  1. 实时性要求低:新数据和老数据都能用,实时性要求不高,只是说用新数据对于实操会有所提高
  2. 生效时间短:每次更新数据量太大了,最长的可能超过半小时,那么我觉得提前半个小时提高业务操作效率也算划得来
  3. 失败率低:更新数据比较简单,没有其他逻辑,就是简单更新一个db的数据,所以失败的概率也比较低
  4. 恢复简单:就算失败了,我只需要重新消费一次数据,或者重新推一份数据就好了(操作简单)

当然了,一般还是先更新DB,我这种场景比较特殊,不过我想表达的是,作为一个程序员思想不能太固话了,一点都不知道变通,没有什么事情是绝对的坏



今天的重点 – 如何正确使用缓存

接下来说的都是对一致性要求比较高的系统,需要

先更新DB再更新缓存

针对第四个问题:查询数据,

先查缓存,没有就去DB查,然后就写入缓存就ok了?

我的回答是:不ok


思考一个问题:更新缓存失败了怎么办?


凉凉了兄弟,DB数据和缓存不一致了,如果一个业务没有用缓存,那么两个业务查出来的结果不一致了。不过一般不会失败,更新缓存失败这里主要指的是分布式的缓存,网络延迟还是有可能导致缓存失败的



方案1(先删除缓存,再去修改DB)

思考一下,是不是先删除缓存,再去修改DB就好了?

在这里插入图片描述

并没有完美解决
在这里插入图片描述

线程2抢先更新了数据,这样还是会导致数据不一致


注意:这里第3步和第4步还有可能先后不一样,查询后还得去检查缓存是否是否已存在



方案2(队列+锁)

在这里插入图片描述

这种方案确实解决了问题,但是会导致另一个问题:

修改的时候,不能读

,这对于高性能的系统来说是不可接受的;上面还有一个小优化,叫延时双删,就是更新完DB,不要更新缓存,而是去删除缓存。

这里有个优化,把队列变成修改缓存的队列,如果队列里面有值,那么就不更新缓存了(和下面第5种方案比较类似)



方案3(DB行锁)

在延时双删的条件下,数据库加锁,查询的时候for update行锁不就好了

不过存在和方案2一样的问题



方案4(半事务)

数据库操作和缓存操作加一个事务?

其实我说的事务是“业务事务”,和数据库的概念一样,主要就是保存之前的快照,异常时执行回滚操作。

更新DB -> 更新缓存 -> 出现异常(如果value很大,那么网络有可能抖动而导致失败) -> 回滚(value大回滚原来的value可能还是失败,所以把value改成空会更好)

其实做到这里已经很完美了,不过还有一种情况是业务事务没办法解决的:应用挂了的问题(没机会执行回滚);比如我刚好更新完DB,应用挂了,数据还是会不一致

当然了你可以说我可以做本地事务,应用启动的时候可以保证回滚回来;但是注意不一致已经产生了



方案5(自研)

如果还有人钻牛角尖,有没有方案可以解决方案4的问题?

在这里插入图片描述

想一下,

更新缓存的时候到底要不要提前删除缓存?


不提前删除缓存是一定会导致数据不一致的(特殊场景),但是提前删除缓存还是可能导致数据不一致;我的思路就是用锁+提前删除缓存来解决一致性问题,拿不到锁就走DB

所以应用挂了,锁还在,那就走DB呗,等到锁没有了,会自动更新缓存,

缓存更新失败还是会一直走db,所以db压力会很大

优点:解决了一致性问题,无论在什么情况下面,都保证了比较强的数据强一致性

缺点:1.如果是分布式系统,锁会变得很重(分布式锁) 2.可能导致缓存击穿



方案6

有个工具canal,可以订阅mysql的bin.log,从而修改db之后马上可以更新到redis。这样的话,相当于canal做了一个缓存和db一致性的方案,而且这种方案也并不能像方案5一样保证实时的强一致性。不过对大部分场景来说应该都是够用的。



总结

没有完美的方案

复杂度低 + 强一致性:方案3(性能差)

复杂度低 + 高性能:方案4(弱一致性)

复杂度高 + 强一致性:方案5(击穿问题)

其实一般没有那么高的一致性要求,而且缓存更新失败概率太低了,通常情况下不会有方案12345;只是在网上看到这样的问题,顺便思考了一下,觉得还挺有意思,有些问题不能细想,一细想问题很多



版权声明:本文为PROGRAM_anywhere原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。