分布式事务,全都是坑!

170 阅读7分钟

事务简介

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有一个事务中的一系列的操作要么全部成功,要么一个都不做。
举个例子:我们转账的过程就是一个事务,张三给李四转10元首先要从张三银行卡里扣10元,然后给李四银行卡里增10元,从我们数据库的角度来看就是两条sql语句。这个过程必须是同时成功或者同时失败,不能出现张三钱扣了但是李四没收到的情况。

本地事务

在同一个数据库连接中执行的事务就是本地事务。代码如下:

//获取数据库连接
Connection connection = new PgConnection();
//开启事务
connection.setAutoCommit(false);
try {
    //从张三银行卡扣余额
    //给李四银行卡添加余额
} catch (Exception e){
    //事务回滚
    connection.rollback();
}

分布式事务场景

跨库事务,同时操作多个数据库

操作多个数据库.png
跨应用事务,微服务架构

跨应用事务.png
总结
上述的两个场景其实都是因为操作数据库的过程不在一个连接中。

分布式事务解决方案

两阶段提交协议(2PC)

2PC.png
如图在分布式架构下,由Business服务去调用order服务和store服务,2PC的工作原理:

  • 阶段一:order服务和store服务进行对应的sql处理但是不提交事务,会将当前结果给到事务管理器(TM)。
  • 阶段二:事务管理器(TM)根据各个资源(RM)的处理结果来选择是提交事务还是回滚事务。如果所有RM的结果是成功的,那么TM会通知所有RM去提交,如果有一个RM失败了,TM就去通知所有RM进行回滚。

2PC问题:

  • 同步阻塞:第一阶段会持续获得数据库连接,直到二阶段完毕才会释放资源。
  • 单点故障:TM出现故障,RM就会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
  • 数据不一致:在二阶段commit提交时,store服务出现故障,那么就会造成order服务下单成功,store服务扣减库存失败问题。

Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。

Seata的三大角色

  • TC (Transaction Coordinator):事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager):事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager):资源管理器

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

Seata中的AT模式

Seata AT模式的核心是对业务无侵入,是一种改进后的两阶段提交。

一阶段本地事务过程:

AT一阶段提交过程 (1).png
已UPDATE user SET name = "李四" where name = "张三" 为例

  1. 解析sql
  2. 根据更新语句生成查询sql,select name from user where name = "张三",这个结果就是更新前的数据,也就是原始数据
  3. 执行刚刚例子的更新语句
  4. 查询更新后的数据,也是执行select name from user where name = "张三"。
  5. 把刚刚第2步和第4步得到的结果插入回滚日志中。
  6. 提交前,向 TC 注册分支,申请刚刚操作的数据id为全局锁。
  7. 提交事务
  8. 向 TC 汇报结果

二阶段-提交过程

AT二阶段提交.png

  1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
  3. 提交事务

二阶段-回滚过程

AT二阶段回滚.png

  1. 通过 XID 和 Branch ID 查找到相应的 UNDO_LOG 记录
  2. 数据校验:拿 UNDO_LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改(无法正常回滚要人工干预处理)
  3. 根据 UNDO_LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句
  4. 提交事务
  5. 向 TC 汇报结果

优点

  1. 实现简单,基本0代码耦合,只需要在TM的地方加一个全局事务注解即可。
  2. 性能优秀,第一阶段则释放本地锁
  3. TC可单独集群部署,架构清晰,高可用保证

Seata中的TCC模式

TCC 是一种侵入式的分布式事务解决方案,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理。它的全称为 Try-Confirm-Cancel,他们的具体含义如下:

  1. try:对业务资源的检查并预留
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放

三阶段示例

以下单扣减库存为例:

未命名文件.png

AT与TCC的区别

AT

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

TCC

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。

TCC常见问题--空回滚

什么是空回滚

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。举个例子,比如扣减库存在TCC中的try操作是冻结库存,假如由于网络抖动try没有执行成功,触发了回滚,则去补偿进行扣减冻结库存,最终会出现超卖情况。

解决方案

Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

TCC常见问题--幂等

什么是幂等

因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口

解决方案

同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status。二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

TCC常见问题--悬挂

什么是悬挂

悬挂是tcc没有严格按照事务链路去调用而产生的,在try阶段出现了网络抖动try暂时没有执行,这个时候触发了回滚操作,整个事务已经进行了回滚,这个时候执行try的网络恢复了,try又成功执行就会资源预留从而形成悬挂。

解决方案

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段Try 方法执行成功。