Redis缓存脏读的三种解决方案对比
“ 我是小羊同学,一个兢兢业业的程序员”
1、背景
在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接去访问DB,这样能充分保护数据库而不至于被高并发直接击垮。
这个场景的解决方案之一就是利用缓存机制来保证请求不会直接打到DB,整体流程如下:
在一般情况下,这种设计是没有问题的,但是如果在高并发下一旦涉及到数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)的数据一致性问题。
2、方案
常见的解决方案无非以下三种:
先更新数据库,再更新缓存
先删除缓存,再更新数据库
先更新数据库,再删除缓存
3、问题
方案一:先更新数据库,再更新缓存
先更新数据库,再更新缓存,这么做的基本上很少很少。这种方案有以下缺点:
并发更新问题,比如线程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同步缓存数据,删除缓存,不过这种方案代码侵入大,维护难,大部分都采用方案三。
推荐阅读:
end
*版权声明:转载文章和图片均来自公开网络,版权归作者本人所有,推送文章除非无法确认,我们都会注明作者和来源。如果出处有误或侵犯到原作者权益,请与我们联系删除或授权事宜。