如何保障MySQL和redis缓存的一致性

本文分析了数据库和Redis缓存一致性问题,讨论了读写场景中的解决方案,并比较了淘汰缓存与更新缓存策略的优劣。重点关注了数据更新顺序问题,提出延时双删和异步更新两种应对策略,以及在先写后读策略中可能产生的短暂数据不一致及其解决办法。
摘要由CSDN通过智能技术生成

一、问题分析

    众所周知,数据库是把数据存储在磁盘上,访问时需要进行IO操作,在请求量小的情况下,耗时还比较低。但随着数据量的增大,访问量的集中,整个数据库负担加重,响应就会变慢,请求延时上升。进而导致用户侧的等待时间变长,很大程度上影响了用户体验。

    为了解决这一问题,在整个请求链路上就引入了缓存的策略,将数据库中的数据copy一份到redis缓存(毕竟可是号称单机抗10w qps)。由于redis的数据是直接存在内存中,因此读写数据速度提升,整体请求响应速度也得到了提高。

    但是这样一来,数据就存在了两个地方:缓存redis和数据库db中,不可避免出现redis和db中数据的一致性问题。

二、针对读场景

  1. 如果redis中存在用户所需数据,直接返回即可;如果不存在,则试图从db中读取数据,读取完成后,将该数据更新到redis中,避免下次再次查询或其他用户查询时,仍从db读取。
  2. 在高并发场景下,两次请求同时请求redis,发现数据并不存在后,都会从db中加载数据,然后再重新将数据写入redis,无论两次请求的写操作谁先谁后,数据本身是一样的,只不过额外进行了一次数据写覆盖。因此不会产生数据不一致的问题。
    在这里插入图片描述

三、针对写场景

  1. 如果redis中本身不存在缓存数据,则直接修改db中的数据即可,不会产生数据不一致问题。
  2. 如果redis中已存在缓存数据,则需要同时修改db和redis中的数据,但是二者修改操作的执行必然存在先后顺序。在高并发的场景下,就有可能产生数据不一致的情况。

四、疑问?

  1. 由于redis中的数据可有可无,那么当数据发生变化时,是对redis中的数据进行修改,还是直接删除对应的redis,然后通过后续的读请求再回源db,将数据重新写入redis呢?
  2. redis和db的数据写操作的顺序问题,是先更新redis,还是先更新db?

五、问题1:缓存数据淘汰 或者 更新

方案1: 淘汰缓存策略

  • 优点:操作简单,直接将对应缓存删除即可。
  • 缺点:由于缓存被删除后,下次的读请求无法命中缓存,需回源db,将数据重新写入redis。

方案2: 更新缓存策略

  • 优点:缓存命中率高,只要缓存进行了更新,后续的读请求不会出现缓存未命中(cache miss)的情况。
  • 缺点:在某些业务场景下,缓存更新的成本过大。且更新后的缓存不一定会被使用。

综合分析

其实业界一般采用的都是缓存淘汰策略,而非缓存更新策略。原因有三:

  1. 大多数情况下,redis缓存中的数据并不是完全copy
    db中的数据,而是将db中多张表的数据进行了重新计算,筛选后更新到redis。如果在db某一张表的数据发生了变化的情况下,需要同步重新计算redis中的值,成本过高。
  2. 缓存更新后的新值,无法保证一定会有读请求命中,如果一直没有请求命中该部分冷数据,其实是产生了一定的资源浪费(计算成本+存储成本)。
  3. 相较于淘汰缓存策略中,仅有一次读请求cache miss的结果来说,淘汰缓存策略的缺点完全可以容忍。

问题2:redis和db写操作,谁先谁后?

注意:以下的方案讨论都是基于redis淘汰缓存操作以及数据库更新操作保证成功(可通过重试机制解决)的情况下,高并发的业务场景中的解决方案。

方案1: 先淘汰缓存,后更新数据库

正常情况

  • A请求进行写操作,先淘汰缓存,再更新数据库。
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。

异常情况1

  • A请求进行写操作,先淘汰缓存。
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。注意,此时redis中被更新的依然是老数据,A请求的数据库更新操作尚未完成。
  • A请求进行数据库更新操作。此时,数据库中是新数据,redis缓存中是老数据,产生了数据不一致的问题。且该不一致会一直持续到缓存自然失效或者下次的更新操作。

提供两种解决思路

异步更新缓存

  1. A请求进行写操作,先淘汰缓存。
  2. B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据。注意,此时不向redis写入新的缓存策略。
  3. A请求通过订阅数据库binlog,对redis缓存数据进行异步更新。
  4. 该方案虽然解决了数据不一致的问题,但是在数据库更新操作完成前,所有的读请求都会直接打到数据库上,具有比较大的风险。

延时双删

  1. A请求进行写操作,先淘汰缓存。
  2. B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。注意,此时redis中被更新的依然是老数据,A请求的数据库更新操作尚未完成。假设该步骤耗时N秒。
  3. A请求进行数据库更新操作。
  4. 由于此时redis中写入了老数据,因此A请求在休眠M秒后(M略大于N),再次对redis进行淘汰缓存操作。
  5. 该方案虽然解决了数据不一致的问题,但是由于请求A在更新完数据库之后,需要休眠M秒再次淘汰缓存,一定程度上影响了数据更新操作的吞吐量。可以尝试将等待M秒更新redis的操作放到另一个单独的线程(比如消息队列+重试机制),可以有效缓解吞吐量降低的问题。

异常情况2

  • A请求进行读操作,此时redis缓存中没有数据,因此直接从数据库中读取数据。
  • B请求进行写操作,先淘汰缓存,再更新数据库。
  • A请求进行将从数据库中读到的老数据,更新到redis。此时产生数据不一致问题。
  • 该种异常情况发生概率极低,一般读操作比写操作要快。如有担心,可以采用上述的延时删除策略。

方案2: 先更新数据库,后更新淘汰缓存

正常情况

  • A请求进行写操作,先更新数据库,再淘汰缓存。
  • B请求进行读操作,由于A请求已将缓存淘汰,B请求没有在redis中发现所需数据,因此从数据库中读取数据,并更新缓存到redis中。

异常情况1

  • A请求进行写操作,先更新数据库。
  • B请求进行读操作,由于A请求尚未淘汰缓存,B请求在redis中发现所需数据,因此直接返回老数据,产生了数据不一致的问题。
  • A请求淘汰缓存。
  • C请求进行读操作,发现redis中没有数据,因此从数据库中读取新数据,并更新至缓存。数据不一致的问题解决。
  • 该场景下,数据最终一致,只是在高并发下产生了一小段时间的数据不一致。

异常情况2

  • A请求进行读操作,此时redis缓存中没有数据,因此直接从数据库中读取数据。
  • B请求进行写操作,更新数据库,并将redis中缓存进行了淘汰(虽然此时redis中并没有任何的缓存)。
  • A请求将从数据库中读到的老数据,更新到redis。此时产生数据不一致问题。
  • 该种异常情况发生概率极低,一般读操作比写操作要快。如有担心,可以采用上述的延时删除策略。

总结

  • 方案1:先淘汰缓存,后更新数据库的策略,有可能导致长时间的数据不一致问题,可以通过延时双删 or 异步更新缓存策略进行解决。
  • 方案2:先更新数据库,后更新缓存,有可能导致极短时间内的数据不一致,但是数据最终是一致的。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值