十二、TSF分布式事务管理

十二、TSF分布式事务管理

随着微服务的增多,业务上不可避免的出现了多个服务(不同服务可能使用的是不同的数据库) 之间协作的问题(比如:跨行转账,2个账户数据在不同的地方,一个账户扣钱,另一个账户收 钱,这2个操作必须同时成功或者失败),这时候多个服务涉及的操作要么同时都成功,要么同 时都失败,这就是分布式事务。接下来我们看一下TSF的分布式事务管理功能。

学习目标

通过本文的学习,你将可以:

  • 了解本地事务与分布式事务
  • 了解分布式事务典型场景
  • 掌握二阶段提交、三阶段提交和TCC
  • 掌握使用TSF管理和监控分布式事务

第一章 本地事务与分布式事务简介

1.1 事务简介
  • 事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成。

  • 事务属性通常称为ACID特性,分别为:

    image-20211203164433250

  • 事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务由事务开始(begin transaction)和事务结束(commit transaction或 rollback transaction)之间执行 的全体操作组成,通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起。

  • 一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。

  • 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。

  • 一致性(consistency):事务在完成时,必须使所有的数据都保持一致状态,即保持数据的完整性(在存储或传输信息的过程中,原始的信息不能允许被随意更改)

  • 隔离性(isolation):由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。

  • 持久性(durability):持续性也称永久性(permanence),指一个事务一旦提交,它对数据库 中数据的改变就应该是永久性的。

1.2 本地事务
  • 大多场景下,我们的应用都只需操作单一的数据库,这种情况下的事务称之为本地事务(Local Transaction)。

  • 本地事务的ACID特性是数据库直接提供支持。

  • 本地事务应用架构如下所示:

    image-20211206111426768

  • 在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。

  • 很多java应用都整合了spring,并使用其声明式事务管理功能来完成事务功能。一般使用的步骤如下:

    • 配置事务管理器。spring提供了一个PlatformTransactionManager接口,其有2个重要的实现类:
      • DataSourceTransactionManager:用于支持本地事务,其内部通过操作java.sql.Connection来开启、提交和回滚事务。
      • JtaTransactionManager:用于支持分布式事务,其实现了JTA规范,使用XA协议进行两阶段提交。需要注意的是,这只是一个代理,我们需要为其提供一个JTA provider,一般 是Java EE容器提供的事务协调器(Java EE server’s transaction coordinator),也可以不 依赖容器,配置一个本地的JTA provider。
    • 在需要开启的事务的bean的方法上添加@Transitional注意,可以看到,spring除了支持本地事务,也支持分布式事务,下面我们先对分布式事务的典型应用场景进行介绍。
1.3 分布式事务
  • 分布式事务

    • 顾名思义就是在分布式环境下运行的事务。
    • 对于分布式事务来说,事务的每个操作步骤是运行在不同机器上服务的。
    • 分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作, 提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)。
  • 在现如今的大型互联网平台中,基本上都是采用分布式的SOA架构,所以分布式事务是非常常见的。比如一个电商平台的下单场景,一般对于用户下单会有两个步骤,一是订单业务采取下订单操作,二是库存业务采取减库存操作,但在大型电子商务平台上这两个业务一般会运行在不同的 机器上,这就是一个典型的分布式事务场景。还有一个常见的场景就是支付宝向余额宝转账,而支付宝和余额宝不是一个系统,怎么保证这两个系统之间的一致性就是分布式事务所关注的问题。

第二章 分布式事务典型场景

2.1 跨库事务
  • 跨库事务:一个应用某个功能需要操作多个库,不同的库中存储不同的业 务数据。

  • 在相对复杂的业务场景中,一个业务可能同时操作9个库。

  • 下图为一个服务同时操作2个库的情况:

    image-20211206113454881

2.2 分库分表
  • 通常一个库数据量较大或者预期未来的数据量较大时,会进行水平拆分,也就是分库分表。

    • 如下图,将数据库B拆分成了2个库:

      image-20211206113618989

  • 对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于sql:insert into user(id,name) values (1,”tianshouzhi”),(2,”wangxiaoxiao”)。这条sql是操 单库的语法,单库情况下,可以保证事务的一致性。

  • 但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数 、据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。

2.3 服务化(SOA)
  • 某应用同时操作9个库业务逻辑非常复杂,对于开发人员是极大的挑战,应 拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过 RPC框架来进行远程调用,实现彼此的通信。

  • 下图为3个服务之间彼此调用的架构:

    image-20211206114401308

  • Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B 又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。

  • 小结:上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方 案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要 求快速响应的业务,是无法接受的。

第三章 二阶段提交、三阶段提交和TCC

