1. 分布式系统-分布式事务及实现方案
|
|
2. 什么是分布式事务
|
|
3. 如何理解分布式事务
|
|
3.1 从分布式的理论角度看
|
|
遵循BASE,允许一定时间内不同节点的数据不一致,但要求最终一致。
3.2 从分布式事务的体系看
XA协议是一个基于数据库层面的分布式事务协议,其分为两部分:事务管理器和本地资源管理器。事务管理器作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。主流的诸如Oracle、Mysql等数据库均已实现了XA接口。
二阶段提交协议(2PC):根据XA协议衍生出来而来;引入一个作为协调者的组件来统一掌控所有参与者的操作结果并最终指示这些节点是否要把操作结果进行真正的提交;参与者将操作成败通知协调者,再由协调者根据所有的参与者反馈情报决定各参与者是否要提交操作还是中止操作。所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段: 提交阶段(执行阶段)
三阶段提交协议(3PC):是对两阶段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直等待阻塞,解决了2PC的单点故障问题,但3PC还是没能从根本上解决数据一致性问题。
Java事务规范
JTA:Java事务API是一个Java企业版的引用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。
JTS: Java事务服务是J2EE平台提供了分布式事务的具体实现规范,j2ee服务器提供商根据JTS规范实现事务并提供JTA接口。
基于业务层:
最终一致性
4. 分布式事务方案之刚性事务
|
|
XA接口是双向的系统接口,在事务管理器以及一个或多个资源管理器之间形成通信桥梁。也就是说,在基于XA的一个事务中,我们可以针对多个资源进行事务管理,例如一个系统访问多个数据库,或即访问数据库又访问像消息中间件这样的资源。这样我们就能够实现在多个数据库和消息中间件直接实现全部提交,或全部取消事务。XA规范不是java规范,而是一种通用的规范;Java中的规范是JTA和JTS:Java事务API是一个Java企业版的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务;Java事务服务是J2EE平台提供了分布式事务的具体实现规范,j2ee服务器提供商根据JTS规范实现事务并提供JTA接口。
5. 两阶段提交(2PC)
|
|
简单而言:参与者(participant)用来管理资源,协调者(coordinator)用来协调事务状态
两段提交(2PC-Prepare&Commit)是指两个阶段的提交:
协调者向所有参与者发送REQUEST-TO-PREPARE 当参与者收到REQUEST-TO-PREPARE消息后,它向协调者发送消息PREPARE或者NO,表示事务是否准备好;如果发送的是NO,那么事务要回滚;
协调者收集所有参与者的返回消息,如果所有参与者都返回的是PREPARED,那么协调者向所有参与者发送COMMIT消息;否则,协调者向所有回复PREPARED的参与者发送ABORT消息;
参与者如果回复了PREPARED消息并且收到协调者发来的COMMIT消息,或者它收到ABORT消息,它将执行提交或回滚,并向协调者发送DONE消息以确认。
二段提交的缺点:
二阶段提交看似能够提供原子性,但它存在着严重的缺陷:
2PC小结
2PC除了本身的算法局限之外,还有一个使用上的限制,就是它主要用在两个数据库之间(数据库实现了XA协议)。两个系统之间是无法使用2PC的,因为不会直接在底层的两个业务数据库之间做一致性,而是在两个服务上面实现一致性。
|
|
6. 三阶段提交(3PC)
|
|
3PC的三个阶段分别是CanCommit、PreCommit、DoCommit:
3PC存在的问题
3PC工作在同步网络模型上,它假设消息传输时间是有上届的,只存在机器失败而不存在消息失败。这个假设太强,现实的情形是,机器失败是无法完美地检测出来的,消息传输可能因为网络拥堵花费很多时间。同时,说阻塞是相对,存在协调者和参与者同时失败的情形下,3PC事务依然会阻塞。实际上,很少有系统会实现3PC,多数现实的系统会通过复制状态机解决2PC阻塞的问题。比如,如果失败模型不是失败-停止,而是消息失败,那样3PC会产生不一致的情形。
7. 分布式事务方案之柔性事务
|
|
7.1 补偿事务(TCC)
|
|
TCC它的核心思想是:“针对每一个操作都要注册一个与其对应的确认和补偿”
拿下单和扣减库存解释下它的三个操作:
7.1.1 TCC的缺点
1.空回滚 当一个分支事务所在服务发生宕机或者网络异常导致调用失败,并未执行try方法,当恢复后事务执行回滚操作就会调用此分支事务的cancel方法,如果cancel方法不能处理此种情况就会出现空回滚。
是否出现空回滚,我们需要判断是否执行了try方法,如果执行了就没有空回滚。解决方法就是当主业务发起事务时,生成一个全局事务记录,并生成一个全局唯一ID,贯穿整个事务,再创建一张分支事务记录表,用于记录分支事务,try执行时将全局事务ID和分支事务ID存入分支事务表中,表示执行了try阶段,当cancel执行时,先判断表中是否有该全局事务ID的数据,如果有则回滚,否则不进行任何操作。比如seata的AT模式中就有分支事务表。
2.幂等问题
由于服务宕机或者网络问题,方法的调用可能出现超时,为了保证事务正常执行我们往往会加入重试机制,因此就需要保证comfirm和cancel阶段操作的幂等性。
我们可以在分支事务记录表中增加事务执行状态,每次执行confirm和cancel方法时都查询该事务的执行状态,以此判断事务的幂等性。
3.悬挂问题
TCC中,在调用try之前会先注册分支事务,注册分支事务之后,调用出现超时,此时try请求还未到达对应的服务,因为调用超时了,所以会执行cancel调用,此时cancel已经执行完了,然而这个时候try请求到达了,这个时候执行了try之后就没有后续操作了,就会导致资源挂起,无法释放。
执行try方法时我们可以判断confirm或者cancel方法是否执行,如果执行了那么就不执行try阶段。同样借助分支事务表中事务的执行状态。如果已经执行了confirm或者cancel那么try就执行。
8 Saga事务
|
|
如下内容主要来源于 这里
Saga是由一系列本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。
Saga的实现有很多种方式,其中最流行的两种方式是:
我们继续以订单流程为例,说明一下该模式:
假设一个完整的订单流程包含了如下几个服务:
1.Order Service: 订单服务 2.Payment Service: 支付服务 3.Stock Service: 库存服务 4.Delivery Service: 物流服务
8.1 基于事件的方式
在基于事件的方式中,第一个服务执行完本地事务之后,会产生一个事件。其他服务会监听这个事件,触发该服务本地事务的执行,并产生新的事件。
采用基于事件的saga模式的订单处理流程如下:
1.订单服务创建一笔新订单,将订单状态设置为"待处理",产生事件ORDER_CREATED_EVENT。 2.支付服务监听ORDER_CREATED_EVENT,完成扣款并产生事件BILLED_ORDER_EVENT。 3.库存服务监听BILLED_ORDER_EVENT,完成库存扣减和备货,产生事件ORDER_PREPARED_EVENT。 4.物流服务监听ORDER_PREPARED_EVENT,完成商品配送,产生事件ORDER_DELIVRED_EVENT. 5.订单服务监听ORDER_DELIVRED_EVENT,将订单状态更新为"完成".
在这个流程中,订单服务很可能还会监听BILLED_ORDER_EVENT, ORDER_PREPARED_EVENT来完成订单状态的实时更新。将订单状态分别更新为"已经支付"和"已经出库"等状态来及时的反映订单的最新状态。
8.2 该模式下分布式事务的回滚
为了在异常情况下回滚整个分布式事务,我们需要为相关服务提供补偿操作接口。
假设库存服务由于库存不足没能正确完成备货,我们可以按照以下流程来回滚整个Saga事务:
1.库存服务产生事件PRODUCT_OUT_OF_STOCK_EVENT。 2.订单服务和支付服务都会监听该事件并作出响应:
- 支付服务完成退款
- 订单服务将订单状态更新为失败。
8.3 基于事件模式的优缺点
优点: 简单且容易理解。各参与方之间无直接沟通,完全解耦。这种方式比较适合整个分布式事务只有2-4个步骤的情形。
缺点: 这种方式如果涉及比较多的参与方,则比较容易失控。各业务参与方可随意监听对方的消息,以至于最后没人直到到底有哪些系统在监听哪些消息。更悲催的是,这个模式还可能产生环形监听,也就是两个业务方相互监听对方所产生的事件。
8.4 基于命令的方式
在基于命令的方式中,我们会定义一个新的服务,这个服务扮演的角色就和一只交响乐队的指挥一样,告诉各个业务参与方,在什么时候做什么事情。我们管这个新服务叫做协调中心。协调中心通过命令/回复的方式来和Saga中其他服务进行交互。
我们继续以之前订单流程来举例。下图中的Order Saga Orchestrator就是新引入的协调中心。
1.订单服务创建一笔新订单,将订单状态设置为"待处理",然后让Order Saga Prchestrator(OSO)开启创建订单事务 2.OSO发送一个"支付命令"给支付服务,支付服务完成扣款并回复"支付完成"消息。 3.OSO发送一个"备货命令"给库存服务,库存服务完成库存扣减和备货,并恢复"出库"消息。 4.OSO发送一个"配送命令"给物流服务,物流服务完成配送,并回复"配送完成"消息。 5.OSO向订单服务发送"订单结束命令"给订单服务,订单服务将订单状态设置为"完成"。 6.OSO清楚一个订单处理Saga的具体流程,并在出现异常时向相关微服务发送补偿命令来回滚整个分布式事务。
实现协调中心的一个比较好的方式是使用状态机。
该模式下分布式事务的回滚
该模式下的回滚流程如下:
8.5 基于命令方式的优缺点
优点:
缺点:
8.6 Saga模式建议
9 本地消息表
|
|
角色:
通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
整体的流程如下图:
上图中整体的处理步骤如下:
一些必要的容错处理如下:
9.1 优点
9.2 缺点
10 MQ事务方案(可靠消息事务)
|
|
MQ事务方案整体流程和本地消息表的流程很相似,如下图:
从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。
那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:
正常情况:事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
异常情况:事务主动方消息恢复
在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
10.1 优点
相比本地消息表方案,MQ 事务方案优点是:
10.2 缺点
11 最大努力通知
|
|
最大努力通知的整体流程如下图:
在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的;
但是最大努力通知,事务主动方尽最大努力(重试,轮询….)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
12 分布式事务的中间件Seata
|
|
12.1 Seata AT 模式
前提
整体机制
两阶段提交协议的演变:
提交异步化,非常快速的完成
回滚通过一阶段的回滚日志进行反向补偿。
写隔离
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
12.2 Seata XA 模式
前提
整体机制
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
执行阶段:
完成阶段:
工作机制
1. 整体运行机制
XA 模式 运行在 Seata 定义的事务框架内:
2. 数据源代理
XA 模式需要 XAConnection。
获取 XAConnection 两种方式:
第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。
第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可
我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。
类比 AT 模式的数据源代理机制,如下:
但是,第二种方法有局限:无法保证兼容的正确性。
实际上,这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,我们只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效。
综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。
类比 AT 模式的数据源代理机制,如下:
3.分支注册
XA start 需要 Xid 参数。这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。将来一个可能的优化方向:把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。
12.3 Seata TCC 模式
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.AT 模式基于 支持本地 ACID 事务 的 关系型数据库:
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中
12.4 Seata Saga 模式
|
|
12.5 概述
适用场景:
优势:
缺点:
不保证隔离性
12.6 Saga的实现
目前SEATA提供的Saga模式是基于状态机引擎来实现的,机制是: