一文搞懂如何选择合适的分布式事务技术

云栖号资讯:【点击查看更多行业资讯】
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!


一、背景

传统单体系统时代,一个系统操作一个数据库存储。在系统运行时,单次请求或者说单个线程内对同一数据$ B g 9库数据的多次操作,均可通过开启本地事务,依赖数据库提供的事务特性,实现单个请求涉及数据操作的事务管理。d ! ^ ; O但随着系统业务快速增长,单机Q X ` & Z系统无U 4 ; K U #法支撑增长后的业务体量,数据库拆分、根据存储特性选用多种数据存储| K ) W、系统拆分为微服务架构等都是常见的@ 9 @ l ^解决手段。

但是伴随这架构演进,在微服务架构体系中,传统单体系统3 Y 4 q ^ M k q &中依赖本地事务的方式在分布式环境中已经无法满足需求,因为一个请求涉及的数据操作可能涉及多个服务、多次远程调用甚至多个数据存储。因此,在微服务架构体系,倘若特定场S c Y Y ; , &景下的核~ k F / m Z G心业务流程,需要在业务处理时实现单次请求的事务管理,必须引入满足架构要求、满足业务要求的分布式事务技术。

本文主要是结合笔者自身在项目中特定业务落R x G 2 o U地分布式事务管理时对各项分布式事务解决方案的一些调b A N B研和思考,由于三阶段提交并未发4 Q @ 3 o e % Q现有成熟的开源技术,所以本文并未涉及。另外个人建议先对分布式相关理论比如:CAP、BASE等有基E r G本了解后,再来深入思考各@ G r T v ` G o H种解决方案,就不会有看时懂了完事忘了的感觉。

二、常见的解决方案

一般产生分布式事务的技术场景有下面三个方面:

  • 微服务下跨服务操作不通数据库实例
  • 单体% 5 $ . ~ d X系统跨数据库实例
  • 跨服务操作同一数据库实例

2.1 2PC两阶段提交

两阶段提交协议将整个事务流程分为准备阶段(P) 和 提交阶段(C) 两个阶段。在事务流程中又分为事务参与者和事务管理器两个角色。事务管理器负责决策整个分布式事务的提交与回滚,事务参与者负责自己本地事务的提交与回滚。2PC规定事务1 0 ] _ a W N P 2的进行流程如下:

  • 准备阶段:事& Y $ u A Q [ Q务管理器给每个事务参与者发送Prepare消息,每个事务参与者执行本地事务,锁定相关资源,并写本地的Und h } N { / -o/Redo日志(注意:此时并不提交本地事务)
  • 提交阶段:如果事务管理器收到任何一个参与者的执行失败反馈或者是超时反馈,会通知每个事务参与者回滚本地事务,否则通知所有参与者提交本地事务;参与者无论执行回滚还是提交操作,均会释放准备阶段占用的锁资源。

当前数据库方面) P j Q x g支持2PC协议的是Oracle和Mysql。同时针对2PC协议规范,目前流行的具体实施方案有 XA方案 和 阿里开源的分布式事务框架Seata 提供的 AT 模式。

2.1.1 XA方案

为了提供统一的对接模型和标准,X/OPEN组织提出了一个DTP(Distributed Transaction Processing Referenc] ^ G Te Model)模型,其中规定了分布式事务中的三个角色:

  • AP:应用程序
  • TM:事务管理器,负责协调管理整f | R q h 8个分布式事务
  • RM:资源管理器也就是事务参与者,负责控制分支事务

