Redis MySQL 一致性知识

Redis 和 MySQL 是一个老生常谈的问题了,如何理解不同的解决方案的区别,互有什么优劣?

更新操作有以下几种方式:

1、先更新数据库后更新缓存: 并发情况下,新请求进行的缓存更新操作可能被就请求的更新操作覆盖。导致 redis 中一直是脏数据

2、先删除缓存后更新数据库 读写并发时可能会出现数据不一致的情况。更新请求删除缓存之后,还没来得及更新数据库。另外一个读取请求发现缓存为空,将数据库中的旧值更新到redis 中,导致数据库中保持新值,redis中保存旧值

3、先更新数据库后删除缓存:

被使用最多的一种方式。但是理论上还是可能出现数据不一致的情况。读请求缓存没有命中,从数据库中读取到旧值。同时,一个写请求在更新数据库,写入一个新值,之后将缓存中的旧值删除了。之后读请求将从数据库中得到的旧值写入到了数据库。导致数据库中保存了新值,缓存中保存了旧值。 必须满足三个条件:

  1. 在读写请求到来之前缓存恰好刚刚失效
  2. 读写并发请求
  3. 更新数据库的时间+删除缓存的时间 < 读数据库+写缓存的时间 但是实际来考虑一下,一般来说更新数据库的时间是要大于读数据库的时间的,因为更新数据库的时候存在加锁等操作,因此条件三是不容易出现的。

因此在实际情况下一般都是采用第三种方案。假如使用第三种方案,还是有一些问题,因为删除缓存可能会失败。这个时候就需要重试机制。但是失败之后直接再次重试很大概率还是会失败,导致这个线程一直被占用。应该考虑异步重试,异步重试是指将重试请求写到消息队列中,使用专门的消费者来处理重试操作。消息队列保证消息可靠,成功消费之前不会丢失。但是还需要权衡使用消息队列的成本:

  1. 写队列失败:操作缓存和写消息队列同时失败的概率很小
  2. 项目中一般都会使用到消息队列,维护成本并不会增加太多

除了使用消息队列处理删除缓存失败的方案,还有一种更加简单的方案。订阅数据库binlog,在操作缓存。也就是说在业务代码中不需要显示的操作缓存,只需要修改数据库。关于订阅数据库binlog其实有很多种开源中间件,使用这种方案的优点在于:

  1. 不需要考虑消息队列投递失败的情况
  2. 自动同步到下游队列,使用中间件可以减少我们的维护成本

所以最终的最佳方式就是「先更新数据库后删除缓存,再配合消息队列或者订阅数据库变更日志」

但是这种方式在某些场景下还是会出现问题。在读写分离+主从同步复制延迟的情况下会出现问题。一个写请求更新了主库的值,之后删除了缓存。另外一个读请求从从库中查询缓存,没有命中,从从库中读取到旧值。由于主库被更新了,之后从库进行主从同步。但是读请求会将获取到的旧值写入到缓存中。现在的情况就是数据库中的是新值,缓存中的数据是旧值。这个时候就需要使用延迟删除策略。延迟删除策略是指,在主库的更新操作之后,先休眠一会儿在删除缓存,也可以在消息队列中写一条延时消息,消费者在一段时间之后删除缓存。这些方案的目的是为了把缓存清除掉,这样一来,下次读取缓存时,缓存不命中,会从数据库中读取。

还需要考虑延时时间需要设为多长,这个是很难评估的,只能尽可能得减少主从复制的时间,减小出问题的概率。

缓存和数据库一致性问题,看这篇就够了 (qq.com)