3.1 二阶段提交
  • 两阶段提交协议(Two Phase Commit)不是在XA规范中提出,但是XA 规范对其进行了优化,因此统一放到这里进行讲解。而从字面意思来理解 ,Two Phase Commit,就是将提交(commit)过程划分为2个阶段(Phase) :

    image-20211206120547855

  • 阶段1:

    • TM(Transaction Manager,事务管理器)通知各个RM(Resource Manager,资源管理器)准备提交它们的事务分支。如果RM判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给TM肯定答复;要是发生了其他情况,那给TM的都是否定答复。 在发送了否定答复并回滚了已经的工作后,RM就可以丢弃这个事务分支信息。
    • 以mysql数据库为例,在第一阶段,事务管理器向所有涉及到的数据库服务器发出prepare”准备提交”请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成”可以提交”,然后把结果返回给事务管理器。
  • 阶段2

    • TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都 prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare失败的话,则TM通知所有RM回滚自己的事务分支。
    • 以mysql数据库为例,如果第一阶段中所有数据库都prepare成功,那么事务管理器向数据库服务器发出”确认提交”请求,数据库服务器把事务的”可以提交”状态改为”提交完成”状态,然后返回应答。如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把”可以提交”的事务回撤。
  • 二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

    1. 同步阻塞问题
    2. 单点故障
    3. 数据不一致
3.2 三阶段提交
  • 三阶段提交(3PC),是二阶段提交(2PC)的改进版本。

  • 与两阶段提交不同的是,三阶段提交有两个改动点。

    • 引入超时机制。同时在协调者和参与者中都引入超时机制。
    • 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
  • 三阶段提交有:CanCommit、PreCommit、DoCommit三个阶段。

  • 三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。

  • CanCommit阶段:3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送 commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应

  • PreCommit阶段:根据反应情况来决定是否可以继续事务的PreCommit操作

    • 如果响应全部yes,则进行事务预执行
    • 如果有一个响应为no,或者等待超时,那么就中断事务,发送中断请求
  • DoCommit阶段:该阶段进行真正的事务提交,也可以分为以下两种情况:

    • 执行提交
    • 中断事务
  • 对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败),在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。 PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的

3.3 TCC
  • TCC 模式是应用层的两阶段提交:
    • 先 try, 再执行 confirm;若 try 失败,则执行cancel。
  • TCC 模式解决的问题包括但不限于:
    • 数据库的两阶段并发性能差。
    • 数据库对XA协议的支持可能不完善。
    • 在分布式架构下,主业务系统并非直接操作数据库,而是调用从业务系统的服务接口 。
  • TCC 模式是一种补偿性分布式事务。其中TCC 三个字母分别对应Try、Confirm、Cancel三种操 作。其中Try 阶段预留业务资源,Confirm 阶段确认执行业务操作,cancel 阶段取消执行业务操作。TCC 模式解决了跨服务操作的原子性问题,对数据库的操作是一阶段提交,性能较好。因此, TCC 模式是现今被广泛应用的一种分布式事务模式。
  • Try: 尝试执行业务
    • 完成所有业务检查(一致性)
    • 预留必须业务资源(准隔离性)
  • Confirm:确认执行业务
    • 真正执行业务
    • 不作任何业务检查
    • 只使用Try阶段预留的业务资源
    • Confirm操作要满足幂等性
  • Cancel: 取消执行业务
    • 释放Try阶段预留的业务资源
    • Cancel操作要满足幂等性

第四章 分布式事务应用持续开发指南

4.1 名词解释

image-20211206155714357

  • 事务:事务是指作为单个逻辑工作单元执行的一系列操作。事务的执行有一致性。同一个事务只能同时被操作或不被操作。

  • 主事务:主事务是事务的发起者。同一个事务只能有一个主事务。

  • 子事务:子事务是同一个主事务下的分支。

  • 事务管理器:事务管理器是一个协调分布式事务、管理事务执行状态的独立服务。

4.2 开发指南-场景描述
  • 场景描述:

    • 两台虚拟机,分别命令为node1、node2,每台虚拟机上都安装了MySQL数据库,现在向node1上的数据库更新用户账户信息,向node2上的数据库新增用户消费信息。

    • 基本流程如下:

      image-20211206160010473

  • 接下来以如上场景来说明TCC的事务开发步骤,以SprigCloud项目为例

  • 基本开发流程如上图

4.2.1 开发指南-配置TCC事务
  • 配置TCC事务

    • 通过Maven引入依赖,在项目的pom文件添加下述配置项:

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.tencent.tsf.transaction</groupId>
      <artifactId>tsf-transaction-core</artifactId>
      <version>0.0.5-SNAPSHOT</version>
      </dependency>
