分布式系统:数据一致性解决方案

程序源代码

共 5037字,需浏览 11分钟

 · 2020-12-17

点击上方蓝色字体,选择“设为星标

回复”资源“获取更多资源

大数据技术与架构
点击右侧关注,大数据开发领域最强公众号!

大数据真好玩
点击右侧关注,大数据真好玩!



在分布式系统中,随着系统架构演进,原来的原子性操作会随着系统拆分而无法保障原子性从而产生一致性问题,但业务实际又需要保障一致性,下面我从学习和实战运用总结一下分布式一致性解决方案。

1. CAP & Base理论



CAP定理指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。这三个要素最多只能同时实现两点,不可能三者兼顾:

  • 一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。

  • 分区容错性:可靠性,无论应用程序或系统发生错误,还是用户以意外或错误的方式使用,软件系统都能继续运行。

  • 可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。





CAP理论3选2是伪命题,实际上必须从A和C选择一个和P组合,更进一步基本上都会选择A,相比一致性,系统一旦不可用或不可靠都可能会造成整个站点崩溃,所以一般都会选择AP。但是不一致的问题也不能忽略,使用最终一致是比较好的办法。



BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:

  • 基本可用(Basically Available:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。注意,这绝不等价于系统不可用。比如:

    • 响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒

    • 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

  • 软状态(Soft State:软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。

  • 最终一致(Eventually Consistent:最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。



最终一致的核心做法就是通过记录对应操作并在操作失败时不断进行重试直到成功为止。

2. 重试

在出现一致性问题时如果系统的并发或不一致情况较少,可以先使用重试来解决。



在调用Service B超时或失败时进行重试:

  • 同步调用,捕获异常重新调用Service B。

  • 异步消息,捕获异常发送延迟消息重新调用Service B。

  • 异步线程,捕获异常开启异步线程重新调用Service B。



如果重试还是不能解决问题,那么需要使用分布式事务来解决。

3. 分布式事务

对于分布式一致性问题可以采用分布式事务来解决。

3.1 2PC-XA协议

XA事务由一个或多个资源管理器(Resource Managers)、一个事务管理器(Transaction Manager)以及一个应用程序(Application Program)组成。

  • 资源管理器(RM):参与者。提供访问事务资源的方法。通常一个数据库就是一个资源管理器。

  • 事务管理器(TM):协调者。分配标识符,监视事务的进度,并负责事务完成和故障恢复。

  • 应用程序(AP):发起者。定义事务的边界,制定全局事务中的操作。



整个过程分为2个阶段:准备(prepare)和提交(commit),prepare前需要先执行对应的DML操作。





Mysql XA事务语句:


注:执行前需要先生成全局唯一的xid
XA {START|BEGIN} xid [JOIN|RESUME]DML操作XA END xid [SUSPEND [FOR MIGRATE]]XA PREPARE xid //1.准备XA COMMIT xid [ONE PHASE] //2.提交XA ROLLBACK xidXA RECOVER

简单的事例:


//准备前阶段mysql> XA START 'xatest';Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO mytable (i) VALUES(10);Query OK, 1 row affected (0.04 sec)
mysql> XA END 'xatest';Query OK, 0 rows affected (0.00 sec)//准备mysql> XA PREPARE 'xatest';Query OK, 0 rows affected (0.00 sec)//提交mysql> XA COMMIT 'xatest';Query OK, 0 rows affected (0.00 sec)

XA看起来很美好,但还是存在一些问题:

  • commit丢失/超时导致数据不一致。A commit成功,B commit时网络超时导致数据库未收到commit请求。

  • 性能低。所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态。

  • 主备数据不一致。

  • 协调者单点故障,在任意阶段协调者发生故障都会导致分布式事务无法进行。



3.2 3PC

3PC通过在参与者加入超时机制解决2PC中的协调者单点带来的事务无法进行的问题,但是性能和一致性仍没有解决。



3.3 TCC



这样看下来强一致是很难做了,还是最终一致把。。。



TCC是通过最终一致来达到分布式事务的效果,即:在短时间内无法保证一致性,但最终会一致。核心分为2个阶段:1.Try  2.Confirm or Cancel。先尝试(Try)操作数据,如果都成功则全部确认(Confirm)该修改,如果有任意一个尝试失败,则全部取消(Cancel)。简单点说就是增加了中间态和可回改能力





通过重试机制保障Confirm和Cancel一定能成功。TCC解决了性能问题,但是业务系统想要实现对业务代码的侵入性很大,可以学习一下阿里GTS是如何做的。



3.4 事务管理器

在实际运用中事务管理器(TM)一般是由应用程序(AP)兼职实现的,这样对业务代码侵入性大,最好是把TM做成中间层。接下来详细看一下TM的职责:协调者。分配标识符,监视事务的进度,并负责事务完成和故障恢复。TM需要具备持久化能力才能完成监控、故障恢复和协调,整个协调过程可以分为3个阶段:

  • 准备阶段:向TM注册全局事务并获取到全局唯一XID,这一步需要定义好事务的范围。

  • 执行阶段:应用程序AP执行业务功能操作自己的RM,全部执行完毕后向TM commit,如果无失败则整个事务成功。

  • 确认/回滚阶段:如果第2步出现异常会触发该阶段,TM咨询各个AP对应操作是否成功,如果成功则commit,如果失败则调用AP进行rollback。



下面简单介绍几种实现方式。

3.4.1 本地事务管理器

对于简单的业务可能只要保障2个数据库之间的一致,这样在本地实现事务管理器比较快成本也不高。





  1. 1. 在做业务逻辑之前把对应事件添加到本地event表中(记录订正时所需要的关键数据)

  2. 2. 执行业务逻辑

  3. 1.本地业务逻辑,操作表数据等等

  4. 2.RPC调用其他服务

  5. 3.修改event状态为确认

  6. 4. 如果第2步不成功,则event状态还是预提交,通过定时任务捞取再执行订正逻辑

  7. 5. 检查业务逻辑中的操作是否完成,如未完成则执行订正逻辑

  8. 1.本地业务逻辑是否执行成功

  9. 2.RPC调用其他服务是否执行成功

  10. 6.返回订正结果

  11. 1.成功则修改event为确认提交

  12. 2.未成功则不操作,等待轮询订正

3.4.2 外部事务管理器

阿里GTS:https://help.aliyun.com/document_detail/157850.html

3.5 DB和MQ之间的一致性



在实际场景中,业务系统对本地DB数据变更后会广播对应的消息,消费者消费消息做自己的业务逻辑,按正常逻辑消息会在数据库变更后发出,如果消息发送超时且失败那么DB和MQ之间就产生了不一致问题,如何解决呢?使用可靠消息来解决,核心逻辑保证消息从投递到消费的过程中不会丢失:生产者confirm、消费者ack和持久化。理论上只要使用生产者确认机制即可,但是不使用消费者ack则没有意义。消费者需要开启手动ack同时做到幂等消费,MQ需要通过将exchange、queue和message进行持久化来保证消息不丢失,生产者则需要通过确认机制保证消息一定投递到MQ种接下来重点讲解一下生产者确认机制。



confirm机制是在消息投递到所有匹配的queue之后发送确认消息给生产者,这样生产者就知道消息投递成功,但是由于消息是在DB操作之后发出的,生产者必须增加记录表来记录消息投递状态,如果投递成功就在收到确认消息时把记录标记为投递成功,如果长时间未收到确认消息则大概率是消息丢失了,再定时重新投递,这样就可以保证消息最终一定能投递成功。核心逻辑其实就是通过全局事务ID来标识DB操作和MQ消息是在一个分布式事务中的



疑点:在学习RabbitMQ confirm机制时发现生产者接收到的确认消息只有消息ID,这个消息ID不能作为全局事务ID,所以无法解决DB和MQ之间的一致性问题,后续再看看其它MQ产品是怎么做的。



上面的方式需要业务系统维护消息状态,这部分可以交给中间件来实现,实现逻辑会变得不一样。





预投递的消息不会分发到queue中,只有在接收到确认投递的请求后才会进行投递,如果确认操作因为网络异常失败了,MQ在过一段时间之后主动询问业务系统该消息是否可投递(失败不断重试),这样就能在异常时做到最终一致,不过依赖MQ产品的能力。

3.6 DB和缓存之间的一致性

DB和缓存之间同样也存在不一致问题,先写DB再写缓存如果缓存写失败就不一致了,同样的需要重试更新缓存来做最终一致。方法可以参考“2.重试”部分,如果一定要保证重试不丢失可以用可靠消息或本地task表来记录重试操作,有条件的可以使用DB DRC消息对业务的侵入会小一些。需要注意的是同步更新和异步更新同时使用时可能会产生更新覆盖的问题,加上毫秒级的时间戳或版本号来丢弃旧的更新。

4. 兜底核对

虽然有了分布式事务,但是在实际场景中可能会因为bug导致数据不一致,这时需要兜底来做最后一道防线,通过定时核对数据是否一致,如不一致手动/自动进行订正。

4.1 系统自核对

如果系统数量和数据量不多的情况下可以由业务系统自行核对,通过发送延迟消息自消费或监听其他系统消息做相关数据核对并进行订正。



4.2 搭建核对系统

在核对工作繁多的情况下,由业务系统自己核对会存在很多耦合,这时可以选择搭建独立的核对系统进行核对和订正。



1.监听drc消息





2.监听业务消息



参考:

  • 阿里GTS:https://help.aliyun.com/document_detail/157850.html?spm=a2c4g.11186623.6.554.47ae4df4Zy6hBI

  • 饿了么DRC:https://zhuanlan.zhihu.com/p/34958596

  • Mysql XA:https://dev.mysql.com/doc/refman/8.0/en/xa-statements.html

版权声明:

本文为大数据技术与架构整理,原作者独家授权。未经原作者允许转载追究侵权责任。
编辑|冷眼丶
微信公众号|import_bigdata


欢迎点赞+收藏+转发朋友圈素质三连


文章不错?点个【在看】吧! ?

浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报