写点什么

谈谈分布式事务

作者:Monin
  • 2023-07-06
    上海
  • 本文字数:4135 字

    阅读完需:约 14 分钟

谈谈分布式事务

一、背景

事务的 ACID 特性可以保障在一个事务中更新多条数据时,要么都更新成功要么都失败。在一个 Domain 服务内部可以使用数据库事务来保证数据一致性(@Transactional)。那如果涉及到跨多个系统、多个数据库的时候,用单一的数据库事务就没办法解决了。

随着微服务、云原生和数据库拆分增多,如何保证这种跨系统、跨数据库的数据一致性问题呢?答案是:分布式事务。

二、分布式事务

数据库事务有 ACID 四个特性,我们知道即使是数据库事务在考虑到性能的因素时,大部分情况下也不需要百分之百地实现 ACID,所以有了事务的四种隔离级别。比如交易平台的 mysql 的隔离级别基于并发性能、热点数据等因素考虑用的就是 RC 隔离级别,而非 mysql 默认的 RR 隔离级别。

分布式事务按照 CAP 理论,分为偏向 CP 的强一致方案,如 XA 协议(2PC)、3PC。偏向 AP 的最终一致性方案,如 TCC、sage、可靠消息通知型事务和 AT 方案。其中最终一致性方案按照作用的层面不同又分为基于业务层面的事务和基于 JDBC 层面的事务。

基于业务层面的事务包括 saga、TCC、可靠消息通知型事务。基于 JDBC 层面的事务包括 Seata AT 方案,实现原理主要是通过拦截 sql,然后通过模拟 TM 事务管理器,RM 资源管理器,然后加一个 TC 全局事务管理器。通过 sql 的操作前的 undo_log、执行结果后的 redo_log 实现。将全局事务拆分为一个个分支事务。

本篇文章先介绍下基于业务层面的事务-TCC 方案、下篇文章简介下基于 JDBC 层面的事务(参考阿里的 Seata AT 方案)

三、名词解析

TCC 由 Try-Confirm-Cancel 三个方法组成的分布式事务解决方案,原理是将两阶段提交提升至应用层的实现。

  1. Try 阶段完成的工作是预定操作资源(Prepare),在正式开始执行业务逻辑之前,先把要操作的资源锁定。

  2. Confirm 阶段完成的工作是执行主要业务逻辑(Commit),它类似于事务的 Commit 操作。在这个阶段中,可以对 Try 阶段锁定的资源进行各种 CRUD 操作。如果 Confirm 阶段被成功执行,就宣告当前分支事务提交成功。

  3. Cancel 阶段的工作是事务回滚(Rollback),它类似于事务的 Rollback 操作。在这个阶段中,你需要通过业务代码,对 Confirm 阶段执行的操作进行人工回滚。

TC、TM、RM

  1. TC 全称是 Transaction Coordinator,TC 扮演了一个事务协调者的角色,负责协调全局事务的提交和回滚,并维护全局和分支事务的状态。

  2. TM 全称是 Transaction Manager,它是事务管理器,主要作用是发起一个全局事务,对全局事务的提交和回滚做出决议。

  3. RM 全称是 Resource Manager,它是资源管理器,向 TC 注册分支事务并上报事务状态,同时负责对当前分支事务进行提交和回滚。每一个分支事务都是全局事务的参与者,这些分支事务的所属应用扮演了 RM 的角色。

四、分布式事务 - TCC 事务

TCC 事务可以分为本地化 TCC 事务和远程式 TCC 事务两种实现方式。

4.1、本地化 TCC 事务

4.1.1 架构设计

本地化 TCC 是将 TCC 的功能嵌入客户端 SDK 中,可以随着应用代码去中心化部署,后端共享数据库存储事务日志。客户端通过周期性上报心跳信息到远端的 TC 集群,集群通过心跳信息判定该客户端是否存活,当机器出现失联情况时能自动完成故障转移,使得失联机器的事务得以继续执行。

架构设计如下图。

4.1.2 事务调用流程

这里通过扣减资源链路来说明下本地化 TCC 事务调用流程,主要由两个步骤组成:

  • Step1 : Domain 服务(事务发起方)通过 RPC 调用下游服务锁定资源

  • Step2 : 锁定资源成功后落库生成初始化单据

