MySQL与缓存一致性问题

2022-06-28 18:48:07 浏览数 (1)

数据一致性问题

“数据一致”一般指的是:缓存中有数据,缓存的数据值 = 数据库中的值。一致性又分为几种程度:

  • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

只读缓存情况

只读缓存:新增数据时,直接写入数据库;更新(修改/删除)数据时,先删除缓存。 后续,访问这些增删改的数据时,会发生缓存缺失,进而查询数据库,更新缓存。

读取数据流程:

更新数据流程:

在更新数据的流程中会有个时序问题:更新数据库与删除缓存的顺序,这里会发生数据不一致的问题

无并发情况下

先更新数据库再删除缓存:

  1. 更新数据库(成功)
  2. 删除缓存(失败)

如果第二步执行失败则会导致, 缓存中还是旧值, 数据不一致

反之如果是先删除缓存, 再更新数据库, 就第二步更新数据库失败了, 只是缓存被删除了, 读操作回源的时候还是会把数据库中的值load回缓存, 数据还是一致的

解决方案;消息队列 异步重试

高并发情况下
先删除缓存再更新数据库

两个线程同时做更新操作, 由于网络问题可能发生如下时序:

时序

线程A

线程B

T1

删除数据X的缓存

T2

读取X,缓存MISS

T3

从数据库Load X 的值到缓存

T4

更新数据库中X的值

或者:

时序

线程A

线程B

T1

删除数据X的缓存

T2

读取X,缓存MISS

T3

更新数据库中X的值

T4

从数据库Load X 的值到缓存

这种情况下会导致缓存中是旧值(线程B Load 进去的值)而数据库中是新值

解决方案: 设置缓存过期时间 延时双删, 时序如下:

时序

线程A

线程C

线程D

T5

Sleep(N)

读取到缓存旧值

T6

删除缓存数据

T7

更新数据库中X的值

缓存miss, load数据库值到缓存

先更新数据库再删除缓存
  1. 线程A先更新了数据库还没来得及删除缓存,此时线程B读取了缓存中还未来得及更新的值 时序线程A线程BT1更新数据库中数据XT2读取X,命中缓存T3删除缓存X
  2. Mysql读写分离架构下如果产生主从延迟也会导致不一致

时序

线程A

线程C

线程D

T1

更新主库X=1

T2

删除缓存

T3

缓存miss,查询从库得到从库旧值(X=0)

T4

旧值写入缓存

T5

此刻从库同步完成

解决方案:

(1) 针对读写分离的场景, 可以采取延迟消息删除缓存(这个延迟的时间要根据项目情况控制下), 另外也要控制主从延迟的时间

(2) 针对第一种情况, 其实只是小段时间的不一致, 一般业务可以接受这种情况, 保证最终一致即可。如果业务一定要强一致, 可以采取: a. 更新数据加写锁 b. 查询数据加读锁

(3) 采取数据库binlog来淘汰缓存, 这种方法成本较高

总结

优先使用“先更新数据库再删除缓存”的执行时序,原因主要有两个:

  • 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力;
  • 业务应用中读取数据库和写缓存的时间有时不好估算,进而导致延迟双删中的sleep时间不好设置。

读写缓存情况

读写缓存:增删改在缓存中进行,并采取相应的回写策略,同步数据到数据库中

  • 同步直写:使用事务,保证缓存和数据更新的原子性,并进行失败重试(如果Redis 本身出现故障,会降低服务的性能和可用性)
  • 异步回写:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库(没写回数据库前,缓存发生故障,会造成数据丢失。该策略在秒杀场中有见到过,业务层直接对缓存中的秒杀商品库存信息进行操作,一段时间后再回写数据库。
无并发情况

执行顺序性

潜在问题

结果

解决策略

先更新缓存,后更新数据库

更新缓存成功,更新数据库失败

数据库中为旧值

消息队列 重试

先更新数据库,后更新缓存

更新数据库成功,更新缓存失败

请求命中缓存,读取缓存旧值

消息队列 重试机制;订阅Binlog日志

高并发情况
写 读并发
  • 先更新数据库,再更新缓存 1.线程A先更新数据库 2.线程B读取数据,命中缓存,读取到旧值 3.线程A更新缓存成功,后续的读请求会命中缓存得到最新值 A 更新了数据库, 还没来得及更新缓存, 这个时候B Load 了缓存, 导致缓存是旧值
  • 先更新缓存,再更新数据库 1.线程A先更新缓存成功 2.线程B读取数据,此时线程B命中缓存,读取到最新值后返回 3.线程A更新数据库成功 这种场景下,虽然线程A还未更新完数据库,数据库会与缓存存在短暂不一致,但在这之前进来的读请求都能直接命中缓存,获取到最新值对业务影响较小

解决方案: 保存请求对缓存的读取记录,延时消息比较,发现不一致后,做业务补偿

写 写并发
  • 先更新数据库,再更新缓存

1.线程A和线程B同时更新同一条数据 2.更新数据库的顺序是先A后B 3.更新缓存时顺序是先B后A 这种场景下会导致缓存更新覆盖,缓存中其实是旧值(A的值, 应该是后更新数据库中B的值)

解决方案: 对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,保证同一时间只有一个线程去更新数据库和缓存;没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。

0 人点赞