Seata 分布式事务 快速开始

一、场景分析

假设现在有这么两个微服务 order、product,order 通过 feign 调用 product。

1.1、表结构

CREATE DATABASE IF NOT EXISTS `oms`;

CREATE TABLE `oms_order` (
  `order_id` int NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `product_id` int NOT NULL COMMENT '商品ID',
  `quantity` int NOT NULL COMMENT '总数量',
  PRIMARY KEY (`order_id`)
) COMMENT='订单表';

CREATE DATABASE IF NOT EXISTS `pms`;

CREATE TABLE `pms_product` (
  `product_id` int NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `stock` int NOT NULL COMMENT '商品库存',
  `product_name` varchar(50) NOT NULL COMMENT '商品名称',
  PRIMARY KEY (`product_id`)
) COMMENT='商品表';

INSERT INTO `pms_product` (`product_id`, `stock`, `product_name`) 
VALUES (1, 100, 'HuaWei Mate60 Pro');

 1.2、本地事务

@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    private final ProductFeignService productFeignService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder() {
        // 1、扣减库存
        PmsDeductStockDto dto = new PmsDeductStockDto();
        dto.setNum(1);
        dto.setProductId(1);
        productFeignService.deductStock(dto);
        // 2、创建订单
        Order order = new Order();
        order.setQuantity(1);
        order.setProductId(1);
        this.baseMapper.insert(order);
        int i = 1/0;// 报错
        return order;
    }
}

OrderServiceImpl#createOrder 加上了 @Transactional 注解,它是一个 Spring AOP 事务方法。因为 / by zero 错误,事务回滚,oms 数据库并没有插入订单数据;但是 pms 却扣减库存成功了。

pms_product 的库存 stock 从 100 被扣减到 99。

数据出现不一致:没有订单,库存却凭空减少。

这种不同数据库,或者相同数据库但不同服务之间的事务操作,所造成的数据不一致现象,叫做分布式事务问题。

我们使用 Seata 看看怎么来解决这个问题 →→

二、角色

Seata 的三大角色:

TC (Transaction Coordinator) - 事务协调者

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

TM (Transaction Manager) - 事务管理器

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

RM (Resource Manager) - 资源管理器

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

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

三、部署 Seata 服务端

3.1、版本选择

根据  Spring Cloud Alibaba 组件版本关系,到官网下载对应的 Seata 版本。

因为我的 Spring Cloud Alibaba 版本是 2021.0.5.0,所以下载 1.6.1.zip

3.2、执行建表语句

创建数据库 seata:

CREATE DATABASE IF NOT EXISTS `seata`;

根据自己的数据库,执行下载包 seata\script\server\db 下脚本:

 

3.3、修改配置

  • 修改 application.yml

修改 seata\conf\application.yml 文件,添加配置中心与注册中心信息。

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
        server-addr: 127.0.0.1:8848
        group: SEATA_GROUP
        data-id: seataServer.properties
        username: nacos
        password: nacos

  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
        application:  seata-server
        server-addr: 127.0.0.1:8848
        group: SEATA_GROUP
        cluster: default
        username: nacos
        password: nacos

Seata 作为分布式事务协调者,可以通过注册中心与微服务通讯。

存疑:

config.nacos.namespace 如果配置 public 会起不来,不知道其他人会不会。

  • 修改 config.txt

修改 seata\script\config-center\config.txt 文件,并将内容上传到 Nacos 配置中心 public 命名空间下,data-id 为 seataServer.properties。

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.40.111:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456

修改事务信息的存储方式为 db,并修改数据库连接方式。

注意 mysql 8.0 以上,使用 com.mysql.cj.jdbc.Driver 驱动。

 3.4、启动

运行 seata\bin\seata-server.bat

Nacos 服务列表有 seata-server 证明启动成功。

登录 http://localhost:7091/#/login 用户名密码 seata/seata

四、配置 Seata 客户端(AT 模式)

4.1、导入依赖