由于网络的不确定性 RPC 可能会出现超时情况,需要引入分布式事务保证原子性。对于扣减资源流程抽象出 TCC 三个方法的逻辑如下:

在单据占用链路本地化 TCC 的执行流程为:

  1. 用户确认操作后会在调用 Try 方法之前向数据库中插入一条事务日志开启 TCC 事务,状态为初始化(init)

  2. 调用下游服务实现的 Try 方法锁定资源,落库初始化状态的订单

  3. 如果步骤 2 未抛异常,修改事务日志状态为待提交(to_submitted),进而调用 Confirm 方法;如果抛异常修改事务日志状态为待回滚(to_rolledBack),调用 Cancel 方法

  4. 如果 Confirm 抛异常,修改事务日志重试时间,等待下次重试;Cancel 同理

  5. 反复执行重试,一旦重试成功则删除日志,或者触发最大重试次数限制或者事务超时限制熔断告警,人工介入处理异常事务。

需要注意两个问题:

  1. 由于 RPC 服务不可避免会出现超时异常,因此下游服务提供的三个接口需要保证幂等性

  2. 由于修改事务日志状态的操作可能出现数据库异常,在极端情况下会导致状态修改失败,造成重复补偿,因此 Confirm 和 Cancel 需要进行状态幂等。

4.1.3 故障转移

由于本地化 TCC 模式是类两阶段提交,对于极端情况如机器出现宕机,事务有可能长时间处于中间状态得不到执行,导致服务间出现数据不一致,产生业务影响(极端情况下需要修复大量数据),因此引入故障转移策略是必要的,本地化 TCC 一般采用的是事务托管机制。

为了讲清楚托管机制,在时间轴上我划定三个时间点,分别为 T1、T2 和 T3:

  1. T1 是客户端心跳失联的时刻

  2. T1 到 T2 这段是集群判定机器失活的最大失联时间,比如最大失联时间为 1min

  3. T2 这个时间点,集群会将失联机器的已存在事务托管给另一台存活机器执行

  4. T3 失联机器恢复心跳,会拉取自 T3 之后的新事务日志继续执行

TC 集群一般会通过 zk/nocas/etcds 等注册中心选举出 Leader 机器执行心跳检查任务,针对失活的客户端会一般优先选择同地域的机器进行托管。托管请求会通过 RPC 请求发送至客户端,客户端接收后刷新自己维护的托管列表,同时客户端侧会有定时任务,周期性拉取托管列表。


4.2、远程式 TCC 事务

4.2.1、架构设计

本地化 TCC 接入简便、改造成本低、去中心化等优势,但是它的架构设计也存在局限性:

  1. 本地化 TCC 的功能是 SDK 内嵌的,导致重试流量无法分散到其他节点

  2. 本地化 TCC 目前不支持事务传播机制,仅能支持事务的单层调用

为了解决上述问题,采用中心化集群协调事务、支持跨多应用的分布式事务解决方案——远程式 TCC。

相较于本地化 TCC,远程式 TCC 将一个分布式事务分为一个全局事务加若干个分支事务(拆分思想:将大事务拆分为一个个小的事务),采用中心化部署的协调者集群完成全局事务的提交、回滚和分支事务的补偿操作。

这里还是扣减资源流程说明下,需要调用两个 TCC 参与方——下游依赖服务 1 和依赖服务 2(RM),整个远程式 TCC 事务的运行流程为:

  1. Domain 服务(事务发起方)先向 TC 集群注册并开启一个全局事务,再通过 RPC 调用两个参与方的 Try 方法

  2. 两个参与方首先向 TC 集群注册分支事务,然后分别调用自己的 Try 方法进行资源锁定操作

  3. 事务发起方感知到业务方法调用没有异常,则向 TC 集群请求提交全局事务;如果抛出异常则要求回滚全局事务

  4. TC 集群查询出所有分支事务,按照注册时间顺序回调客户端完成相应的补偿分支,提交则回调 Confirm,回滚则回调 Cancel

对发起方来说,业务操作仅需要调用两个下游服务的 Try 方法完成资源,剩下的提交或者回滚操作是由框架自动完成的,如果分支补偿出现异常,则进入异步重试。

4.2.2、乱序防御

