数据一致性的一些思考

结论

没有银弹,需要根据自己的业务场景做取舍。

  • 业务量有多少,需要主从读写分离么,需要分库分表么?
  • 是读多还是写多?
  • 是要最终一致性,还是强一致性?
  • 对缓存一致性的要求是多少?1分钟?一秒钟?
  • 查询结构是怎样的。是需要多表合并,还是多行合并,还是多库合并?
  • 该如何容灾?更新、删除缓存失败你能不能接受?写数据库失败怎么办?
  • 如果删除缓存失败,你还允不允许更新数据库?

要根据实际业务场景来定制方案。

DB主从一致

大部分业务场景都是读多写少,而且数据库(mysql)写很少看到写挂的,都是读有瓶颈。
所以主从读写分离就出现了,写的时候写主库,然后同步到从库,读的时候就读从库。

那么,很明显从库不是最新数据,就会有不一致的情况。

为了解决主从数据库读取旧数据的问题,常用的方案有四种:

(1)半同步复制
(2)强制读主
(3)数据库中间件
(4)缓存记录写key

解决方案1:半同步复制

不一致是因为写完成后,主从同步有一个时间差,假设是500ms,这个时间差有读请求落到从库上产生的。有没有办法做到,等主从同步完成之后,主库上的写请求再返回呢?

答案是肯定的,就是大家常说的“半同步复制”semi-sync:
(1)系统先对DB-master进行了一个写操作,写主库
(2)等主从同步完成,写主库的请求才返回
(3)读从库,读到最新的数据(如果读请求先完成,写请求后完成,读取到的是“当时”最新的数据)

方案优点:利用数据库原生功能,比较简单
方案缺点:主库的写请求时延会增长,吞吐量会降低

解决方案2:强制读主库

如果不使用“增加从库”的方式来增加提升系统的读性能,完全可以读写都落到主库,这样就不会出现不一致了:

方案优点:“一致性”上不需要进行系统改造
方案缺点:只能通过cache来提升系统的读性能。

解决方案3:数据库中间件

如果有了数据库中间件,所有的数据库请求都走中间件,这个主从不一致的问题可以这么解决:

(1)所有的读写都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库
(2)记录所有路由到写库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库
(3)经验主从同步时间过完后,对应key的读请求继续路由到从库

方案优点:能保证绝对一致
方案缺点:数据库中间件的成本比较高

解决方案4:缓存记录写key法

既然数据库中间件的成本比较高,有没有更低成本的方案来记录某一个库的某一个key上发生了写请求呢?很容易想到使用缓存,当写请求发生的时候:

(1)将某个库上的某个key要发生写操作,记录在cache里,并设置“经验主从同步时间”的cache超时时间,例如500ms
(2)修改数据库

而读请求发生的时候:

(1)先到cache里查看,对应库的对应key有没有相关数据
(2)如果cache hit,有相关数据,说明这个key上刚发生过写操作,此时需要将请求路由到主库读最新的数据
(3)如果cache miss,说明这个key上近期没有发生过写操作,此时将请求路由到从库,继续读写分离

方案优点:相对数据库中间件,成本较低
方案缺点:为了保证“一致性”,引入了一个cache组件,并且读写数据库时都多了一步cache操作

数据库和缓存 最终一致性方案

数据库和缓存由于是异步更新,所以必然会有不一致的情况,要根据自己的业务场景去抉择。

根本原因:

操作数据库慢,操作缓存快。

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

第一种:先更新数据库,再更新缓存

并发问题

