概述
在开发中经常使用到redis。redis作为缓存可以加快程序响应速度。从redis缓存中读取数据大致需要0.5ms左右,从数据库需要几毫秒。大致业务流程如下图:
每次设置缓存时都有一个过期时间,根据不同业务,过期时间也不一致,设置过期时间能保证缓存数据最终一致性问题。这样能保证在更新数据库成功,更新缓存失败,或者缓存了脏数据时,缓存过期后,能正确的读取到最新的值。
我们常见的三种缓存更新方案:
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
方案一先更新数据库,再更新缓存
先更新数据库,再更新缓存,这么做的基本上很少很少。这种方案有以下缺点:
- 并发更新问题,比如线程A更新了数据库,线程B更新了数据库,线程B更新了缓存,线程A更新了缓存,这样最终存入的就是脏数据。
- 业务维护难度大,比如有些更新操作多,但是读取时并不多,可能浪费更新到redis的资源,另外redis缓存的数据并不一定是直接写入数据库的,可能是经过刷选,过滤,复杂计算得出的,这个时候维护麻烦,每次写入数据库,都得更新缓存,重复计算,刷选。并且不一定是更新一张表的数据要更新缓存,可能缓存跟多张表的数据有关系。
这里实际使用最多的是方案二和方案三
方案二先删除缓存,再更新数据库
这种方案在我们实际中使用较多,大部分都能容忍可能出现的脏数据的业务,及时出现脏数据,缓存过期后,也会读取最新的值。说下这种方案存在的问题
- 存在脏数据的可能,比如线程A删除缓存,线程B查询缓存不存在数据,从数据库获取,获取成功后,数据存入缓存,现在A更新数据。这样缓存中的数据就是脏数据了。
脏数据解决方案采用双删
# 删除缓存
redisConn.delete("cacheKey")
# 更新数据库
db.execute("update t set count = count +1 where id = 10")
# 延时删除缓存
sleep(1000)
redisConn.delete("cacheKey")
这种方案有以下缺点:
- 多次操作redis删除key
- 延时删除,导致接口性能不高,影响接口吞吐量
- 第二次可能删除失败,还是存在问题
解决方案,异步删除时可以使用MQ消息队列(比如RocketMq的延时消息),确保删除成功,删除失败则重试,这种方案对业务代码影响大,造成大量的侵入,并且MQ也可能存在消息堆积,删除延迟过长的问题。
方案三先更新数据库,再删除缓存
我司(核心接口每天请求量几千万级别,集群百万QPS)目前采用的是这种方案,先更新数据库,再删除缓存。这种方案虽然也会出现脏数据,但是概率极低,而且redis也有过期时间,能够保证最终一致性。
# 更新数据库
db.execute("update t set count = count +1 where id = 10")
# 删除缓存
redisConn.delete("cacheKey")
存在的问题
- 请求A查询数据库,得一个旧值,请求B将新值写入数据库,请求B删除缓存,请求A将查到的旧值写入缓存。这种情况下会存在脏数据。
出现这种问题的概率极低,除非是查询比写入慢。要解决也可以采用异步延时删除。说实话如果对于这种极低概率的脏数据都不能容忍,建议不需要使用缓存了。毕竟现在大部分都是读写分离,主从还存在延时呢。这种要强一致性的建议走mysql。对msql进行扩容比如分库分表,读写分离等等。
当然非得使用缓存又要保存数据强一致性,也有办法。采用消息队列异步删除,采用binlog同步缓存数据,删除缓存,不过这种方案代码侵入大,维护难,大部分都采用方案三。
缓存强一致性方案流程如下:
参考资料:redis缓存和数据库双写一致性问题
最后
以上就是无语棒棒糖为你收集整理的Redis缓存一致性问题解决方案的全部内容,希望文章能够帮你解决Redis缓存一致性问题解决方案所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复