本文共 23965 字,大约阅读时间需要 79 分钟。
本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:
多个数据要同时操作,如何保证数据的完整性,以及一致性?
答:事务,是常见的做法。事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。举个栗子:
用户下了一个订单,需要修改余额表,订单表,流水表,于是会有类似的伪代码:start transaction; CURD table t_account; any Exception rollback; CURD table t_order; any Exception rollback; CURD table t_flow; any Exception rollback;commit;
事务,可保证数据的完整性以及一致性。
互联网的业务特点,数据量较大,并发量较大,经常使用拆库的方式提升系统的性能。如果进行了拆库,余额、订单、流水可能分布在不同的数据库上,甚至不同的数据库实例上,此时就不能用数据库原生事务来保证数据的一致性了。
事务是基于数据进行操作,需要保证事务的数据通常存储在数据库中,所以介绍到事务,就不得不介绍数据库事务的 ACID 特性。
ACID 指数据库事务正确执行的四个基本特性的缩写,包含以下四个:
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。
如果事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
例如:银行转账,从 A 账户转 100 元至 B 账户,分为两个步骤:
这两步要么都完成,要么都不完成。因为如果只完成第一步,第二步失败,钱会莫名其妙少了 100 元。
指在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏。
以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。
数据库允许多个并发事务同时对数据进行读写和修改的能力,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。
隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
例如:现有有个交易是从 A 账户转 100 元至 B 账户,在这个交易事务还未完成的情况下,如果此时 B 查询自己的账户,是看不到新增加的 100 元的。
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
简单而言,ACID 是从不同维度描述事务的特性:
补偿事务,是一种在业务端实施业务逆向操作事务。TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现。
主要是对业务系统资源做检测及预留、
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。
因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,当 Try 阶段服务全部正常执行,Confirm执行确认业务逻辑操作。
Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
当 Try 阶段存在服务执行失败, 进入 Cancel 阶段。
主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。
int Do_AccountT(uid, money){ start transaction; //余额改变money这么多 CURD table t_account with money for uid; anyException rollback return NO; commit; return YES;}
那么,针对修改余额的补偿事务可以是:
int Compensate_AccountT(uid, money){ //做一个money的反向操作 return Do_AccountT(uid, -1*money){ }
同理,订单操作,事务是:Do_OrderT,新增一个订单;
订单操作,补偿事务是:Compensate_OrderT,删除一个订单。要保证余额与订单的一致性,伪代码:
// 执行第一个事务int flag = Do_AccountT();if(flag=YES){ //第一个事务成功,则执行第二个事务 flag= Do_OrderT(); if(flag=YES){ // 第二个事务成功,则成功 return YES; } else{ // 第二个事务失败,执行第一个事务的补偿事务 Compensate_AccountT(); }}
TCC例子
假入 Bob 要向 Smith 转账,思路大概是: 我们有一个本地方法,里面依次调用缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
TCC例子2
以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。try
假设商品库存为 100,购买数量为 2。这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。Confirm 阶段
当 Try 阶段服务全部正常执行, 执行Confirm业务逻辑操作Confirm 阶段使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel
当 Try 阶段存在服务执行失败, 进入 Cancel 阶段 Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。TCC 事务机制相比于XA 事务机制(2PC,3PC),有以下优点:
画外音:上面的例子还只考虑了余额+订单的一致性,就有22=4个分支,如果要考虑余额+订单+流水的一致性,则会有222=8个if/else分支,复杂性呈指数级增长。*
单库是用这样一个大事务保证一致性:
start transaction; CURD table t_account; any Exception rollback; CURD table t_order; any Exception rollback; CURD table t_flow; any Exception rollback;commit;
拆分成了多个库后,大事务会变成类似以下的三个小事务,发生在三个库甚至三个不同实例的数据库上:
start transaction1; //第一个库事务执行 CURD table t_account; any Exception rollback; …// 第一个库事务提交commit1;
一个事务,分成执行与提交两个阶段:
执行(CURD)的时间很长提交(commit)的执行很快
于是三个事务的整个执行过程的时间轴如下:
注意,可能出现数据不一致:
第一个事务成功提交之后,最后一个事务成功提交之前,如果出现问题(例如服务器重启,数据库异常等),都可能导致数据不一致。如果改变事务执行与提交的时序,变成事务先执行,最后一起提交,就是后置提交,如下:
答:
串行事务方案,总执行时间是303ms,最后202ms内出现异常都可能导致不一致;
后置提交优化方案,总执行时间也是303ms,但最后2ms内出现异常才会导致不一致;
虽然没有彻底解决数据的一致性问题,但不一致出现的概率大大降低了。
答:对事务吞吐量会有影响:
无法彻底解决多个事务数据不一致性风险
串行事务方案,第一个库事务提交,数据库连接就释放了;
而后置提交优化方案,所有库的连接,要等到所有事务执行完才释放;这就意味着,数据库连接占用的时间增长了,系统整体的吞吐量降低了。
分布式事务,两种常见的实践:
把
trx1.exec(); trx1.commit();trx2.exec(); trx2.commit();trx3.exec(); trx3.commit();
优化为:
trx1.exec(); trx2.exec(); trx3.exec();trx1.commit(); trx2.commit(); trx3.commit();
这个小小的改动(改动成本极低),不能彻底解决多库分布式事务数据一致性问题,但能大大降低数据不一致的概率,牺牲的是吞吐量。
对于一致性与吞吐量的折衷,还需要业务架构师谨慎权衡折衷。
随着互联网快速发展,微服务,SOA 等服务架构模式正在被大规模的使用,现在分布式系统一般由多个独立的子系统组成,多个子系统通过网络通信互相协作配合完成各个功能。具体来说,在分布式环境下,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。
当一个分布式事务跨多个节点时,保持事务的原子性与一致性,是非常困难的。我们需要一个分布式事务的解决方案保障业务全局的数据一致性。
有很多用例会跨多个子系统才能完成,比较典型的是电子商务网站的下单支付流程,至少会涉及交易系统和支付系统。而且这个过程中会涉及到事务的概念,即保证交易系统和支付系统的数据一致性,此处我们称这种跨系统的事务为分布式事务。
在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。
可以看到,如果多个数据库之间的数据更新没有保证事务,将会导致出现子系统数据不一致,业务出现问题。事务的原子性
事务操作跨不同节点时,当多个节点某一节点操作失败时,需要保证多节点操作的原子性。事务的一致性
当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。事务的隔离性
事务隔离性的本质就是如何正确处理多个并发事务的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。
概念
二阶段提交2PC(Two phase Commit)是一种在分布式环境下,所有节点进行事务提交,保持一致性的算法。角色
事务的发起者称协调者,事务的执行者称参与者。在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。
2PC通过引入一个协调者(Coordinator)来统一掌控所有参与者(Participant)的操作结果,并指示它们是否要把操作结果进行真正的提交(commit)或者回滚(rollback)。
两阶段
2PC将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。投票阶段
(1)甲发邮件给乙丙丁,通知明天十点开会,询问是否有时间; (2)乙回复有时间; (3)丙回复有时间; (4)丁迟迟不回复,此时对于这个事务,甲乙丙均处于阻塞状态,算法无法继续进行;提交阶段
(1)协调者甲将收集到的结果通知给乙丙丁; 画外音:什么时候通知,以及反馈结果如何,在此例中取决与丁的时间与决定, 假设丁回复有时间,则通知commit; 假设丁回复没有时间,则通知rollback; (2)乙收到通知,并ack协调者; (3)丙收到通知,并ack协调者; (4)丁收到通知,并ack协调者; 画外音:如果甲没有收到所有ack,则分布式事务迟迟不会结束,下一轮投票则迟迟不会开展。2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
性能低-同步阻塞
2PC在执行过程中,所有节点都处于事务阻塞状态,所有节点所持有的资源(例如数据库数据,本地文件等)都处于锁定状态。典型情况为:
可靠性差-协调者单点问题
另外,如有协调者或者某个参与者出现了崩溃,为了避免整个算法处于一个完全阻塞状态,即所有的参与者出于锁定事务资源的状态中,无法完成相关的事务操作。此时往往需要借助超时机制来将算法继续向前推进。数据不一致-Commit不一定成功
在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
这个时候需要重试(短时间内可能无法充实成功,可以使用一个支持重试的MQ),并且下游需要幂等。
参与者和协调者同时 down 掉
协调者在发送完 commit 消息后 down 掉,而唯一接受到此消息的参与者也 down 掉了。新协调者接管,也是一个懵逼的状态,不知道此条事务的状态。无论提交或者回滚都是不合适的。这个是两阶段提交无法改变的总的来说,2PC是一种比较保守并且低效的算法,分布式事务真的很难做。
XA是由X/Open组织提出的分布式事务的规范,主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。
XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。
下图说明了事务管理器™、资源管理器(RM),与应用程序(AP)之间的关系:
由全局事务管理器管理和协调的事务,可以跨越多个资源(如数据库或JMS队列)和进程。 全局事务管理器一般使用 XA 二阶段提交协议与数据库进行交互。代码示例
应用与XA
MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager
接口,并通过底层事务服务(即JTS)实现。
像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:
由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。
针对两阶段提交存在的问题,三阶段有两个改进点:
三阶段提交的分别为:can_commit
,pre_commit
,do_commit
。
也就是说,除了引入超时机制之外,3PC把2PC的投票阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
3PC的CanCommit阶段其实和2PC的投票阶段很像。参与者根据自身情况回复一个预估值(YES/NO),相对于真正的执行事务,这个过程是轻量的,具体步骤如下:
这个和 2PC 阶段不同的是,此时参与者没有锁定资源,没有写 redo,undo,执行回滚日志。回滚代价低
本阶段协调者会根据第一阶段CanCommit的询问结果采取相应操作:
针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:
针对第二、三种异常情况,协调者认为事务无法正常执行,于是向各个参与者发出abort通知,请求退出Prepared状态,具体步骤如下:
如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:
针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:
优点
这样,就算是在阶段3协调者挂了,参与者继续提交事务。
缺点
没解决数据一致性问题
但是这种机制也会导致数据一致性问题,因为在DoCommit阶段,由于网络原因,协调者发送的rollback响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到rollback命令并执行回滚的参与者之间存在数据不一致的情况。在分布式数据库中,如果期望达到数据的强一致性,那么服务基本没有可用性可言,这也是为什么许多分布式数据库提供了跨库事务,但也只是个摆设的原因,在实际应用中我们更多追求的是数据的弱一致性或最终一致性,为了强一致性而丢弃可用性是不可取的。
解决同步事务。
seata 之类分布式事务的性能评估不是 吞吐量 而是损耗,即因为引入分布式事务, 你的RT增加了多少比例。
微服务倡导将复杂的单体应用拆分为若干个功能简单、松耦合的服务,这样可以降低开发难度、增强扩展性、便于敏捷开发。当前被越来越多的开发者推崇。
系统微服务化后,一个看似简单的功能,内部可能需要调用多个服务并操作多个数据库实现,服务调用的分布式事务问题变的非常突出。分布式事务已经成为微服务落地最大的阻碍,也是最具挑战性的一个技术难题。每一个微服务内部的数据一致性仍有本地事务来保证。而整个业务层面的全局数据一致性需要一个分布式事务的解决方案保障业务全局的数据一致性。
由2014年阿里中间件团队发布 TXC(Taobao Transaction Constructor)改造来的阿里分布式事务解决方案:GTS(Global Transaction Service),已正式推出开源版本,取名为“Fescar”,希望帮助业界解决微服务架构下的分布式事务问题。
现在更名为Seata。
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
参考文档:
高速增长的互联网时代,快速试错的能力对业务来说是至关重要的:
接入成本低,对业务无侵入
不应该因为技术架构上的微服务化和分布式事务支持的引入,给业务层面带来额外的研发负担。这里的“侵入”是指,因为分布式事务这个技术问题的制约,要求应用在业务层面进行设计和改造。这种设计和改造往往会给应用带来很高的研发和维护成本。把分布式事务问题在中间件
这个层次解决掉,不要求应用在业务层面做额外工作。
高性能
引入分布式事务支持的业务应该基本保持在同一量级上的性能表现,不能因为事务机制显著拖慢业务。具体来说,引入分布式事务的保障,必然会有额外的开销,引起性能的下降。我们希望把分布式事务引入的性能损耗降到非常低的水平,让应用不因为分布式事务的引入导致业务的可用性受影响。
既有的分布式事务解决方案按照对业务侵入性分为两类,即:对业务无侵入的和对业务有侵入的。
业务无侵入的方案-XA
既有的主流分布式事务解决方案中,对业务无侵入的只有基于 XA 的方案,但应用 XA 方案存在 3 个方面的问题:要求数据库提供对 XA 的支持
如果遇到不支持 XA(或支持得不好,比如 MySQL 5.7 以前的版本)的数据库,则不能使用。受协议本身的约束,事务资源的锁定周期长
长周期的资源锁定从业务层面来看,往往是不必要的,而因为事务资源的管理器是数据库本身,应用层无法插手。这样形成的局面就是,基于 XA 的应用往往性能会比较差,而且很难优化。已经落地的基于 XA 的分布式解决方案,都依托于重量级的应用服务器(Tuxedo/WebLogic/WebSphere 等),这是不适用于微服务架构的。
侵入业务的方案
实际上,最初分布式事务只有 XA 这个唯一方案。XA 是完备的,但在实践过程中,由于种种原因(包含但不限于上面提到的 3 点)往往不得不放弃,转而从业务层面着手来解决分布式事务问题。比如:都属于这一类。这些方案的具体机制在这里不做展开,网上这方面的论述文章非常多。总之,这些方案都要求在应用的业务层面把分布式事务技术约束考虑到设计中,通常每一个服务都需要设计实现正向和反向的幂等接口。这样的设计约束,往往会导致很高的研发和维护成本。
不可否认,侵入业务的分布式事务方案都经过大量实践验证,能有效解决问题,在各种行业的业务应用系统中起着重要作用。但回到原点来思考,这些方案的采用实际上都是迫于无奈。设想,如果基于 XA 的方案能够不那么重,并且能保证业务的性能需求,相信不会有人愿意把分布式事务问题拿到业务层面来解决。
一个理想的分布式事务解决方案应该:像使用本地事务一样简单,业务逻辑只关注业务层面的需求,不需要考虑事务机制上的约束。
详细实现思路可参考
解决异步事务。
该实现方式应该是业界使用最多,其核心思想是将分布式事务拆分成本地事务进行处理,来源于ebay。
我们可以从下面的流程图中看出其中的一些细节:
基本思路就是:上游事务-消息生产方
消息生产方(也就是发起方)需要在数据库额外建一个本地消息表,并记录消息发送状态。写消息表和写业务数据的操作要在一个事务里提交,也就是说他们要在一个数据库里面。消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送,直到发送成功即MQ响应了ACK。
下游事务-消息消费方
查询自己的那份本地消息表,如果已经消费过的则不处理;否则需要处理这个消息,并完成自己的业务逻辑,最后将业务数据和消息处理成功写入本地消息表两个操作放到一个事务里,以避免重复消费问题。此时如果本地事务处理成功,表明已经处理成功了,就更新本地消息表记录该条消息已经处理;如果处理失败,那么就会重试执行。
如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
处理未完成消息
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。小结
该方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。库存服务和订单服务分别在不同的服务器节点上,下面把分布式事务最先开始处理的事务方称为事务主动方(库存服务),在事务主动方之后处理的业务内的其他事务称为事务被动方(订单服务)。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:
事务主动方处理本地事务。
事务主动方在本地同一个事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地同个事务中完成扣减库存和写消息表(图中 1、2)。
事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
上面例子中,库存服务把事务待处理消息写到消息中间件,下游的订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。
事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
上面例子中,订单服务把事务已处理消息写回到消息中间件,然后库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。当步骤 1 处理上游业务逻辑出错,事务回滚,相当于什么都没发生。
当步骤 2、步骤 3 下游事务处理出错,由于未处理的事务消息还是保存在事务发送方本地消息表中,事务发送方可以定时轮询为超时消息数据,再次发送到消息中间件通知事务被动方进行处理。
事务被动方消费事务消息重试处理。
如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知所有事务被动方回滚。
@Transactionpublic void buy(){ user.buy();//新建一张msgtable本地消息表,消息在这里插入insertInitMsgToDB();}public void sendMsg(){ //发送消息移到了新方法里kafkaTemplete.sendMdg();}//主流程执行buy();sendMsg();
@Kafkalistener@Transactionpublic void msgConsume(Record record){ if(isConsumed(record)){ //查询本地消息表,已经消费过的则不处理 return; } //处理业务逻辑 deal(record); // 更改本地消息表消息状态为成功 changeRecord(record); // 还可以加逻辑,如果业务逻辑处理失败就给生产者发送消息,让他进行补偿事务等处理}
enable.auto.commit
是 true,就有可能导致at least once
,或者at most once
的问题: enable.auto.commit
:false,消费数据、业务流程完成后再手动提交offsetrequest.required.acks
:-1,生产者必须等ISR全部确认后才返回Begin transaction // 对用户id为A的账户扣款1000元update user_account set amount = amount - 1000 where userId = 'A' // 通过本地事务将事务消息(包括本地事务id、支付账户、收款账户、金额、状态等)插入至消息表: insert into trans_message(xid,payAccount,recAccount,amount,status) values(uuid(),'A','B',1000,1);、end transactioncommit;
这里为了保证幂等性,防止触发重复的转账操作,需要在执行转账操作方新增一个trans_recv_log
表用来做幂等:在第二阶段用户B收到消息后,通过判断trans_recv_log表来检测相关记录是否被执行,如果未被执行则会对B账户余额执行加1000元的操作,并会将该记录增加至trans_recv_log,事件结束后通过回调更新trans_message的状态值。
Begin transaction /**读取消息, B账户加1000.....*/update trans_message set status = 0 where xid = ?end transactioncommit;
方案的优点如下:
缺点如下:
重点
事务-理想化方案
事务消息实际上是一个很理想的想法:我们只要把消息扔到MQ,那么这个消息肯定会被消费成功。生产方不用担心消息发送失败,也不用担心消息会丢失。事务消息,关键一点是把上小节中繁琐的消息状态和重发等用中间件形式封装了。回到现实,消费方如果消息处理失败了,还有机会继续消费,直到成功为止(消费方逻辑bug导致消费失败情况不在本文讨论范围内)。
但遗憾的是市面上大部分MQ都不支持事务消息,其中包括看起来可以一统江湖的kafka。
RocketMQ号称支持,但是还没开源(事务消息相关部分没开源)。阿里云据说免费提供,没玩过(羡慕下阿里等大厂内部猿类们)。不过从网上公开的资料看,用起来还是有些不爽的地方。这是后话了,毕竟解决了很多问题。
下面以网传RMQ为例,说明事务消息大概是怎么玩的: RMQ的事务消息相对于普通MQ,相当于提供了2PC的提交接口:Prepared B:生产方需要先发送一个prepared消息给RMQ。如果操作1失败,返回失败。
执行本地事务A,
Confirm B:步骤2如果成功则需要发送Confirm B消息给RMQ,然后B处于Ready状态;步骤2失败,则调用RMQ cancel接口。
CheckTransaction:步骤3发送Confirm消息失败了(或者超时)该如何处理呢?
RMQ会要求生产方实现一个check的接口,并告知RMQ自己本地事务是否执行成功(第4步)。RMQ会定时轮询所有处于pre状态的消息,并调用对应的check接口,以决定此消息是否可以提交。
Consume:事务被动方B消费到B事务消息,并执行本地事务B。当然第5步也可能会失败。这时候需要RMQ支持消息重试。处理失败的消息过段时间再进行重试,直到成功为止(超过重试次数后会进死信队列,可能得人肉处理了,因为没用过所以细节不是很了解)。
RMQ还是很强大的。我们认为这个程度的一致性已经能够满足绝大部分互联网应用场景。代价是生产方做了不少额外的事情,但相比没有事务消息情况,确实解放了不少劳动力。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,核心思想:
先往MQ推half半事务,但此时不会投递给订阅者。然后处理该事务,最后根据结果发送commit或rollback给MQ,如果是commit就发送half给订阅方执行下游事务;否则抛弃该条消息。
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
使用消息中间件MQ,可能由于消费者挂掉或网络波动,短时间无法消费消息,但生产者只关心消息是否发出去,而不关心是否被消费,所以这就是上面CAP理论中的最终一致性,也是弱一致性。
优点: 实现了最终一致性,不需要依赖本地数据库事务。在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
图5:MQ Server 对该消息发起消息回查。 图6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 图7:发送方根据检查得到的本地事务的最终状态再次提交二次确认。 图8:MQ Server基于 commit/rollback 对消息进行投递或者删除。做过支付宝交易接口的同学都知道,我们一般会在支付宝的回调页面和接口里,解密参数,然后调用系统中更新交易状态相关的服务,将订单更新为付款成功。同时,只有当我们回调页面中输出了success字样或者标识业务处理成功相应状态码时,支付宝才会停止回调请求。否则,支付宝会每间隔一段时间后,再向客户方发起回调请求,直到输出成功标识为止。
其实这就是一个很典型的补偿例子,跟一些MQ重试补偿机制很类似。
一般成熟的系统中,对于级别较高的服务和接口,整体的可用性通常都会很高。如果有些业务由于瞬时的网络故障或调用超时等问题,那么这种重试机制其实是非常有效的。
当然,考虑个比较极端的场景,假如系统自身有bug或者程序逻辑有问题,那么重试1W次那也是无济于事的。那岂不是就发生了“明明已经付款,却显示未付款不发货”类似的悲剧?
其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。
在某些特殊的情况下,还会有“人工补偿”的,这也是最后一道屏障。
saga是 分布式事务 最主流的模式
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction
(长活事务)论文。
Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。
Saga 的执行顺序有两种,如上图:事务正常执行完成
T1, T2, T3, …, Tn,例如:扣减库存(T1),创建订单(T2),支付(T3)等,依次有序完成整个事务。事务回滚
T1, T2, …, Tj, Cj,…, C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。概念
命令协调(Order Orchestrator)即中央协调器负责集中处理事件的决策和业务逻辑排序。中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。
以电商订单的例子为例:
小结
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
该方案的问题是协调器OSO存在单点风险。
概念
事件编排(Event Choreography0):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。
以电商订单为例:
命令协调设计的优点如下:
命令协调设计缺点如下:
事件/编排设计优点如下:
事件/编排设计缺点如下:
值得补充的是,由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证事务隔离性。即当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
在电商等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。在分布式领域基于 CAP 理论以及 BASE 理论,有人就提出了柔性事务的概念。
基于 BASE 理论的设计思想,柔性事务下,在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。
并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐。
下面介绍的是实现柔性事务的一些常见特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样。
在分布式事务执行过程中,如果某一个步骤执行出错,就需要明确的知道其他几个操作的处理情况,这就需要其他的服务都能够提供查询接口,保证可以通过查询来判断操作的处理情况。
为了保证操作的可查询,需要对于每一个服务的每一次调用都有一个全局唯一的标识,可以是业务单据号(如订单号)、也可以是系统分配的操作流水号(如支付记录流水号)。除此之外,操作的时间信息也要有完整的记录。
幂等性,其实是一个数学概念。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。也就是说,同一个方法,使用同样的参数,调用多次产生的业务结果与调用一次产生的业务结果相同。
之所以需要操作幂等性,是因为为了保证数据的最终一致性,很多事务协议都会有很多重试的操作,如果一个方法不保证幂等,那么将无法被重试。
幂等操作的实现方式有多种,如在系统中缓存所有的请求与处理结果、检测到重复操作后,直接返回上一次的处理结果等。
参考
参考
XA 事务机制(2PC,3PC)属于CP模型,强一致性,全局锁,效率很低。
2PC/3PC
依赖于数据库,能够很好的提供强一致性(好像不能保证吧,转载者注)和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。TCC
适用于执行时间确定且较短,实时性要求高(因为是先提交事务,出错就用补偿事务,而不是2PC/3PC那样的rollback,所以速度更快),对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。本地消息表/MQ 事务
都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。Saga 事务
由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比3PC来说缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。
本文介绍的偏向于原理,业界已经有不少开源的或者收费的解决方案,篇幅所限,就不再展开介绍。
实际运用理论时进行架构设计时,许多人容易犯“手里有了锤子,看什么都觉得像钉子”的错误,设计方案时考虑的问题场景过多,各种重试,各种补偿机制引入系统,导致系统过于复杂,落地遥遥无期。
世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!
—— 阿里中间件技术专家沈询有些问题,看起来很重要,但实际上我们可以通过合理的设计或者将问题分解来规避。
设计分布式事务系统也不是需要考虑所有异常情况,不必过度设计各种回滚,补偿机制。
如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。
如果系统要实现回滚流程的话,有可能系统复杂度将大大提升,且很容易出现 Bug,估计出现 Bug 的概率会比需要事务回滚的概率大很多。
在设计系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,可以考虑当出现这个概率很小的问题,能否采用人工解决的方式,这也是大家在解决疑难问题时需要多多思考的地方。
在理解了各个技术方案的流程,优缺点,使用场景后,结合项目中遇到的具体业务需求和各种资源(人力、设备、时间),再借鉴业界案例,网上资源,试着设计+评审一套基本方案。如资源有限,去github找成熟开源框架或付费方案。开源框架使用前研究下原理,相关配置参数并进行稳定性测试,研究下源码