(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
在这里插入图片描述

解决:序列化达到最终一致

序列化,用一个消息队列顺序刷新缓存。会达到最终一致性
在这里插入图片描述

优缺点

好处:

  • 不存在并发写库的问题。
  • 写流程容灾分析
    • 写1.1 DEL缓存失败:没关系,后面会覆盖
    • 写1.4 写MQ失败:没关系,Databus或Canal都会重试
    • 消费MQ的:1.5 || 1.6 失败:没关系,重新消费即可
  • 读流程容灾分析
    • 读2.3 异步写MQ失败:没关系,缓存为空,是OK的,下次还读库就好了

缺点:

  • 会有一点延迟
  • 会有ABA的问题,比如关注–取关–关注,操作的人再刷新可能还是未关注(缓存停留在第二步)。

第二种:先删缓存,再更新数据库

在这里插入图片描述

并发问题

(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求A将新值写入缓存
(5)请求B将旧值写入缓存(覆盖了)

在这里插入图片描述

解决 :延时双删

在上面的基础上加上一个延时,再更新一次。
(6)延时一段时间,请求A将新值写入缓存(再次覆盖)

优缺点
  • 优点
    • 实现简单
    • 通常不会有错误数据(删缓存很少有问题,而更新缓存肯定涉及到逻辑,可能会有bug)
    • 异步刷新,补缺补漏 (如果删缓存有问题,后面会有更新缓存顶上)
  • 缺点
    • 如果删和更新都失败,这个脏数据会停留比较长时间。(缓存设置过期时间不就行了?)
    • 并发问题难以完美解决。

第三种:先更新数据库,再删缓存

由于数据库比缓存慢,所以更新完数据库再更新缓存,不一致的时间的较短的。

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

在这里插入图片描述

并发问题

(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存

在这里插入图片描述
这种情况需要满足:

  • 缓存刚好失效的时候有人写库。
  • 写库比读库快。
解决

首先,给缓存设有效时间是一种方案。
其次,采用异步延时删除策略,保证读请求完成以后,再进行删除操作也可以。

第四种:先更新缓存,再更新数据库

应该没人会选择吧。

更新数据库失败了,脏缓存咋办。。。

对于删缓存失败的解决方案

方案一:

如下图所示

在这里插入图片描述

流程如下所示

(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功 然而,该方案有一个缺点,对业务线代码造成大量的侵入。

于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二:

流程如下图所示:
在这里插入图片描述

(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。

数据库和缓存强一致性方案

在这里插入图片描述
强一致性,包含两种含义:

缓存和DB数据一致
缓存中没有数据(或者说:不会去读缓存中的老版本数据)

首先我们来分析一下,既然已经实现了“最终一致性”,那它和“强一致性”的区别是什么呢?

没错,就是“时间差”,所以:

“最终一致性方案” + “时间差” = “强一致性方案”

那我们的工作呢,就是加上时间差,
实现方式:我们加一个缓存,将近期被修改的数据进行标记锁定。
读的时候,标记锁定的数据强行走DB,没锁定的数据,先走缓存

优缺点

容灾完善

写流程容灾分析

  • 写1.1 标记失败:没关系,放弃整个更新操作
  • 写1.3 DEL缓存失败:没关系,后面会覆盖
  • 写1.5 写MQ失败:没关系,Databus或Canal都会重试
  • 消费MQ的:1.6 || 1.7 失败:没关系,重新消费即可

读流程容灾分析

  • 读2.1 读Cache_0失败:没关系,直接读主库
  • 读2.3 异步写MQ失败:没关系,缓存为空,是OK的,下次还读库就好了

无并发问题

这个方案让“读库 + 刷缓存”的操作串行化,这就不存在老数据覆盖新数据的并发问题了

缺点剖析

  1. 增加Cache_0强依赖
    这个其实有点没办法,你要强一致性,必然要牺牲一些的。
    但是呢,你这个可以吧Cache_0设计成多机器多分片,这样的话,即使部分分片挂了,也只有小部分流量透过Cache直接打到DB上,这是完全是可接受的

  2. 复杂度是比较高的

  3. 吞吐量大大降低。

总结

常规最终一致性解决方案:

  • 读先读缓存,缓存不在,读库。
  • 写库写队列,或者订阅biglog,然后回刷缓存。

参考

https://blog.kido.site/2018/12/01/db-and-cache-01/
https://www.cnblogs.com/rjzheng/p/9041659.html
https://blog.kido.site/2018/12/09/db-and-cache-04/
https://zhuanlan.zhihu.com/p/59167071

相关阅读:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的提千万

江山父老能容我,不使人间造孽钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值