0%

seata(二)——AT模式

AT模式

AT模式是Seata分布式事务解决方案中的一种事务模式,它采用了类似于传统数据库事务的两阶段提交协议(Two-Phase Commit,2PC)来实现分布式事务的一致性。

核心机制

执行阶段:每个微服务的请求完成后,基于本地数据库的事务能力,保证业务数据和回滚日志在同一个本地事务中提交,快速释放连接和对资源的锁定;

完成阶段:全局提交时分支事务已经完成提交,会清理回滚日志,快速结束流程;全局回滚基于XID和BranchID查询回滚日志,完成数据回滚;

实现方式

在AT模式下,全局事务由一个唯一的全局事务ID(Global Transaction ID)来标识,它负责协调和管理所有的分支事务。

当一个分支事务参与全局事务时,它需要将自己注册到全局事务中,并在事务提交前进行本地的预提交操作(Prepare——一阶段),将事务的执行结果保存在本地日志中,但并不提交到数据库中。如果所有的分支事务都预提交成功,Seata会发出一个全局提交命令,要求所有的分支事务提交事务。此时,每个分支事务都会根据全局提交命令提交本地的事务,如果提交成功,则通知Seata自己已经提交成功,Seata最终会返回一个全局提交成功的结果。如果有任何一个分支事务提交失败,则Seata会发出一个全局回滚命令,要求所有的分支事务回滚事务,从而保证所有分支事务的一致性。

  1. TM负责定义全局事务的边界,向TC申请,开启一个全局事务;
  2. 全局事务创建成功后,生成全局唯一的XID;
  3. XID会在微服务请求链路上下文中传播;
  4. RM向TC注册分支事务,并归属到XID对应的全局事务进行调度;
  5. TM向TC发起相应XID的全局事务提交或回滚决议;
  6. TC完成对XID管理的全部分支事务提交或回滚的调度;

工作机制

img

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

Field Type Key
id bigint(20) PRI
name varchar(100)
since varchar(100)

AT 分支事务的业务逻辑:

1
update product set name = 'GTS' where name = 'TXC';

一阶段(perpare预提交)

过程:

  1. 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
1
select id, name, since from product where name = 'TXC';

得到前镜像:

id name since
1 TXC 2014
  1. 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
1
select id, name, since from product where id = 1;

得到后镜像:

id name since
1 GTS 2014
  1. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

    回滚记录数据格式如下:包括 afterImage 前镜像、beforeImage 后镜像、 branchId 分支事务ID、xid 全局事务ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}

这样就可以保证,任何提交的业务数据的更新一定有相应的回滚日志。

在本地事务提交前,各分支事务需向 全局事务协调者 TC 注册分支 ( Branch Id) ,为要修改的记录申请 全局锁 ,要为这条数据加锁,利用 SELECT FOR UPDATE 语句。而如果一直拿不到锁那就需要回滚本地事务。TM 开启事务后会生成全局唯一的 XID,会在各个调用的服务间进行传递。

有了这样的机制,本地事务分支(Branch Transaction)便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源。相比于传统的 XA 事务在第二阶段释放资源,Seata 降低了锁范围提高效率,即使第二阶段发生异常需要回滚,也可以快速 从UNDO_LOG 表中找到对应回滚数据并反解析成 SQL 来达到回滚补偿。

  1. 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  3. 将本地事务提交的结果上报给 TC。

二阶段-回滚

决议是全局回滚:

  1. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
1
update product set name = 'TXC' where id = 1;
  1. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

注意:这里删除回滚日志记录操作,一定是在本地业务事务执行之后

二阶段-提交

如果决议是全局提交,此时各分支事务已提交并成功:

  1. 全局事务协调者(TC) 会向分支发送第二阶段的请求。收到 TC 的分支提交请求,该请求会被放入一个异步任务队列中,并马上返回提交成功结果给 TC。
  2. 异步队列中会异步和批量地根据 Branch ID 查找并删除相应 UNDO LOG 回滚记录。

数据源代理

在AT模式中,应用需要使用Seata组件中的JDBC代理数据源DataSourceProxy,实现对真正目标数据源的代理访问。

应用场景

AT模式是Seata分布式事务解决方案中的一种事务模式,它适用于以下场景:

  1. 数据库访问:当需要同时操作多个数据库,并且需要保证这些操作的原子性时,AT模式是一种可行的选择。例如,将订单信息插入到订单表中,同时将库存信息减少,这两个操作需要保证在同一个事务中完成。
  2. 远程服务调用:当需要在多个服务之间调用,同时需要保证这些调用的原子性时,AT模式是一种可行的选择。例如,下订单需要调用用户服务和库存服务,需要保证这些调用的结果是一致的。
  3. 消息队列处理:当需要使用消息队列进行异步处理,并且需要保证这些处理的原子性时,AT模式是一种可行的选择。例如,将订单信息写入消息队列,并将库存信息更新为已扣减状态,这两个操作需要保证在同一个事务中完成。

优点

  1. 实现简单。AT模式只需要使用数据库本身提供的特性来实现分布式事务,不需要额外的业务代码来处理事务的提交和回滚,从而减少了业务代码的复杂度和维护成本。
  2. 性能高。AT模式不需要涉及全局锁和协调者,因此可以有效地提高分布式事务的性能和吞吐量。
  3. 容错性强。AT模式的本地事务和回滚机制可以有效地避免事务的异常和失败,从而提高了分布式事务的容错性和可用性。
  4. 兼容性好。AT模式可以与多种数据库和应用程序框架兼容,能够满足不同业务场景下的需求。

