从放弃到入门-分布式事务

219 阅读14分钟

为什么需要分布式事务

业务系统的复杂度提升,数据量的增加,导致出现分布式事务。
随着互联网、金融等行业的快速发展,业务越来越复杂,一个完整的业务往往需要调用多个子业务或服务,随着业务的不断增多,涉及的服务以及数据也越来越多、越来越复杂。传统的系统难以支撑,出现了应用和数据库等的分布式系统。分布式系统又带来了数据一致性的问题,从而产生了分布式事务。

什么叫分布式事务

分布式条件下,多个节点操作的整体事务一致性。
特别是在微服务场景下,业务 A 和业务 B 关联,事务 A 成功,事务 B 失败,由于跨系统,就会导致不被感知。此时从整体来看,数据是不一致的。
此时,业务系统只能拿到不完全的 A 服务数据,缺失 B 服务的数据。

如何实现分布式下的一致性

典型情况下是两个思路:

  • 理想状态:直接想单机数据库事务一样,多个数据库自动通过某种协调机制,实现了跨数据库节点的一致性。 使用场景:要求严格的一致性,比如金融交易类业务。
  • 一般情况:可以容忍一段时间的数据不一致,最终通过超时终止,调度补偿,等等方式,实现数据的最终状态一致性。
    使用场景:准实时或非实时的处理,比如 T+1 的各类操作,或者电商类操作。
  1. 强一致 : XA
  2. 弱一致 :
    2.1 不用事务,业务侧补偿冲正
    2.2 所谓的柔性事务,使用一套事务框架保证最终一致的事务

XA 分布式事务协议

基于第一个强一致的思路,就有了基于数据库本身支持的协议,XA 分布式事务。
XA 整体设计思路可以概括为,如何在现有事务模型上微调扩展,实现分布式事务。

  • 应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作。
  • 资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式
  • 事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。

XA协议存在的问题

  • 同步阻塞问题 全局事务内部包含了多个独立的事务分支,这一组事务分支要不都成功,要不都失败。各个事务分支的ACID特性共同构成了全局事务的ACID特性。也就是将单个事务分支的支持的ACID特性提升了一个层次到分布式事务的范畴。即使在非分布式事务中,即本地事务,如果对操作读很敏感,我们也需要将事务隔离级别设置为SERIALIZABLE。而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。也就说,如果我们使用MySQL来支持XA分布式事务的话,那么最好将事务隔离级别设置为SERIALIZABLE。但是这个隔离级别效率最低。
  • 单点故障 由于协调者的重要性,一旦协调者TM发生故障,参与者RM就会一直阻塞下去。尤其是在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
  • 数据不一致 在两阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了commit请求。而在这部分参与者接收到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

BASE 柔性事务

本地事务 -> XA(2PC) -> BASE
如果将实现了 ACID 的事务要素的事务称为刚性事务的话,那么基于 BASE 事务要素的事务则称为柔性事务。 BASE 是基本可用、柔性状态和最终一致性这三个要素的缩写。

  • 基本可用(Basically Available)保证分布式事务参与方不一定同时在线。
  • 柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉。
  • 而最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性。 在 ACID 事务中对隔离性的要求很高,在事务执行过程中,必须将所有的资源锁定。 柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。通过放宽对强一致性要求,来换取系统吞吐量的提升。

2PC两阶段提交

MySQL支持两阶段提交协议:

  1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件)
  2. 提交阶段(commit phase):如果事务管理器收到两参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意 :必须在最后阶段释放锁资源。

3PC三阶段提交

与两阶段提交不同的是,三阶段提交有两个改动点:
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

  • CanCommit阶段
    3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。 1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
    2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
  • PreCommit阶段
    协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。
    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
    1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
    2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
    3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
    假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
    1.发送中断请求 协调者向所有参与者发送abort请求。
    2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
  • doCommit阶段 该阶段进行真正的事务提交,也可以分为以下两种情况。
    执行提交
    1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    3.响应反馈 事务提交完之后,向协调者发送Ack响应。
    4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。
    中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
    1.发送中断请求 协调者向所有参与者发送abort请求
    2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
    4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
    在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。

TCC

BASE 柔性事务 TCC
TCC 模式即将每个服务业务操作分为两个阶段,第一个阶段检查并预留相关资源,第二阶段根据所有服务业务的 Try 状态来操作,如果都成功,则进行 Confirm 操作,如果任意一个 Try 发生错误,则全部 Cancel。
TCC 使用要求就是业务接口都必须实现三段逻辑:
1.准备操作 Try:完成所有业务检查,预留必须的业务资源。
2.确认操作 Confirm:真正执行的业务逻辑,不做任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务能且只能成功一次。
3.取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
TCC 不依赖 RM 对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务,不同于AT的是就是需要自行定义各个阶段的逻辑,对业务有侵入。 TCC 需要注意的几个问题:
1、允许空回滚
2、防悬挂控制
3、幂等设计

SAGA

Saga 模式没有 try 阶段,直接提交事务。
复杂情况下,对回滚操作的设计要求较高。

Seata

Seata-TCC/AT 柔性事务
Seata 是阿里集团和蚂蚁金服联合打造的分布式事务框架。 其 AT 事务的目标是在微服务架构下,提供增量的事务 ACID 语意,让开发者像使用本地事务一样,使用分布式事务,核心理念同 Apache ShardingSphere 一脉相承。
Seata AT 事务模型包含TM (事务管理器),RM (资源管理器) 和 TC (事务协调器)。 TC 是一个独立部署的服务,TM 和 RM 以 jar 包的方式同业务应用一同部署,它们同 TC 建立长连接,在整个事务生命周期内,保持远程通信。 TM 是全局事务的发起方,负责全局事务的开启,提交和回滚。 RM 是全局事务的参与者,负责分支事务的执行结果上报,并且通过 TC 的协调进行分支事务的提交和回滚。 Seata 管理的分布式事务的典型生命周期:
TM 要求 TC 开始一个全新的全局事务。
TC 生成一个代表该全局事务的 XID。
XID 贯穿于微服务的整个调用链。
TM 要求 TC 提交或回滚 XID 对应全局事务。
TC 驱动 XID 对应的全局事务下的所有分支事务完成提交或回滚。

hmily

Hmily 是一个高性能分布式事务框架,开源于2017年,目前有 2800 个 Star,基于TCC 原理实现,使用 Java 语言开发(JDK1.8+),天然支持 Dubbo、SpringCloud、Motan 等微服务框架的分布式事务。 支持嵌套事务(Nested transaction support)等复杂场景
支持 RPC 事务恢复,超时异常恢复等,具有高稳定性
基于异步 Confirm 和 Cancel 设计,相比其他方式具有更高性能
基于 SPI 和 API 机制设计,定制性强,具有高扩展性
本地事务的多种存储支持 : redis/mongodb/zookeeper/file/mysql
事务日志的多种序列化支持 :java/hessian/kryo/protostuff
基于高性能组件 disruptor 的异步日志性能良好
实现了 SpringBoot-Starter,开箱即用,集成方便
采用 Aspect AOP 切面思想与 Spring 无缝集成,天然支持集群
实现了基于 VUE 的 UI 界面,方便监控和管理

本地消息表

本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

MQ

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。 以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。

本文整理自网络