通俗易懂分布式事务之2PC、3PC、Seata AT模式、Seata TCC模式

通俗易懂分布式事务之2PC、3PC、AT、TCC

单机服务事务提交回滚操作是需要拿到Connection对象,调用提交commit方法或者rollback方法回滚的,例如下面操作

java 复制代码
Connection conn = DriverManager.getConnection(...);
try{
  con.setAutoCommit(false);
  Statement stmt = con.createStatement();
  //1 or more queries or updates
  con.commit();
}catch(Exception e){
  con.rollback();
}finally{
  con.close();
}

要想提交或者回滚,必须拥有Connection对象,然而在分布式环境,jvm都是不同的,自然就拿不到其他服务的Connection对象,所以在分布式环境,我们无法保证原子性。因此分布式事务就需要另寻出路。

1. 术语

  • 全局事务:分布式环境,操作涉及很多服务,全局事务能保证这些服务的原子性,要提交就全部服务都会提交,要回滚全部服务都会回滚
  • 分支事务:分支事务建立在全局事务当中,是属于单个服务的事务

2. 2PC

2PC 即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase)

与本地事务的区别就是 会加入一个事务协调者的角色,这个事务协调者控制整体的事务提交

2.1 流程

  1. PreCommit阶段
    事务协调者开启全局事务,参与者向事务协调者注册分支事务,然后走自己的业务代码,在这过程中如果发生了异常参与者会发送信息到事务协调者我异常了,需要全部回滚。如果一切正常,告诉事务协调者我可以提交了,等待事务协调者发送commit指令
  2. Commit阶段
    事务协调者向所有分支事务发送commit指令,释放数据库资源

在这里举个具体的例子

我们有两个服务:订单服务、库存服务

用户需要买东西,首先要创建订单,创建订单前需要去锁定库存,然后再去创建订单。

复制代码
1. 事务协调者开启全局事务
2. 库存服务开启事务,向事务协调者注册一个分支事务
3. 库存服务锁定库存
4. 订单服务开启事务,向事务协调者注册一个分支事务
5. 订单服务创建订单
6. 事务协调者进行全局提交事务
7. 库存服务提交事务
8. 订单服务提交事务
9. 全局事务完成

在任何一个流程中异常,事务协调者都会发起全局回滚事务,这种方式,在全局事务完成前,Connection对象都不会释放,因为你释放了你就无法控制了,缺点很明显,如果订单服务需要处理很久,库存服务Connection对象都不会释放,一直占用着,这种是强原子性的很浪费资源

2.2 实现2PC

XA协议,是X/Open组织提出的跨异构技术实现2PC的接口标准。

使用XA协议首先前提就是,需要关系型数据库支持,目前主流数据库:

  • MySQL: InnoDB 引擎支持 XA 事务,并实现了 XA 接口。
  • PostgreSQL: 从版本 8.0 开始支持 XA 事务,通过插件接口实现。
  • Oracle: 支持 XA 事务,使用 OracleXADataSource 提供 XA 接口。
  • SQL Server: 支持 XA 事务,使用 MSDTC 提供 XA 接口。
  • DB2: 支持 XA 事务,使用 DB2 Universal JDBC 驱动器提供 XA 接口。
  • Sybase ASE: 支持 XA 事务,使用 JConnect 提供 XA 接口。

2.2.1 MySQL实现流程:

  1. START

第一步开启XA事务

库存DB:

shell 复制代码
XA START 'xid'
UPDATE  product SET num = num - 1 where id = 100
XA END 'xid'

订单DB:

shell 复制代码
XA START 'xid'
INSERT INTO order values(xxx)
XA END 'xid'
  1. PREPARE

第二步,准备就绪,等待事务协调者同意我提交

库存DB:

shell 复制代码
XA PREPARE 'xid'

订单DB:

shell 复制代码
XA PREPARE 'xid'
  1. commit

全部提交

库存DB

shell 复制代码
XA COMMIT 'xid'

订单DB:

shell 复制代码
XA COMMIT 'xid'

2.2.3 seata支持XA

XA协议JDK接口定义:javax.sql.XADataSource

seata框架支持XA协议,seataXA模式文档:https://seata.apache.org/zh-cn/docs/v1.6/dev/mode/xa-mode/

seata官方XA模式demo:
https://github.com/apache/incubator-seata-samples/tree/master/xa-sample/springboot-feign-seata-xa

2.3 2PC缺陷

  • 强一致性,每个分支事务得等待所有分支事务都准备好,才能提交释放
  • 协调者发生故障。分支事务会一直阻塞下去。

2. 3PC