通过上面的介绍可知,由于远程式 TCC 下应用是分布式部署的,通过 RPC 进行网络通信,不可避免会存在由于网络环境造成的请求乱序问题,比如二阶段跑在了一阶段前面的情况,常遇到的问题可分为两类:空回滚、Try 方法悬挂。

4.2.2.1、空回滚

什么是空回滚:

  1. 发起方的调用由于超时异常,导致全局事务回滚

  2. 但此时分支事务已经注册,但 Try 方法尚未执行

  3. 客户端此时先收到了 TC 集群的回调 Cancel 请求

  4. 对参与方 A 来说 Try 尚未执行,先执行了 Cancel,造成回滚一个没有被预留过的资源

解决方案是需要框架能识别出空回滚方法,并直接忽略它,不回调真正的 Cancel

4.2.2.2、Try 方法悬挂

什么是 Try 方法悬挂:

  1. 是空回滚的衍生现象

  2. 当客户端正常忽略了空回滚之后,Try 方法继续执行锁定资源,导致该资源长时间被锁定得不到释放

解决方案要求框架能识别出悬挂的 Try 方法并忽略执行。

对于上面两个问题,如果要求在上下游业务代码中自定义处理这些乱序问题将变得非常麻烦。TCC 框架一般都会集成相应的解决方案,一般是通过 AOP 切面记录操作日志的方法,来统一解决乱序问题。请求操作日志由全局事务 XID、分支事务 Branch_ID、状态组成,通过这些日志我们可以判断某个事务请求的执行顺序,从而规避请求乱序问题。

下面的通过流程图介绍事务日志表是如何工作的:

  1. 正常的流程为 Try 方法达到,事务控制表记录为空,插入一条记录状态为初始化,之后 Cancel 请求达到发现事务控制表里有记录,状态为初始化,因此继续执行 Cancel

  2. 如果 Cancel 请求到来发现事务控制表里没有自身 XID 的记录,说明自己是一个空回滚,需要新增一条记录,用于防止后续的 Try 悬挂

  3. 如果 Try 方法到达,发现事务控制表记录不为空,意识到自己是一个悬挂的 Try 方法,因此忽略执行

4.2.3、故障转移

远程式 TCC 的故障转移需要分客户端和集群两方面来看。

对于客户端来说一旦发生宕机等异常,TC 集群会转而向其他客户端机器进行重试,理论上只要应用下存在正常的机器,事务最终都会重试成功。

由于 TC 集群侧每一台机器均负责一部分事务的重试操作,因此一旦发生机器宕机,需要将未完成的重试事务托管给

其他机器继续执行,类似于本地 TCC 下的事务托管机制。

TC 集群侧一般采用心跳表维护机器心跳,Leader 节点负责检查机器的存活与否,对于失联的机器会选择一台存活机器告知其托管宕机机器未完成的重试事务。

TC 集群机器宕机时刻,正在发往该机器的全局事务提交请求可能会丢失,此时需要客户端会向其他节点重试,该类重试操作需要 TC 集群的事务日志操作保证幂等,确保全局事务状态提交完成。

五、小结

TCC 的优劣:

  1. 优:

    一阶段写隔离,二阶段没有锁性能好

    不是真正的业务回滚,cancle 只是释放了被 try 占用的资源


  1. 劣:

    需要一定的业务代码改造,把串行的业务逻辑拆分成 Try-Confirm-Cancel 三个不同的阶段执行

    需要引入分布式事务框架,比较重,需要考虑分布式事务集群短暂不可用时的降级方案

对于 TCC 事务,需要根据微服务上下游依赖情况,把串行的业务逻辑拆分成 Try-Confirm-Cancel 三个不同的阶段执行,需要考虑到下游资源的锁定操作、然后根据根据事务的执行情况确定提交或回滚操作。

需要特别注意的是幂等性,接口幂等性是保证数据一致性的重要前提。因此对于单据模型包含 status、stage 字段的业务场景天然更适合些。


本文抛砖引玉,如有表述不清楚的地方欢迎指出讨论。

发布于: 刚刚阅读数: 3
用户头像

Monin

关注

道术器用 2022-08-16 加入

还未添加个人简介

评论

发布
暂无评论
谈谈分布式事务_分布式事务_Monin_InfoQ写作社区