为了账号安全,请及时绑定邮箱和手机立即绑定

MySQL和redis数据一致性问题

大纲

图片描述

为什么引入缓存

在系统中引入缓存的目的:提升性能,缓解MySQL的压力。
例如:当业务刚开始,处于起步阶段,请求量很小,这时直接操作数据库即可。但是当业务增加,请求量越来越大时,如果每次请求
还是直接操作数据库,那么数据库的压力就会随着请求量的增大而变大,出现性能问题。这个时候常用的解决办法就是引入缓存。

因为缓存在内存中,而内存时又是无法感知数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。

处理方法:

  1. MySQL中的数据全部写入缓存;
  2. 写请求只是更新MySQL,不更新缓存;
  3. 定时任务去刷新缓存

图片描述

这个方法所有读请求都可以直接命中缓存,不需要再查数据库,性能非常高。但是,因为是定时去刷新缓存,缓存和数据库会存在不一致问题(取决于定时任务的执行频率)。

一致性

一致性就是数据保持一致,又分为:强一致性、弱一致性、最终一致性。

  • 强一致性: 系统写入什么,读取到的也会是什么,对系统的性能影响比较大

  • 弱一致性:约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态

  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这个模型是使用比较普遍或者说是比较受推崇的一种一致性模型。

缓存模式

一般都是如何使用缓存的?有三种经典的缓存模式:

  • Cache-Aside Pattern
  • Read-Through/Write through
  • Write behind

Cache-Aside Pattern:旁路缓存模式

旁路缓存模式是为了尽可能的解决缓存和数据库的一致性问题提出的。

Cache-Aside Pattern的读写流程如下:

图片描述

Read-Through/Write-Through:读写穿透

Read/Write Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。

Read-Through的流程其实和旁路缓存模式的读请求流程很像,就是多了一层Cache-Provider,实际只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。

流程如下图:

图片描述

Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新。

流程如下图:

图片描述

Write behind:异步缓存写入

Write behind和Read-Through/Write-Through都是由Cache Provider来负责缓存和数据库的读写,但又有不同:Read/Write Through是同步更新缓存和数据的,而Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

流程如下图:

图片描述

这种模式,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。

更新缓存还是删除缓存

如何解决缓存和数据库的一致性问题,下面列出了4种策略:

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

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

  • 先更新数据库,后删除缓存

  • 先删除缓存,后更新数据库

这4种策略中,对于数据库而言,不管先后顺序,最终都是更新。而对于缓存来说则有两种情况,一是更新缓存,二是删除缓存。

那么操作缓存时,到底是更新缓存,还是删除呢?

首先第一点,更新缓存对于删除缓存来说,有2个明显的缺点:

  1. 缓存中保存的值,是需要经过一系列计算得出的值,更新的评率高的话,就有点浪费性能了。
  2. 对于读少写多的场景而言,可能更多的是数据还没读取到,就已经又被更新了,这也有些浪费性能了。

其次,对于一般的场景,我们使用的缓存模式就是Cache-Aside模式,而旁路缓存模式,在写请求的时就是删除缓存而不是更新缓存。

举例说明
例如下面这种情况(并发引起的一致性问题):

图片描述

  1. A先发起一个写操作,第一步先更新数据库;
  2. B再发起一个写操作,第二步更新了数据库;
  3. 由于网络等原因,B先更新了缓存;
  4. A更新缓存。

这时缓存中保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致,就出现了脏数据。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

通常的解决方案就是:加锁。
两个请求同时修改同一条数据,每个请求在改之前,先去获取锁,拿到锁才允许更新数据库和缓存,拿不到锁就返回失败等待下次重试。
这种方案也会遇到性能浪费的情况(第一点的2个缺点)。

所以对于先更新数据库,后更新缓存先更新缓存,后更新数据库 这2种策略来说,很少使用。

先操作数据库还是缓存

删除缓存对应的方案:先更新数据库,后删除缓存和*先删除缓存,后更新数据库

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

图片描述

同时两个请求,请求A更新操作和请求B查询操作

  1. 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作
  2. 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中
  3. 但是此时请求 A 并没有更新成功,或者事务还未提交

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

这种策略也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

所以在并发的场景下,不管是谁先谁后,是更新缓存还是删除缓存,只要存在异常,两步操作只要其中一步失败,都会出现数据不一致的情况。

如何确保两步都执行成功?

对于以上问题如何解决呢?第一种是缓存延时双删;第二种是重试。

缓存延时双删(针对第四种策略)

图片描述

  1. 先删除缓存;
  2. 再更新数据库;
  3. 延迟一会,再次删除缓存。

但是对于更新数据库之后在删除缓存来说,如果使用的是MySQL主从(读写分离),那么主从同步之间也会有时间差。

图片描述

  1. 请求 A 更新操作,删除了 Redis
  2. 请求主库进行更新操作,主库与从库进行同步数据的操作
  3. 请求B 查询操作,发现Redis中没有数据去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据

那么此时解决办法就是如果是对Redis进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

此外还有一个重要的问题,延迟一会是延迟多久?

  1. 延迟时间要大于「主从复制」的延迟时间
  2. 延迟时间要大于请求B 读取数据库 + 写入缓存的时间

但如果是在分布式和高并发场景下,延迟时间实际很难估算。所以只能是凭借经验估算延迟时间,尽可能的降低不一致出现的概率。
综合来看,保证先删除缓存,再更新数据库这种策略下数据一致性的解决方案也是尽可能的保证数据一致性。实际使用中要尽可能
的降低主从同步的延迟时间,降低出现问题的概率。

重试(针对第三种策略)

对于先更新数据库,后删除缓存这种策略出现数据不一致的解决方案就是使用消息队列进行删除的重试。

图片描述

  1. 请求 A 先对数据库进行更新操作
  2. 在对Redis进行删除操作的时候发现报错,删除失败
  3. 此时将Redis的key作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对Redis进行删除操作

但是这种方案耦合度比较高。还一种优化方案:读取binlog日志异步删除缓存。Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

图片描述

总结

  1. 一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性无法做到都满足要求。
    前面的例子中给出的加锁的方法,虽然能解决问题,但我们也要付出相应的代价,甚至很可能会超过引入缓存带来的性能提升。
    所以,既然决定使用缓存,就必须容忍一致性问题,我们只能尽可能地去降低问题出现的概率。
    ​同时我们也要知道,缓存都是有失效时间的,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。

  2. 每种方案各有利弊,无论使用哪种方案都不能百分百保证数据一致,都只是尽可能的降低出问题的概率。引入缓存的最根本目的是缓存数据库的压力,
    提高性能。实际场景中,根据自身业务评估去选择,因为没有最好的,只有最适合的。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消