order、product 的 pom.xml 添加依赖:

<!-- seata -->
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

4.2、配置 application.yml

order、product 的 application.yml 添加配置:

seata:
  application-id: ${spring.application.name}
  tx-service-group: default_tx_group
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      username: nacos
      password: nacos

  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
      username: nacos
      password: nacos
  • tx-service-group 

seata 服务分组,要与服务端配置 service.vgroupMapping 的后缀对应。

  • config.nacos.data-id

新版本的 seata 服务端、客户端配置可以共用同一套。

 4.3、创建 undo_log 表(仅AT模式)

order、product 数据库创建 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`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) 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(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

4.4、添加 @GlobalTransactional 注解

@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    private final ProductFeignService productFeignService;

    @Override
    // @Transactional(rollbackFor = Exception.class)
    @GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
    public Order createOrder() {
        // 1、扣减库存
        PmsDeductStockDto dto = new PmsDeductStockDto();
        dto.setNum(1);
        dto.setProductId(1);
        productFeignService.deductStock(dto);
        // 2、创建订单
        Order order = new Order();
        order.setQuantity(1);
        order.setProductId(1);
        this.baseMapper.insert(order);
        int i = 1/0;
        return order;
    }
}

4.5、启动并测试

访问 localhost:8012/order/create

order-service 控制台报错:

product-service 回滚了事务:

数据库:

在文章开头从 100 减为 99 后,就没有再变化了。证明 Seata 是有效的。

五、原理

5.1、2PC

Seata AT 模式,整体机制还是两阶段提交协议(2PC Two-Phase Commit),只不过是协议的演变。

AT 模式(Automatic Transaction):

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

就因为 AT 模式 自动生成和清理回滚日志、自动提交和回滚事务、自动加锁和解锁,所以才叫 AT(Automatic Transaction)。

5.2、生命周期

1、TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID,会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。当一进入事务方法中就会生成 XID , global_table 就是存储的全局事务信息。

2、RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。当运行数据库操作方法,branch_table 存储事务参与者分支事务信息。

3、TM 请求 TC 告诉 XID 对应的全局事务是进行提交还是回滚。

4、TC 驱动 RM 们将 XID 对应的自己的本地事务进行提交还是回滚。

5.3、流程图

带入我们上面的场景,就是:

订单服务,它既要开启全局事务,又要管理对 oms 的事务操作,所以它既是 TM 又是 RM。

商品服务,它只需要管理对 pms 的事务操作,所以它只是 RM。

  • 第一阶段

1.1、用户下单,订单服务TM模块请求TC开启全局事务。TC存储全局事务信息(global_table),返回给TM XID(类似于 192.168.123.1:8091:2900960722371846145 这种格式)。

1.2、订单服务调用商品服务,传递 XID。

1.2.1、商品服务RM模块向TC注册分支事务扣减库存,TC存储商品服务的分支事务信息(branch_table)。

1.2.2、商品服务RM模块生成回滚日志记录(un_do)和本地事务一起提交。

1.3、订单保存。

1.3.1、订单服务RM模块向TC注册分支事务保存订单。TC存储订单服务的分支事务信息(branch_table)。

1.3.2、订单服务RM模块生成回滚日志记录(un_do)和本地事务一起提交。

  • 第二阶段(正常提交 commit)

2.1、订单服务TM模块检测到下单逻辑无异常,向TC请求提交全局事务。

2.2、TC通知商品服务RM分支事务提交,RM清理本地回滚日志记录。

2.3、TC通知订单服务RM分支事务提交,RM清理本地回滚日志记录。

  • 第二阶段(异常回滚 rollback)

2.1、订单服务TM模块捕获到下单异常,向TC请求回滚全局事务。

2.2、TC通知商品服务RM分支事务回滚,RM根据本地回滚日志记录回滚数据。

2.3、TC通知订单服务RM分支事务回滚,RM根据本地回滚日志记录回滚数据。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值