分布式事务Seata

三大模块

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata 实现分布式事务,设计了一个关键角色 UNDO_LOG (回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 UNDO_LOG 表中,以便业务异常能随时回滚。

image

Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。

Seata 也是从两段提交演变而来的一种分布式事务解决方案,提供了 ATTCCSAGAXA 等事务模式,这里重点介绍 AT模式。

既然 Seata 是两段提交,那我们看看它在每个阶段都做了点啥?下边我们还以下单扣库存、扣余额举例。

先介绍 Seata 分布式事务的几种角色:

  • Transaction Coordinator(TC): 全局事务协调者,用来协调全局事务和各个分支事务(不同服务)的状态, 驱动全局事务和各个分支事务的回滚或提交。
  • Transaction Manager™: 事务管理者,业务层中用来开启/提交/回滚一个整体事务(在调用服务的方法中用注解开启事务)。
  • Resource Manager(RM): 资源管理者,一般指业务数据库代表了一个分支事务(Branch Transaction),管理分支事务与 TC 进行协调注册分支事务并且汇报分支事务的状态,驱动分支事务的提交或回滚。

Seata 实现分布式事务,设计了一个关键角色 UNDO_LOG (回滚日志记录表),我们在每个应用分布式事务的业务库中创建这张表,这个表的核心作用就是,将业务数据在更新前后的数据镜像组织成回滚日志,备份在 UNDO_LOG 表中,以便业务异常能随时回滚。

第一个阶段

比如:下边我们更新 user 表的 name 字段。

update user set name = '小富最帅' where name = '程序员内点事'

首先 Seata 的 JDBC 数据源代理通过对业务 SQL 解析,提取 SQL 的元数据,也就是得到 SQL 的类型(UPDATE),表(user),条件(where name = '程序员内点事')等相关的信息。

图片第一个阶段的流程图

先查询数据前镜像,根据解析得到的条件信息,生成查询语句,定位一条数据。

select  name from user where name = '程序员内点事'

图片数据前镜像

紧接着执行业务 SQL,根据前镜像数据主键查询出后镜像数据

select name from user where id = 1

图片数据后镜像

把业务数据在更新前后的数据镜像组织成回滚日志,将业务数据的更新和回滚日志在同一个本地事务中提交,分别插入到业务表和 UNDO_LOG 表中。

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

{
    "branchId":641789253,
    "xid":"xid:xxx",
    "undoItems":[
        {
            "afterImage":{
                "rows":[
                    {
                        "fields":[
                            {
                                "name":"id",
                                "type":4,
                                "value":1
                            }
                        ]
                    }
                ],
                "tableName":"product"
            },
            "beforeImage":{
                "rows":[
                    {
                        "fields":[
                            {
                                "name":"id",
                                "type":4,
                                "value":1
                            }
                        ]
                    }
                ],
                "tableName":"product"
            },
            "sqlType":"UPDATE"
        }
    ]
}

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

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

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

最后本地事务提交,业务数据的更新和前面生成的 UNDO LOG 数据一并提交,并将本地事务提交的结果上报给全局事务协调者 TC。

第二个阶段

第二阶段是根据各分支的决议做提交或回滚:

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

图片

如果决议是全局回滚,过程比全局提交麻烦一点,RM 服务方收到 TC 全局协调者发来的回滚请求,通过 XIDBranch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。

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

图片

上边说了几种分布式事务各自的优缺点,下边实践一下分布式事务中间 Seata 感受一下。

Seata的执行流程

  1. A 服务【订单微服务】的 TM[ 事务发起者 ] 向 TC[seata 服务端 ] 申请开启一个全局事务, TC 就会创建一个全局事务并返回一个唯一的 XID
  2. A 服务开始远程调用 B 服务【账户微服务】,此时 XID 会在微服务的调用链上传播
  3. B 服务的 RM 向 TC 注册分支事务,并将其纳入 XID 对应的全局事务的管辖
  4. B 服务执行分支事务,向数据库做操作
  5. 全局事务调用链处理完毕, TM 根据有无异常向 TC 发起全局事务的提交或者回滚
  6. TC 协调其管辖之下的所有分支事务, 决定是否回滚

Seata 实践

Seata 是一个需独立部署的中间件,所以先搭 Seata Server,这里以最新的 seata-server-1.4.0 版本为例,下载地址:https://seata.io/en-us/blog/download.html

解压后的文件我们只需要关心 \seata\conf 目录下的 file.confregistry.conf 文件。具体查看官方文档

Seata Server

file.conf

file.conf 文件用于配置持久化事务日志的模式,目前提供 filedbredis 三种方式。

图片file.conf 文件配置

注意:在选择 db 方式后,需要在对应数据库创建 globalTable(持久化全局事务)、branchTable(持久化各提交分支的事务)、 lockTable(持久化各分支锁定资源事务)三张表。

-- the table to store GlobalSession data
-- 持久化全局事务
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
-- 持久化各提交分支的事务
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
-- 持久化每个分支锁表事务
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(96),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

registry.conf

registry.conf 文件设置 注册中心 和 配置中心:

目前注册中心支持 nacoseurekarediszkconsuletcd3sofa 七种,这里我使用的 eureka作为注册中心 ;配置中心支持 nacosapollozkconsuletcd3 五种方式。

图片registry.conf 文件配置

配置完以后在 \seata\bin 目录下启动 seata-server 即可,到这 Seata 的服务端就搭建好了。

Seata Client

Seata Server 环境搭建完,接下来我们新建三个服务 order-server(下单服务)、storage-server(扣减库存服务)、account-server(账户金额服务),分别服务注册到 eureka

每个服务的大体核心配置如下:

spring:
    application:
        name: storage-server
    cloud:
        alibaba:
            seata:
                tx-service-group: my_test_tx_group
    datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://47.93.6.1:3306/seat-storage
        username: root
        password: root

# eureka 注册中心
eureka:
    client:
        serviceUrl:
            defaultZone: http://${eureka.instance.hostname}:8761/eureka/
    instance:
        hostname: 47.93.6.5
        prefer-ip-address: true

业务大致流程:用户发起下单请求,本地 order 订单服务创建订单记录,并通过 RPC 远程调用 storage 扣减库存服务和 account 扣账户余额服务,只有三个服务同时执行成功,才是一个完整的下单流程。如果某个服执行失败,则其他服务全部回滚。

Seata 对业务代码的侵入性非常小,代码中使用只需用 @GlobalTransactional 注解开启一个全局事务即可。

@Override
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void create(Order order) {

    String xid = RootContext.getXID();

    LOGGER.info("------->交易开始");
    //本地方法
    orderDao.create(order);

    //远程方法 扣减库存
    storageApi.decrease(order.getProductId(), order.getCount());

    //远程方法 扣减账户余额
    LOGGER.info("------->扣减账户开始order中");
    accountApi.decrease(order.getUserId(), order.getMoney());
    LOGGER.info("------->扣减账户结束order中");

    LOGGER.info("------->交易结束");
    LOGGER.info("全局事务 xid: {}", xid);
}

前边说过 Seata AT 模式实现分布式事务,必须在相关的业务库中创建 undo_log 表来存数据回滚日志,表结构如下:

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME     NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

到这环境搭建的工作就完事了,完整案例会在后边贴出 GitHub 地址,就不在这占用篇幅了。

测试 Seata

项目中的服务调用过程如下图:

图片服务调用过程

启动各个服务后,我们直接请求下单接口看看效果,只要 order 订单表创建记录成功,storage 库存表 used 字段数量递增、account 余额表 used 字段数量递增则表示下单流程成功。

图片原始数据

请求后正向流程是没问题的,数据和预想的一样

图片下单数据

而且发现 TM 事务管理者 order-server 服务的控制台也打印出了两阶段提交的日志

图片控制台两次提交

那么再看看如果其中一个服务异常,会不会正常回滚呢?在 account-server 服务中模拟超时异常,看能否实现全局事务回滚。

图片全局事务回滚

发现数据全没执行成功,说明全局事务回滚也成功了

图片

那看一下 undo_log 回滚记录表的变化情况,由于 Seata 删除回滚日志的速度很快,所以要想在表中看见回滚日志,必须要在某一个服务上打断点才看的更明显。

图片回滚记录

注意:

1,异常需要层层往上抛,如果你在子服务将异常处理的话(比如全局异常处理GlobalExceptionHandler),seata会认为你已经手动处理了异常。

2,出现事务失效的情况下,优先检查 RootContext.getXID() ,xid是否传递且一致。

3,主服务加上@GlobalTransactional注解即可,被调用服务不用加@GlobalTransactional@Transactional.

4,@GlobalTransactional(rollbackFor = Exception.class)最好加上rollbackFor = Exception.class,表示遇到Exception都回滚,不然遇到有些异常(如自定义异常)则不会回滚。

参考:

面试被问分布式事务(2PC、3PC、TCC),这样解释没毛病!

对比 5 种分布式事务方案,还是宠幸了阿里的 Seata(原理 + 实战)

Seata 是什么?

分布式事务 SEATA-1.4.1 AT模式 配合NACOS 应用

springcloud-nacos-seata 实现分布式事务

分布式事务事务 数据 修改 seata 服务

posted @ 2022-10-03 22:46  hongdada  阅读(185)  评论(0编辑  收藏  举报