存在的问题

AT模式存在的问题主要包括以下几个方面:

  1. 依赖于应用程序的异常处理机制:AT模式通过应用程序的异常处理机制来实现事务回滚,需要开发人员自行处理分支事务的异常情况。因此,在实际应用中,需要开发人员编写大量的异常处理代码,使得代码量较大,且错误处理不够统一和规范。
  2. 分支事务的阻塞问题:在AT模式中,当一个分支事务处于执行过程中时,会阻塞其他分支事务的执行,导致系统的并发性能下降。这是由于AT模式中需要通过全局锁来保证事务的一致性,因此当一个分支事务获得全局锁后,其他分支事务就无法获取锁并执行。
  3. 回滚操作的性能问题:当一个分支事务需要回滚时,AT模式需要执行大量的UndoLog操作来将数据恢复到之前的状态。这些操作需要消耗大量的时间和系统资源,导致回滚操作的性能较低。
  4. 分布式事务的可扩展性问题:AT模式的实现方式需要应用程序显式地管理事务,使得系统的可扩展性受到限制。当系统需要扩展到多个节点时,需要对AT模式的代码进行大量的修改和调整,使得系统的维护和升级变得困难。

因此,在选择AT模式时,需要根据具体的业务需求和系统性能做出权衡,同时需要考虑系统的可用性和数据兼容性等问题。

对策

  1. 使用声明式事务管理:在使用Spring框架时,可以使用声明式事务管理的方式,通过在方法上添加@Transactional注解来管理事务。这样可以避免手动编写事务管理代码,简化开发流程,并提高代码的可维护性和可扩展性。

  2. 使用分布式事务管理框架:Seata是一种流行的分布式事务管理框架,可以支持多种分布式事务模式,如TCC、AT、XA等。使用Seata可以实现分布式事务的自动化管理,避免手动编写事务管理代码,并提高系统的可维护性和可扩展性。

  3. 使用异步消息机制:在AT模式中,可以将分支事务的处理异步化,通过使用消息队列等异步消息机制来处理分支事务。Seata支持使用Kafka和RocketMQ两种消息中间件来实现异步消息模式,可以通过在Seata的配置文件中配置消息中间件的相关参数来启用异步消息模式。对于Kafka和RocketMQ两种消息中间件,Seata提供了相应的插件,可以直接在Seata中使用。在异步消息模式下,Seata的全局事务管理器将分支事务的状态保存到消息中,由消息中间件进行分发和处理,从而减轻全局事务管理器的压力,提高了性能和可靠性。

    1. 配置消息中间件:首先需要在Seata的配置文件中配置消息中间件的相关参数,包括消息中间件类型、地址、用户名、密码等信息。
    2. 引入消息中间件插件:Seata支持Kafka和RocketMQ两种消息中间件,需要在Seata的classpath中引入相应的插件,例如对于Kafka,需要引入seata-server-extensions/seata-registry/seata-registry-kafka插件。
    3. 定义消息格式:在Seata中,需要定义全局事务和分支事务的消息格式,包括事务ID、分支事务ID、分支事务状态等信息。
    4. 启用异步消息模式:在Seata的配置文件中将transaction.coordinator.type配置为async-commit,即可启用异步消息模式。
  4. 使用乐观锁机制:在数据访问时,可以使用乐观锁机制来避免使用全局锁导致的性能问题。乐观锁机制通过使用版本号等机制来实现数据的并发控制,从而提高系统的并发性能。

    乐观锁机制并不会在事务访问数据时对数据进行加锁,而是在提交修改时检查版本号,因此不会造成事务阻塞和等待的情况。

    在使用乐观锁机制时,每个事务在访问数据时都会读取当前的版本号,并在提交修改前检查版本号是否发生变化。如果版本号发生变化,说明有其他事务已经修改了数据,当前事务需要重新读取数据并重新执行操作;如果版本号未发生变化,则说明数据未被修改,当前事务可以提交修改。

  5. 使用分布式缓存:在AT模式中,回滚操作需要执行大量的UndoLog操作,消耗大量的时间和系统资源。通过使用分布式缓存,可以将回滚操作中的数据存储在缓存中,从而避免不必要的数据库操作,提高系统的性能。

综上所述,通过使用声明式事务管理、分布式事务管理框架、异步消息机制、乐观锁机制、分布式缓存等技术手段,可以避免AT模式中存在的问题,提高系统的性能和可维护性。

涉及到的回滚机制

在AT模式中,主要涉及到两种回滚机制:本地回滚和全局回滚。

  1. 本地回滚:当一个分支事务出现异常时,AT模式会自动进行本地回滚,即将分支事务中所做的修改全部撤销,将数据恢复到事务开始前的状态。
  2. 全局回滚:当某个分支事务执行失败并进行了本地回滚时,全局事务需要回滚,即将已经执行成功的分支事务中的修改全部撤销,将数据恢复到事务开始前的状态。在AT模式中,全局回滚是由应用程序显式调用Seata提供的API来触发的。

需要注意的是,AT模式的回滚机制是通过UndoLog实现的。每个参与者在执行分支事务前,会生成UndoLog。当分支事务需要回滚时,Seata会利用UndoLog中记录的信息来回滚事务。同时,如果某个分支事务执行失败,全局事务协调器也会收集所有分支事务的UndoLog,并根据UndoLog的内容来决定是否需要回滚全局事务。