4.2.2 开发指南-添加注解
  • 启用Tcc事务在Spring Cloud的启动类加入注解@EnableTcc:

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableTcc
    public class App {
    public static void main(String[]args){
    SpringApplication.run(App.class);
    }
    }
4.2.2 开发指南-添加注解(续)
  • 在事务的入口函数上添加@TsfTcc注解,TsfTcc注解有如下属性:
    • serviceName:事务所属的服务名,必选。
    • type:事务类型,TransactionType.ROOT表示主事务,TransactionType.BRANCH 表示子事务,默认值为Root,可选。
    • timeout_ms:事务的超时时间,属性可选,默认为60秒,可选。
    • confirmMethodName:confirm方法名,主事务可选,子事务必选。
    • cancelMethodName:cancel方法名,主事务可选,子事务必选。
    • autoRetry: 事务超时后,是否由服务器托管继续自动重试。
  • 建议注解加在接口定义的方法上
4.2.3 开发指南-定义主事务
  • 定义主事务

    • 主事务函数只需要定义一个事务入口函数即可。type选择Root类型;函数抛出 Throwable异常,返回值没有特殊要求;建议业务将所需的参数都封装成一个对象, 只使用这一个对象作为入参即可(所有的参数必须实现Serializable接口),一个主事务函数定义的样例如下:

      1
      2
      3
      4
      5
      6
      7
      public class MainTransaction {

      @TsfTcc(serviceName = "myTcc", type = TransactionType.ROOT, timeout_ms = 60000)
      public String beginTcc(MyParams params) throws Throwable {
      //这里写业务逻辑,调用子事务函数
      }
      }
4.2.4 开发指南-定义子事务
  • 定义子事务
    • 一个完整的子事务需要定义3个方法:Try,Confirm,Cancel。
    • 其中只有Try方法才需要加TsfTcc注解,在TsfTcc标签中指定对应Confirm和Cancel方法名即可。
    • 子事务函数定义约束如下:
      • Try,Confirm,Cancel的前两个参数必须为String txId和long branchId(在主事务调用子事务的时候,这两个参数分别为null和0即可),其中txId为主事务Id,全局唯一;branchId为子事务Id,用于区分子事务的父子关系和子事务之间的调用顺序。
      • Try函数返回Throwable异常;Try函数的返回值为void,Tcc通过异常返回失败结果。
      • Confirm和Cancel函数返回值为boolean类型,操作成功返回true,操作失败返回False。
4.2.6 开发指南-事务函数样例
  • 一个子事务函数定义的样例如下:
1
2
3
4
5
6
7
8
9
public interface SubService{

@TsfTcc(serviceName = "subService", type = TransactionType.BRANCH, confirmMethodName = "subConfirm", cancelMethodName = "subCancel")
public void subTry(String txId, long branchId, MyParams Params) throws Throwable;

public boolean subConfirm(String txId, long branchId, MyParams Params) throws Throwable;

public boolean subCancel(String txId, long branchId, MyParams Params) throws Throwable;
}
  • 主事务与子事务结合样例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MainTransaction{

    SubService subService;

    @TsfTcc(serviceName = "myTcc", type = Transaction.ROOT, timeout_ms = 600000)
    public String beginTcc(MyParams params) throws Throwable{
    subService.subTry(null, 0, params);
    }
    }
4.3 分布式事务应用程序开发常见问题
  • 如何保证全局事务的正确执行
  • 子事务如何访问外部服务
  • 业务需要处理什么类型的异常
  • 业务什么时候会进行回滚
  • 分布式事务的超时、重试机制
  • 如何保证全局事务的正确执行
    • Tcc通过主事务函数作为入口,协调控制多个跨库/跨服务的子事务的执行流程。因此,Tcc保证的是在各个子事务之间的事务特性,一次性Confirm或者Cancel所有的子事务。然而Tcc不能够保证每个子事务内部的事务性质,因此在使用Tcc时的最佳实践应该是在每个子事务内部需要自己执行本地事务
  • 子事务如何访问外部服务
    • 在SpringCloud中,可以通过集成FeignClient去访问外部的服务,再次以上述的子事务为例, 假如子事务需要调用一个代金券服务的相关接口,则配置如下:
      • 在接口上增加了如下注解:
        • @FeignClient(value = “couponService”),value的值为外部服务在服务注册中心注册的服务名
        • @RequestMapping(value = “/api/v6/data/couponService”),value的值为外部服务的根URL
      • 在方法上增加了如下注解:
        • @RequestMapping(value = “/try”, method = RequestMethod.POST),value的值为外部 服务的api的URL以及请求方式。
  • 业务需要处理什么类型的异常
    • 主事务函数中主要需要捕获两个异常:TransactionCancelledException异常表示事务失败 cancelled,TransactionTimeoutException异常表示事务超时。运行时业务抛出的异常会被包装在这两个异常中,可以通过getCause()方法获取业务真正的运行时异常。
  • 业务什么时候会进行回滚
    • 当前业务只有在子事务Try失败的时候进行cancel操作,当进入confirm阶段之后,confirm失败只会继续重试confirm,直到超时为止。
  • 分布式事务的超时、重试机制
    • 用户可以在主事务的注释中定义超时时间,默认超时时间为60秒,此时的超时时间为整个事务的超时时间。当事务超时后,事务管理器对事务进行接管,以每5秒一次的重试频率自动触发重试,重试频率逐渐增长,直到600秒。用户可以在控制台上控制中断这一重试过程,也可以手动触发重试。7天后,重试自动终止。对于未超时的事务,Try 阶段自动重试3次,3次重试不成功自动触发 Cancel 进行回滚。对于 Confirm 或者 Cancel 阶段始终不能执行成功的情况,会重试直到超时。超时后由事务管理器接管。

