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

  1. 云栖社区>
  2. 博客>
  3. 正文

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

云栖号资讯小哥 2020-07-21 15:36:21 浏览752

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


一、背景

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

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

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

二、常见的解决方案

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

  • 微服务下跨服务操作不通数据库实例
  • 单体系统跨数据库实例
  • 跨服务操作同一数据库实例

2.1 2PC两阶段提交

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

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

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

2.1.1 XA方案

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

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

DTP模型还定义了TM和RM之间通信的接口规范,这个规范就是XA,本职上也是数据库提供的2PC接口协议。基于XA方案的事务流程如下:

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

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

2.2.2 Seata的AT方案

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

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

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

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

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

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

2.2 TCC补偿型

TCC补偿型分布式事务是纯业务层的解决方案。它要求每个分支事务实现三个操作:try预处理、confirm确认、cancel撤销。三个操作本质上也是两阶段协议,try操作为一阶段,confirm或者cancel只有一个会被执行,为二阶段。TCC称之为补偿性分布式事务的原因,也是因为这是纯业务层的解决方案,不需要依赖底层数据存储是否支持事务,通过cancel操作的补偿内容,实现分布式事务整体的原子性。TCC方案的一般事务流程如下:

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

TCC补偿性分布式事务方案默认confirm操作和cancel操作不会失败。实际实施中这个是无法百分百保证的,所以一般需要配套适合的事务恢复机制,本质上就是对确认或回滚时失败的事务按一定策略进行重试,直至成功。如果还不行,则需要人工介入干预。在实际分布式场景中,要实际实现TCC补偿性分布式事务,一般还要考虑以下三个问题:

  • 空回滚:是指在没有调用try操作的时候,调用了二阶段的cancel操作,从而导致数据不一致的问题。这种情况一般发生在分支事务所在服务出现故障无法提供服务,try操作调用失败,然后在整个事务回滚时故障恢复,cancel正常执行。
  • 幂等:因为TCC需要对confirm和cancel提供重试机制,所以存在因为幂等问题导致数据不一致的风险。
  • 悬挂:是指在某些异常场景下,cancel比try先执行,导致try操作预留资源无法处理的问题。一般发生在RPC调用try时网络拥堵导致调用方超时抛出异常,继而产生事务回滚,然后分支事务的cancel可能会比try先执行。

2.2.1 TCC-Transaction

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

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

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

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

2.3 基于可靠消息最终一致

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

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

2.3.1 本地消息表方案

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

  • 事务发起方的本地事务包括自己的业务操作 和 增加事务消息日志 两部分操作,由本地事务保证原子性。
  • 事务发起方通过定时任务的方式扫描事务消息表,对未发送消息实施发送,在消息中间件反馈发送成功后删除对应事务消息日志记录,保证发起方投递事务的可靠性。
  • 事务参与者监听消息队列,基于消息队列的消息确认(ACK)机制,参与者接收到消息并业务处理完成后向消息中间件发送ack,此时消息中间件清除该消息且不再发送,保证参与者接收消息的可靠性问题。
  • 事务参与者对事务消息的消费方法实现幂等性,解决可能存在的消息重复消费问题。

2.3.2 RocketMQ事务消息方案

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

  • 事务发起方发送prepare消息到RocketMQ,RocketMQ存储该消息并反馈发起方消息接收成功,prepare消息不允许被消费者消费。
  • 发起方接收到RocketMQ的反馈后,执行本地事务,若本地事务执行成功,发起方向RocketMQ发送prepare消息的commit指令,否则发送rolback。
  • RocketMQ接收到发起方二次对prepare消息的确认指令后,执行消息的对应操作,commit控制消息进入实际的消费队列,rollback会删除对应消息,至此,保证了本地事务与消息发送的原子性问题。
  • 对于事务发起方二次确认的可靠性问题,RocketMQ提供反向的定时任务汇差事务状态机制,最多重试15次,超过则丢弃消息。
  • 事务参与者监听RocketMQ,基于消息确认(ACK)机制,参与者接收到消息并业务处理完成后向RocketMQ发送ack,保证参与者接收消息的可靠性问题。
  • 事务参与者对事务消息的消费方法实现幂等性,解决可能存在的消息重复消费问题。

三、对比选型

1

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

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

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

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

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/live

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

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