三阶段提交协议(3PC)主要是为了解决两阶段提交协议的阻塞问题,2pc存在的问题是当协作者崩溃时,参与者一直阻塞。

与两阶段提交不同的是

  1. 引入超时机制
  2. 在最前面引入CanCommit阶段,为了防止参与者服务不可用,询问各个服务能不能开启事务

2.1 流程:

  1. CanCommit阶段
    事务询问阶段,协调者向参与者发送CanCommit请求,询问是否可以执行事务提交操作。如果有参与者返回NO,就不进行下一步操作了
  2. PreCommit阶段
    进入PreCommit后就和2PC一样了,区别就是有超时机制
  3. Commit阶段
    提交,释放数据库资源,与2PC一样

2.2 缺点

还是要等到全局事务完毕资源才释放,占用资源大

3. Seata AT模式

Seata AT模式文档:https://seata.apache.org/zh-cn/docs/v1.6/dev/mode/at-mode

Seata AT模式的核心是对业务无侵入,是一种改进后的2PC

主要的实现是,在每个服务的数据库中新建一张undo_log表,结构如下:

content: 更改后的内容,

rollback_info: 回滚的内容

每个分支事务执行SQL都会解析SQL,保存content,rollback_info,插入到undo_log表中。如果全局事务通知需要回滚,去通过对比content和解析rollback_info,执行sql达到回滚的效果,如果全局事务通知全局事务成功,异步删除undo_log的记录。

这种方式不需要等待全局事务的提交才提交,能解决2PC、3PC资源占用的问题,实际就是异常就去补偿的思想

3.1 流程

  1. 事务入口会开启全局事务,分支事务注册事务(非入口事务通过传递xid来判断是加入哪个事务)
  2. 分支事务执行SQL之前会解析SQL,生成前置镜像
  3. 解析出SQL如果是更新修改数据这种情况,seata会锁住更新修改的数据,另一个全局事务进来想要更改更新修改的数据就需要等待全局事务结束才能进行更改对应着seata的lock_table表
  4. 执行SQL
  5. 根据SQL生成后置镜像,如果到时候需要回滚,直接执行后置镜像
  6. 插入数据到undo_log表
  7. 提交本地事务
  8. 如果所有分支事务都是正常的,就释放锁,然后异步删除所有的分支事务undo_log的记录
  9. 如果发生异常,所有事务都对前置镜像进行解析,生成SQL,执行回滚操作
  10. 如果发生异常,需要回滚,后置镜像当中的值,不等于当前值,代表有其他业务一样的更改了这行数据,这时候就需要人工去处理了(往往如果设置了数据库字段update_time自动更新时间会很容易导致这个情况出现,不能用数据库的自动更新时间,自动更新时间得去到业务代码实现里)

3.2 缺点

seata AT模式,事务是软状态需要考虑数据最终一致性,性能相对来说不是那么高,得去加锁,得动态去解析SQL插入数据库增加了和数据库的交互

4. Seata TCC模式

Seata TCC模式文档:https://seata.apache.org/zh-cn/docs/v1.6/dev/mode/tcc-mode

TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),TCC模式对代码的入侵很大,但它的性能很好,还可以便捷的解决、空回滚、幂等、悬挂问题。

空回滚:没有执行try却执行了cancel。参与者分支注册完成之后会执行参与者一阶段try RPC 方法发送rpc时候网络延迟抖动,事务协调者全局回滚,参与者没有执行try却进入cancel
幂等:多次进入try。执行完二阶段之后,由于网络抖动或者宕机问题,会造成事务协调者收不到参与者执行confirm的返回结果,事务协调者会重复发起调用,直到二阶段执行结果成功
悬挂:执行了cancel又进入try。进入try 方法时,出现网路拥堵,由于 seata 全局事务有超时限制,执行 try 方法超时后,进行全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 ,执行 try 方法进行资源预留,从而造成悬挂。

Seata社区这个博客写的挺好 https://seata.apache.org/zh-cn/blog/seata-tcc/ 解释了seata是怎么样处理解决、空回滚、幂等、悬挂问题。

4.1 流程

假设一个转账需求,A给B转100,一般做法是判断够不够钱,如果够钱A-100,B+100

在TCC模式下把这个需求拆分为 Try-Confirm-Cancel

  1. try 阶段,查询A够不够钱,够的话冻结A 100块钱
  2. confirm阶段,执行业务代码,预先给B + 100
  3. 如果一切正常,执行A-100,B + 100
  4. 如果有异常,执行try的回滚逻辑,A解除冻结的100

4.2 缺点

TCC模式有代码侵入,需要把一个业务拆分为三个方法,事务具有软状态,确认和取消操作都可能出现问题,需要考虑如何处理失败情况以保证最终一致性