数据一致性的一些思考

标签: 数据 一致性 思考 | 发表时间:2021-04-15 11:23 | 作者:HappyTeemo
出处:https://juejin.cn/tag/%E6%9E%B6%E6%9E%84

结论

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

  • 业务量有多少,需要主从读写分离么,需要分库分表么?
  • 是读多还是写多?
  • 是要最终一致性,还是强一致性?
  • 对缓存一致性的要求是多少?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上,这是完全是可接受的

  1. 复杂度是比较高的

  2. 吞吐量大大降低。

总结

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

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

参考

blog.kido.site/2018/12/01/… www.cnblogs.com/rjzheng/p/9… blog.kido.site/2018/12/09/… zhuanlan.zhihu.com/p/59167071

相关 [数据 一致性 思考] 推荐:

数据一致性的一些思考

- - 掘金 架构
没有银弹,需要根据自己的业务场景做取舍. 业务量有多少,需要主从读写分离么,需要分库分表么. 是需要多表合并,还是多行合并,还是多库合并. 该如何容灾?更新、删除缓存失败你能不能接受. 如果删除缓存失败,你还允不允许更新数据库. 要根据实际业务场景来定制方案. 大部分业务场景都是读多写少,而且数据库(mysql)写很少看到写挂的,都是读有瓶颈.

微服务下的数据一致性思考

- -
之前讲到了数据库层和缓存层的改造思路,而对于业务层的改造,采用了集中式服务转微服务的架构方案. 既然是微服务,就意味着面临大量的服务间的内部调用及服务依赖,这就意味着,如果一次请求的调用涉及到两个或多个微服务之间的调用,恰好有下游的微服务调用失败,我们就必须要考虑到回滚及服务间保证数据一致性的问题.

大数据的一致性

- - 阳振坤的博客
看到了一篇关于数据一致性的文章:下一代NoSQL:最终一致性的末日. (  http://www.csdn.net/article/2013-11-07/2817420 ),其中说到: 相比关系型数据库,NoSQL解决方案提供了shared-nothing、容错和可扩展的分布式架构等特性,同时也放弃了关系型数据库的强数据一致性和隔离性,美其名曰:“最终一致性”.

COMMIT和数据一致性

- - Oracle - 数据库 - ITeye博客
[align=justify; direction: ltr; unicode-bidi: embed; vertical-align: baseline;]2.在执行一条update语句后一直未提交,数据会写到数据文件中吗. 一致性查询及一致性读原理. 如果8点钟可以查询出两条记录,假设一下,如果此查询很慢,从8点开.

数据库与缓存数据一致性解决方案

- - 掘金 后端
在分布式并发系统中,数据库与缓存数据一致性是一项富有挑战性的技术难点. 本文将讨论数据库与缓存数据一致性问题,并提供通用的解决方案. 假设有完善的工业级分布式事务解决方案,那么数据库与缓存数据一致性便迎刃而解,实际上,目前分布式事务不成熟. 在数据库与缓存数据一致解决方式中,有各种声音. 先操作数据库后缓存还是先缓存后数据库.

关于分布式系统的数据一致性问题

- - 互联网 - ITeye博客
现在先抛出问题,假设有一个主数据中心在北京M,然后有成都A,上海B两个地方数据中心,现在的问题是,假设成都上海各自的数据中心有记录变更,需要先同步到主数据中心,主数据中心更新完成之后,在把最新的数据分发到上海,成都的地方数据中心A,地方数据中心更新数据,保持和主数据中心一致性(数据库结构完全一致).

分布式系统数据一致性的6种方案(转)

- - 企业架构 - ITeye博客
编者按:本文由「高可用架构后花园」群讨论整理而成,后花园是一个面向架构师的增值服务,如需了解,请关注「高可用架构」后回复 VIP.                                                                                 问题的起源.

如何保证缓存与数据库的双写一致性?

- -
如何保证缓存与数据库的双写一致性. 你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题. 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去.

对数据库架构的再思考

- - 人月神话的BLOG
前面在谈PaaS的时候曾经谈到过共享数据库,私有数据库的问题,在这里再谈谈在多业务系统建设过程中的数据架构模式问题. 首先来看下传统的数据交换解决方案如下图:. 业务场景为单独构建的四个业务系统,在四个业务系统中SID数据为需要跨四个应用交互和共享的数据. 传统的做法则是对四个应用存在的SID库数据进行数据集成和交换,则后续的每一个业务系统中都有全部的共享基础数据,任何一个应用的SID库数据需要通过数据交换和集成同步四份.