第五章 使用TSF管理和监控分布式事务

5.1 使用TSF管理和监控分布式事务
  • 腾讯云TSF框架,提供基于TCC(Try-Confirm-Cancel)的事务方案,解决跨服务的一致性问题。
    • 第一阶段:主业务服务分别调用所有从业务服务的 try 操作,并在活动管理器中记录所有从业务服务。当所有从业务服务 try 成功或者某个从业务服务 try 失败时,进入第二阶段
    • 第二阶段:活动管理器根据第一阶段从业务服务的 try 结果来执行 confirm 或 cancel 操作。如果第一阶段所有从业务服务都 try 成功,则协作者调用所有从业务服务的 confirm 操作,否则,调用所有从业务服务的 cancel 操作。(confirm操作,需要业务满足幂等)
  • 前面我们已经介绍了二阶段提交,三阶段提交以及TCC提交方式,在TSF中使用的是TCC分布式事务方案
5.1 使用TSF管理和监控分布式事务(续)
  • 在第二阶段中,confirm 和 cancel 同样存在失败情况,所以需要对这两种情况做异常处理以保证数据一致性。

    • Confirm /Cancel失败:会一直重试对应的Confirm或者Cancel操作直到成功,因此需要保证业务是幂等的。

    image-20211206164733490

  • try 阶段:尝试执行,完成所有业务检查(一致性),预留必需业务资源(准隔离性)。

  • Confirm 阶段:确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源, Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。

  • Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。

第六章 TSF分布式事务最佳实践

6.1 TSF分布式配置
  • 腾讯云TSF框架,提供精准掌握事务流程,包括:

    • 不同状态的事务筛选

    • 事务IP、服务名、方法名

    • 子事务列表

    • 参数列表

      image-20211206165720423

  • 在TSF控制台中提供了“事务管理”功能:

    1. 打开TSF控制台。
    2. 点击【事务管理】功能。
    3. 控制台上选择需要查看的事务时间段,选择需要查看的事务状态,并填写关键词。关键词可 以选择事务相关的服务名、方法名称。
    4. 点击查询,将展示事务id、起始节点、超时时长等等相关信息,查询界面如上图。
6.1 TSF分布式配置(续)
  • 查看子事务

    image-20211206170132844

  • 点击上一页中的主事务id,可以查看主事务下的子事务运行状态,包含子事务的方法名、服务名、 节点、状态、起止时间等信息。每一次子事务发起的请求都会记录在子事务列表中,包含子事务的确认、取消、重试等等。当子事务发生重试时,可以查看子事务重试是由主事务触发、由事务管理器启动发起重试还是由用户在控制台上进行手动触发。

6.1 TSF分布式配置(续)

image-20211206170234965

  • 点击子事务列表右侧的“查看事务参数”,可以查看每条子事务的参数内容,如上图:
6.2 TSF分布式事务最佳实践

image-20211206170318675

  • 已超时事务处理:
    • 当主事务的处理时间超过了SDK中设置的超时时限后,认为事务已处于超时状态。针对超时事务, TSF 提供了两种重试渠道:自动重试和手动重试。当事务超时时,框架自动重试,重试时间间隔 从5秒一次倍速递增直到600秒一次,当超时时间超过7天后,自动停止重试。手动重试是指用户可以在控制台上通过点击的方式触发重试
  • 操作步骤:
    1. 打开TSF控制台。
    2. 点击【事务管理】功能。
    3. 批量选择需要进行手动重试或需要停止/触发自动重试的事务,点击启动操作,如上图。

思考题

  • 开发TSF分布式事务的主要流程是怎么样的?
  • 分布式事务的主要场景有哪些?