DTP模型` w Q j v p还定义了TM和RM之间通信的接口规范,这个规7 9 F范就是XA,本职上也是数^ S U A C n据库提供的2PC接口协议。基于XA方案的事务流程如下:

  • AP 持有 D1 和 D2 两个数据源;
  • AP 通过 TM 通知 D1 的 RM 操作数据,同时通知 D2 的 TM 操作数据,此时 D1 和 D2 操作的数据锁定,RM 不提交事务;
  • TM 收到 两个数据源的 R0 M { mM 执行恢复,只要有一方失败,则向另外的 RM 发起回滚指令,对应 RM 回滚分支事务释放资源锁;
  • 若均为成功,TM 向所有 RM 发起提交指5 q ^ v ? 9 c令,所有 RM 接到指令提交事务,释放资源锁。

XK O ] 9 @ & WA方案本质上是数据库层面的分布式事务,要求数据库支持事务,实现强一, b 8致性。在整个事务流程中,从准备阶段到第二阶段的commit或rollback的整个过程中,TM一直持有对应相关数c 2 . ~ -据资源的锁,如果有其他事务要修改数据库的该条数据,就必须等待锁的释放,存在长事务风险。

2.2.2 Seata的AT方案

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务,提供了 AT、TCC、Sf @ K ^ 5 - x V vAGA 和 XA 四种事务模式。这里主要说明的是AT模式。Seata的AT模式本质上对2PC协议的优化演进,它是工作在JM # ,ava应用层的中间件,通过对支持3 3 q ^ p本地 ACID 事务的关系型数据库的分支事务的协调来驱动完成全局事务。Seata对分布式事务的参与角色做了划分并提供了以下三个组件:

  • TC:事务协调器。是一个独立的中间件,需要独立部署运行,负责维护全局事务, f w的运行状态,接收 TM 的指令发起全局事务的提交J D M与回滚,负责与 RM0 + S 通信协调各分支事务的提交与回滚。
  • TM:事务管理器。作为一个JAR 或是 依赖 嵌入到应用程序中工作,负责开启一个全局事务,并最终向 TC 发起全局事务操作指令。
  • RM:分支事务控制器。嵌入应用程序,负责分支注册,状态汇报、接收 TC 指令等工作,控制分支事务的提交与回滚。

Seata在AT模式下,整体的一个事务流程如下:

  • TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
  • X V 6 8 j 9 (程调用时,XID 在q a @调用A K A U u u y _ 2链路的上下文中传播,各 RM 向 TC 注册 分支事务,并将其纳入b g x XID对应的全局事务管辖
  • 注册完毕后 RM 控制分支事务执行同时插入回滚日志记录并提交,释放资源锁
  • TM 向 TC 发起针对 XID 的全局提交 或 回滚决议
  • TC 调度 XID 下管辖的全部分支事务 完成提交或回滚(回滚就是反向操作)

Seata的AT模式对于2PC协议的演进主要是两个方面。一方面是在准备阶段就控制事务提交,减少资源锁的占用时间,但是同时分支事务的 N L y 1 t $业务操作和回滚日志插入在一个本地事务,保证了原子性;另一方面是将RM转移到应用层面。而针对于事务提前提交与回滚反向操作这种模式下一阶段存在的写隔离问题,Seata对于整个分布式事务提出了全局锁的概念。在一阶段提交本地事务前都需要 K 4 h , e A先去获取对应资源的全局锁,获取不到全局锁,便不能提交本地事[ S - T A务。

当然如果获取对应全局锁在一定范围内不能成功时,也不会继续阻塞,而是会放弃,同时回滚本地事务,代表y W I该分支事务一阶段执行失败。对于一阶段的读隔离问题,SeataAT模式默认的全局隔离级别为读未提交,如果需要读已提交,需要对SELCT语句增加FOR UPDATE。Seata会代理SELCT FOR UPDATE语句,在执行对应资源查询时先获取该资源的全局锁。获取到全局锁后执行读操作,一定读到的是其他事务提交后的数据。

2.2 TCC补偿型

Tx 8 u v W c : k NCC补偿型分布式事务是纯业务层的解决方案。它要求每个分5 m 9支事务实现三个操作:try预处理、confirm确认c g Y { [ j d &、cancel撤销。三个操作本质上也是两阶段协议,tryD ) N w a 7 b操作为一阶段g $ 3 s V L U,con] a = sfirm或者cancel只有一个会被执行,为二阶段。z [ Y H j o ?TCC称之为补偿性分布式事务o j ~ o的原因,也是因为这是纯业务层的解? f - Q y d 决方案,不需要依赖底层数据存储是否支持事务,通过cancel操作的补偿内容,实现分布式事Z Y e K / 3务整体的原子性。TCC方案的一般{ ~ 5 r % W v ~事务流程如下:

  • TM发起全局事务,所有分支事务执行try操作
  • try操作全部成功时,TM发起所有分支事务的cons ; ofirm操作
  • try存在失败时,TM发起所有分支事务的cancel操作

TCC补偿性分布式事务方案默认confirm操作和cancel操作不会失败。实际实施中这个是无法百分百保证的,所以一般需要配套适合的事务恢复机制,本质上就7 p _ ! ; x I g i是对确认或回滚时失败的事务按一定策略进行重试,直至成@ ) f z K q + p G功。如果还不行,则需要人M s @ 7 R工介入干预。在实际分布式场景中,要实际实现TCC补偿性分布式事务,一般还要考Q } j m : H虑以下三个问题:

  • 空回滚:是指在没有调用try操作的时候,调用了二阶段的cancel操作,从而导致数据不一0 * ? k t v = V J致的问题v j N & [ & : t。这种情况一般发生在分支事务所在服务出现故障无法提供服务,try操作调用失败,然后在整个事务回滚时故障恢复,c| t U ( - N 6ancel正常执行。
  • 幂等:因为TCC需要对confirm和cancel提供重试机制,所以存在因9 q D q ! , ,为幂等问题导致数据不一致的风险。
  • 悬挂:是指在某些异常场景下,cancel比try先执行,导致try操作预留资l P ] s x e i源无法处理的问题。一般发生在RPC调用try时网络拥堵导致调用方超时抛出异常,继而产生事务回滚,然后分支事务的cancel可能会比try先执行。

2.2.1 TCC-Transaction

TCC-Transaction是开源的TCC补偿性分布式事务框架,TCC为Try、Confirm、Cancel的缩写。TCC-Transactw L H m Yion基于AOP的思想,对事务方法进行加强,解析事务注解中配置的确认和回滚方法生成对应的上下文信息,在TM对事务的管理下,2 K ~ . e - 3实现Tj 9 y ~ O *CC补偿性的分布式事务。同时提供基于定时任务的事务恢复机制,支持重试策略可配,实现异常场景下的事务重试。在该框架中,事务A u @ Q v D Z y发起方扮演TM的角色,管理协调所在服务发起的所有TCC事务。对于每一个事务方法,业务人员需要自行实现对应的确认和S + u g t _ F 2回滚方法逻辑,这里建议一定从整个业务流程考虑分布式事务设计,避免单纯的基于本地事务的思想使用。

TCC-Transaction内部有两个核心的拦截器(在对应切面类中e 0 @ O调用),分别是Comp8 V ! 0ensableTransactionInterceptor和ResourceCoordinatorInterceptor d a / G k f y &,两者均已环绕通知的方式对事F Z 8 9 o F务方法加强。前者负责对事务方法增加事务处理,包括事务的开启、回滚、确认以及事务资5 e W源清理。后者负责分支事务的注册。在微服务场景中,它支持Spring-Cloud和Dubbo两种微服务体系,核心是通过隐式传参的方式,将事务上下文在整个调用链中传递,实现分支事务与全局事务的关联。TCC-Transaction实际的事务流程如下:

  • 事务发起方发起全局事务,依次执行所有分支事务的try操作;
  • 在执行try操作前,会开启各分支事务,从事务上下文中获取全o 6 s U Q A局事务id,完成分支事务注册并在各分支事务记录在对应的分支事务表中;
  • try阶段执行后,如果全部成功,会由事务发起方触发分布式, c j N事务g j g p的提交,由事务管理6 C x器获取当前事务,遍历当前事务的所有参与者,依次发起确认提交;
  • 如果存在执行失败,这里是捕获到了异常,由事务发起方触发分布式事务的回滚,和确认提交同理。
  • 整个事务操作执行结束后,由事务发起方对相关事务资源进行清理释放。

TCC-Transa) f M . r cction在三个操作中均不依赖本地事务,但是在微服务场景中,无论确认回滚,比起原有逻辑都会增加最少一倍的rpc或是http请求次数,另外由于各O z X ( x k @参与的服务基本都会维护分支事务存储记录,性能影响还是较为明显的,不太适合复杂操作的大事务场景。

2.3 基于可靠消息最终一致

基于可靠消息的最终一致方案: 2 U ]追求的是各分支事务的最终一致。将整个分布式事务分为主线或者是主事务和分支事务两部分,通过消N S * G j F ! Z息中间件进行解耦。事务发起方执行完本地事务后发出一条消息,默认事P 8 ? ^ y 9 { $务参与方一定能够接收并成功处理事务。核心是强调只要消息发给事务参与方,最终能达到事务一致,不存在子事务失败导致整个事务回滚的场景,方案需要参与者一定成功,必要时候只能人工干预补偿。该方案需要解决以下问题:

  • g * ^ ( : v X u地事务与消息发送的原子性问题:事务发起方在本地事务执行成功后m z w消息必须发出i $ 2 u g去,否则就丢弃消息、回滚本地事务。
  • 事务参与方接收消息的可靠性问题:事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
  • 消息重复消费问题:实现事务参与方的方法幂等性。

2.3.1 本地消息表方案o Y 0 G W

通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确i $ O q G 2认消息发送给W D v A y @ 3消费方成功后再将消息删除。整个事务流程如下:

  • 事务发起方的本地事务包括自己的业务操作. L J m ) @ 和 增加事务消息日志 两部分操作,由本地事务保证原子性。
  • 事务发起方通过定时任务的方式扫描事务K ) S k 7 @消息表,对未发送消息实施发送,在消息中间件反馈发送成功后删除对应事务消息日志记录,保证发起方投递事务E 3 9 Y W = 0的可靠性。
  • 事务L n 4 l { B G r %参与者监听消息队列,基于消息队列的消息确认(ACK)机制,参与者接收到消2 P f 3息并业务处理完成后向消息中间件发送ack,此时消息中间件清除该消息且不再发送,保证参与者接收消息的y ~ m &可靠性问题。
  • 事务参与者对事务消息的消费方法实现幂等性,解决可能存在的消息重复消费问题。

2.3.2 RocketMQ事务消息方案

RocketMQ在4.3版V R b S K U 3 W U本开始支持事务消息,即prepare消息。这种消息在发送到MQ之后不会立即投A Z I Q t * 3递到消费者,会由发送者二次确认后,由RocketMQ根据确认指令决定投递还是丢弃。基于该方案的整个事务流程如下:

  • 事务发起方发送prepare消息到RocketMQ,RocketMQ存储该消息并反馈发起方消息接收成功,prepare消息不允许被消费者消费。
  • 发起方接收到RocketMQ的反馈后,执行本地C 4 l G W 8 j事务,若本地事务执行成功,发起方向RocketMQ发送prepare消息的commit指令,否则发送rolbaV } x ` ,ck。
  • RocT D 5 ( H V 0ketMQ接收! R V `到发起方二次对prepare消息的确认指令x : =后,执行消息的对应操作,commitf M c ` 4 ~ & X C控制消息进入实际的 ) G [ T J ~ a s消费队列,roll! + . k V L /back会删除对应消息,至此,保证了本地事务与消息发送的原子性问题。
  • 对于事务发起方二次确认的可靠性问题,RocketMQ提供反向的定时任务汇差事务/ O B c |状态机制,最多重试15次,超过则丢弃消息。
  • 事务参与者监听RocketMQ,基于^ F h x消息确认(ACK)机制,参与者接收到] o F 3 i消息并业务处理完成后向RocketMQ发送ack( W q o r U q ? 4,保证参与者接收消息的可靠性问题。
  • 事务参与者对事务消息的消费方法实现幂等性,解决可能存在的消息重复消费问题。

三、对比选型

一文搞懂如何选择合适的分布式事务技术

以上是笔者结合自己实际项目做的一~ , 3 o e ~些考量,个t _ m人感觉. 4 .如果项目实际没什么问题^ : e t e R,尽量使用Seata。Seata的代码侵入性极低,同时读写隔离都有处理。RocketMQ事务消息还是要结合实际业务场景去考量,适合那种只关注主线逻辑其他均可异步的场景。

性能方面,个人感觉RocketMQ最佳,毕竟只是多了一步确认。TCC-Transaction和Seata相差不大,二者均多了一倍的远程调用次数,当然实际如何还是要^ / f看具体落地,就笔者本人基于项目环境抽出的一个小模块整合测试,二者差别不大。

从事务协调器角色的高可用方面k . N s而言,Seatab M 2 I I ~的可独立部署多节点,共享数据库事务数据,而TCC-Transaction因为事e Q 3务协调在进程内管理所以由事务发起服务自身决定,一旦在所有分支事务的try操作执行后发起方挂了,那么本次事务涉及I 6 p I R 7 D相关数据均需要手动干预。

总的来n i } g V说,分布式事务能不用就不用,需要好好权衡性能牺牲是否值得,如果实在要用,一定要结合具W z ( .体业务场景具体分析,切忌以本地事务的思想考量整体分布式事务落地的设计。

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.F v $ dcom/0 z 4 2 @ : 4 d [live

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-07-21
本文作者:Mr_小白
本文来自:“掘金”A 0 Q A } X p,了解相关信息可以关注“掘金”