Redis缓存一致性问题解决方案

在开发中经常使用到redis。redis作为缓存可以加快程序响应速度。从redis缓存中读取数据大致需要0.5ms左右,从数据库需要几毫秒。大致业务流程如下图:
Redis缓存一致性问题解决方案_第1张图片

每次设置缓存时都有一个过期时间,根据不同业务,过期时间也不一致,设置过期时间能保证缓存数据最终一致性问题。这样能保证在更新数据库成功,更新缓存失败,或者缓存了脏数据时,缓存过期后,能正确的读取到最新的值。

我们常见的三种缓存更新方案:

  1. 先更新数据库,再更新缓存
  2. 先删除缓存,再更新数据库
  3. 先更新数据库,再删除缓存

方案一先更新数据库,再更新缓存
先更新数据库,再更新缓存,这么做的基本上很少很少。这种方案有以下缺点:

  1. 并发更新问题,比如线程A更新了数据库,线程B更新了数据库,线程B更新了缓存,线程A更新了缓存,这样最终存入的就是脏数据。
  2. 业务维护难度大,比如有些更新操作多,但是读取时并不多,可能浪费更新到redis的资源,另外redis缓存的数据并不一定是直接写入数据库的,可能是经过刷选,过滤,复杂计算得出的,这个时候维护麻烦,每次写入数据库,都得更新缓存,重复计算,刷选。并且不一定是更新一张表的数据要更新缓存,可能缓存跟多张表的数据有关系。

这里实际使用最多的是方案二和方案三

方案二先删除缓存,再更新数据库
这种方案在我们实际中使用较多,大部分都能容忍可能出现的脏数据的业务,及时出现脏数据,缓存过期后,也会读取最新的值。说下这种方案存在的问题

  1. 存在脏数据的可能,比如线程A删除缓存,线程B查询缓存不存在数据,从数据库获取,获取成功后,数据存入缓存,现在A更新数据。这样缓存中的数据就是脏数据了。

脏数据解决方案采用双删

       # 删除缓存
        redisConn.delete("cacheKey")
        # 更新数据库
        db.execute("update t set count = count +1 where id = 10")
        # 延时删除缓存
        sleep(1000)
        redisConn.delete("cacheKey")

这种方案有以下缺点:

  1. 多次操作redis删除key
  2. 延时删除,导致接口性能不高,影响接口吞吐量
  3. 第二次可能删除失败,还是存在问题

解决方案,异步删除时可以使用MQ消息队列(比如RocketMq的延时消息),确保删除成功,删除失败则重试,这种方案对业务代码影响大,造成大量的侵入,并且MQ也可能存在消息堆积,删除延迟过长的问题。

方案三先更新数据库,再删除缓存

我司(核心接口每天请求量几千万级别,集群百万QPS)目前采用的是这种方案,先更新数据库,再删除缓存。这种方案虽然也会出现脏数据,但是概率极低,而且redis也有过期时间,能够保证最终一致性。

        # 更新数据库
        db.execute("update t set count = count +1 where id = 10")
        # 删除缓存
        redisConn.delete("cacheKey")

存在的问题

  1. 请求A查询数据库,得一个旧值,请求B将新值写入数据库,请求B删除缓存,请求A将查到的旧值写入缓存。这种情况下会存在脏数据。

出现这种问题的概率极低,除非是查询比写入慢。要解决也可以采用异步延时删除。说实话如果对于这种极低概率的脏数据都不能容忍,建议不需要使用缓存了。毕竟现在大部分都是读写分离,主从还存在延时呢。这种要强一致性的建议走mysql。对msql进行扩容比如分库分表,读写分离等等。

当然非得使用缓存又要保存数据强一致性,也有办法。采用消息队列异步删除,采用binlog同步缓存数据,删除缓存,不过这种方案代码侵入大,维护难,大部分都采用方案三。

缓存强一致性方案流程如下:
Redis缓存一致性问题解决方案_第2张图片

参考资料:redis缓存和数据库双写一致性问题

你可能感兴趣的:(Redis)