Seata+Nacos分布式事务例子(AT和TCC)

安装 Nacos

官网下载 Nacos,直接启动即可,这里省略

安装 SeataServer 端

seata 需要一个 server (TC) 作为协调者处理 AT、TCC 和 SAGA 事务模型。

官网下载 Seata

下载地址:https://github.com/seata/seata/releases

配置 Seata 使用 Nacos 注册中心

修改 seata/conf/application.yml

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 192.168.1.201:8848
      group: DEFAULT_GROUP
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      data-id: seataServer.properties
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.1.201:8848
      group: DEFAULT_GROUP
      namespace:
      cluster: default
      username:
      password:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""

设置 nacos 地址 192.168.1.201:8848

启动 seata:seata/bin/steata-server.bat

nacos 上可以看到 seata-server 的注册信息 

登录 seata 后台:http://localhost:7091 7091 是管理端口,nacos 上注册的是 8091 是事务的服务端口(这个就比 Nacos 处理的要好,Nacos 就一个端口)

订单服务和库存服务数据库初始化

CREATE TABLE `t_order` (
  `uri` varchar(50) NOT NULL,
  `name` varchar(50) DEFAULT NULL COMMENT '订单名称',
	`commodity_code` varchar(50) DEFAULT NULL COMMENT '产品编码',
	`count` bigint(20) default 0 comment '订单数量',
	`money` bigint(20) default 0 comment '金额',
  PRIMARY KEY (`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='订单';


CREATE TABLE `t_storage` (
  `uri` varchar(50) NOT NULL,
	`commodity_code` varchar(50) DEFAULT NULL COMMENT '产品编码',
	`count` bigint(20) default 0 comment '库存数量',
  PRIMARY KEY (`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='库存';

INSERT INTO `t_storage`(`uri`, `commodity_code`, `count`) VALUES ('product01', 'product01', 1000);


-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录
DROP TABLE `undo_log`;

CREATE TABLE `undo_log` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `branch_id` BIGINT(20) NOT NULL,
  `xid` VARCHAR(100) NOT NULL,
  `context` VARCHAR(128) NOT NULL,
  `rollback_info` LONGBLOB NOT NULL,
  `log_status` INT(11) NOT NULL,
  `log_created` DATETIME NOT NULL,
  `log_modified` DATETIME NOT NULL,
  `ext` VARCHAR(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

配置订单服务

pom.xml 关键配置: 增加 seata 依赖配置,这里使用的是 spring cloud alibaba seata 组件,这个组件包含了 seata 所有依赖和基于 springcloud 的 seata 注册自动化配置。

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	<version>2.2.6.RELEASE</version>
</dependency>

application.yaml 相关 seata 配置 (数据库等其他配置省略):

seata:
  registry:
    # 使用nacos作为注册中心
    type: nacos
    nacos:
      server-addr: 192.168.1.201:8848
      # Seata服务名(应与seata-server实际注册的服务名一致)
      application: seata-server
      # Seata分组名(应与seata-server实际注册的分组名一致)
      group: DEFAULT_GROUP  
  # 一般是用来动态调整seata的集群分组,处理冷备、环境隔离等场景
  txServiceGroup: my_test_tx_group
  service:
    vgroupMapping:
      my_test_tx_group: default

创建订单的 Controller、Entity、Mapper 和 Service: OrderController:

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    public OrderService iOrderService;
    /**
     * 创建订单
     * @param order  传递的实体
     */
    @ResponseBody
    @RequestMapping(method=RequestMethod.POST,value="/add")
    public ResultVO<Integer> add(@RequestBody Order order) {
        log.info("保存和修改:{}", order);
        iOrderService.createOrder(order);
        ResultVO result = ResultVO.success("OK");
        return result;
    }
}

OrderEntity:

@TableName("t_order")
public class Order {

    private static final long serialVersionUID = 1L;
    @TableId(value="uri", type = IdType.ASSIGN_ID)
    protected String uri;
    
    /**
     * 订单名称
     */
    @TableField("name")
    private String name;


    /**
     * 产品编码
     */
    @TableField("commodity_code")
    private String commodityCode;


    /**
     * 订单数量
     */
    @TableField("count")
    private Long count;


    /**
     * 金额
     */
    @TableField("money")
    private Long money;
}

Mapper 和 XML 省略,参考 Mybatispuls 代码生成的结果。

OrderServiceImpl

@Service
@Primary
@Transactional
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Autowired
    private StorageServiceRemote storageServiceRemote;
    @Autowired
    private OrderMapper orderMapper;
    @Override
    @GlobalTransactional
    public void createOrder(Order order) {
        log.info("创建订单:{}", order);
        order.setUri(null);
        this.save(order);
        //扣减库存
        log.info("扣减库存:{}", order.getCount());
        storageServiceRemote.reduce(order.getCommodityCode(), order.getCount());

        //模拟异常
        if(Objects.equals("product01", order.getCommodityCode()) && order.getCount() == 12) {
            throw new UcloudSystemException("测试seata1异常");
        }
    }
}

这里的关键是 @GlobalTransactional,开启全局事务,这个是全局事务的发起者,也就是 TM。

配置库存扣减调用 Feign 服务:

StorageServiceRemote

@FeignClient(name = "service-storage",fallbackFactory = StorageServiceRemoteFallbackFactory.class)
public interface StorageServiceRemote {
    @PostMapping("/storage/reduce")
    public ResultVO reduce(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count);
}

StorageServiceRemoteFallbackFactory,这里直接抛出异常,保证事务能回滚:

@Slf4j
@Component
public class StorageServiceRemoteFallbackFactory implements FallbackFactory<StorageServiceRemote> {
    @Override
    public StorageServiceRemote create(Throwable cause) {
        log.error("异常:", cause);
        throw new UcloudSystemException("测试feign异常");
    }
}

service-storage 是库存的服务名

配置库存服务

pom.xml 和 yaml 配置参考 "订单服务" 的配置,只是两个服务的服务名不一样。

配置库存的 Controller、Entity、Mapper 和 Service: StorageController:

@Slf4j
@RestController
@RequestMapping("/storage")
public class StorageController {

    @Autowired
    public StorageService iStorageService;

    @ResponseBody
    @PostMapping(value="/reduce")
    public ResultVO<Integer> reduce(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count){
        log.info("扣减库存:{}, {}", commodityCode, count);
        iStorageService.reduce(commodityCode, count);
        ResultVO result = ResultVO.success("OK");
        return result;
    }


}

StorageEntity:

@TableName("t_order")
public class Order {

    private static final long serialVersionUID = 1L;
    @TableId(value="uri", type = IdType.ASSIGN_ID)
    protected String uri;
    
    /**
     * 产品编码
     */
    @TableField("commodity_code")
    private String commodityCode;

    /**
     * 库存数量
     */
    @TableField("count")
    private Long count;
}

Mapper 和 XML 省略,参考 Mybatispuls 代码生成的结果。

StorageServiceImpl

@Service
@Primary
@Transactional
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage> implements StorageService {

    @Autowired
    private StorageMapper storageMapper;

    @Override
    @Transactional
    public void reduce(String commodityCode, Long count) {
        Storage storage = this.getOne(new QueryWrapper().eq(true, "commodity_code", commodityCode));
        storage.setCount(storage.getCount() - count);
        log.info("更新库存信息:{}", storage);
        if("product01".equalsIgnoreCase(commodityCode) && count == 11) {
            throw new UcloudSystemException("测试回滚异常");
        }
        this.update(storage, new QueryWrapper().eq(true, "commodity_code", commodityCode));
    }
}

Storage 是分支事务,因此这里只要配置 @Transactional 就行了

测试验证

验证调用 Storage 异常事务回滚

调用:http://localhost:6318/order/add

{
  "commodityCode": "product01",
  "count": 11,
  "flag": 0,
  "money": 100,
  "name": "订单",
}

提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> RM 生成 Storage 快照 undo -> Storage 保存 -> Storage 模拟抛出异常

事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 本地 RM 回滚 -> 删除 Storage 快照 -> Order 本地 RM 回滚

这种其实可以不需要全局分布式事务参与,都是本地事务回滚。

验证调用 Storage 成功后 Order 异常

调用:http://localhost:6318/order/add

{
  "commodityCode": "product01",
  "count": 12,
  "flag": 0,
  "money": 100,
  "name": "订单",
}

提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> RM 生成 Storage 快照 -> Storage 保存 -> 返回 Order 模拟抛出异常

事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 提交 Commit -> Order 异常后通过 TC 调用 Storage 回滚 undo 日志 -> Storage 回滚并删除快照 -> Order 本地 RM 回滚

数据库的生成过程:

异常发生前数据库信息 库存信息已经发生变化 876 

 undo 中生成的快照数据: 

{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.3.79:8091:8043697401397272602","branchId":8043697401397272603,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"t_storage","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"uri","keyType":"PRIMARY_KEY","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"commodity_code","keyType":"NULL","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":-5,"value":["java.lang.Long",888]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"del_flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"create_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653979471000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"update_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653980897000,0]]}]]}]]},"afterImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"t_storage","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"uri","keyType":"PRIMARY_KEY","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"commodity_code","keyType":"NULL","type":12,"value":"product01"},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"count","keyType":"NULL","type":-5,"value":["java.lang.Long",876]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"del_flag","keyType":"NULL","type":4,"value":0},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"create_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653979471000,0]]},{"@class":"io.seata.rm.datasource.sql.struct.Field","name":"update_date","keyType":"NULL","type":93,"value":["java.sql.Timestamp",[1653981091000,0]]}]]}]]}}]]}

异常发生后数据库信息: 库存发生回滚 888 

undo 快照内容被清空: 

分支事务回滚的时候是阻塞状态,全局事务发起者和分支事务的堆栈:

全局事务发起者,回滚阻塞,同步调用 TC:

        at io.seata.core.protocol.MessageFuture.get(MessageFuture.java:58)
        at io.seata.core.rpc.netty.AbstractNettyRemotingClient.sendSyncRequest(AbstractNettyRemotingClient.java:165)
        at io.seata.tm.DefaultTransactionManager.syncCall(DefaultTransactionManager.java:95)
        at io.seata.tm.DefaultTransactionManager.rollback(DefaultTransactionManager.java:72)
        at io.seata.tm.api.DefaultGlobalTransaction.rollback(DefaultGlobalTransaction.java:163)
        at io.seata.tm.api.TransactionalTemplate.rollbackTransaction(TransactionalTemplate.java:165)
        at io.seata.tm.api.TransactionalTemplate.completeTransactionAfterThrowing(TransactionalTemplate.java:139)
        at io.seata.tm.api.TransactionalTemplate.execute(TransactionalTemplate.java:109)
        at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalTransaction(GlobalTransactionalInterceptor.java:147)
        at io.seata.spring.annotation.GlobalTransactionalInterceptor.invoke(GlobalTransactionalInterceptor.java:122)

子应用 RM 事务管理,回滚数据:

        at io.seata.rm.AbstractRMHandler.doBranchRollback(AbstractRMHandler.java:122)
        at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:67)
        at io.seata.rm.AbstractRMHandler$2.execute(AbstractRMHandler.java:63)
        at io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate(AbstractExceptionHandler.java:116)
        at io.seata.rm.AbstractRMHandler.handle(AbstractRMHandler.java:63)
        at io.seata.rm.DefaultRMHandler.handle(DefaultRMHandler.java:63)
        at io.seata.core.protocol.transaction.BranchRollbackRequest.handle(BranchRollbackRequest.java:35)
        at io.seata.rm.AbstractRMHandler.onRequest(AbstractRMHandler.java:150)
        at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.handleBranchRollback(RmBranchRollbackProcessor.java:63)
        at io.seata.core.rpc.processor.client.RmBranchRollbackProcessor.process(RmBranchRollbackProcessor.java:58)
        at io.seata.core.rpc.netty.AbstractNettyRemoting.lambda$processMessage$2(AbstractNettyRemoting.java:265)
        at io.seata.core.rpc.netty.AbstractNettyRemoting$$Lambda$1055/1686572615.run(Unknown Source)

TCC 分布式事物

前面都是基于 Seata 的 AT 分布式事务模式,有些场景需要使用到 TCC 分布式事务,TCC 分布式事务对代码有一定的侵入性,但是性能更好,能处理复杂业务场景。

TCC 相关测试代码

OrderControler 中添加方法:

    /**
     * 创建订单TCC
     * @param order  传递的实体
     */
    @ResponseBody
    @RequestMapping(method=RequestMethod.POST,value="/addTcc")
    public ResultVO<Integer> addTcc(@RequestBody Order order) {
        log.info("保存和修改:{}", order);
        iOrderService.createOrderTcc(order);
        ResultVO result = ResultVO.success("OK");
        return result;
    }

OrderServiceImpl 中增加测试事务的方法:注意也是使用 @GlobalTransactional

    @GlobalTransactional
    public void createOrderTcc(Order order) {
        log.info("创建订单TCC:{}", order);
        order.setUri(null);
        this.save(order);

        //扣减库存
        log.info("扣减库存TCC:{}", order.getCount());
        storageServiceRemote.reduceTcc(order.getCommodityCode(), order.getCount());

        //模拟异常
        if(Objects.equals("product01", order.getCommodityCode()) && order.getCount() == 12) {
            throw new UcloudSystemException("测试seata1异常TCC");
        }
    }

Feign 添加 reduceTcc 远程方法:

    @PostMapping("/storage/reduceTcc")
    public void reduceTcc(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count);

StorageControler 库存添加接口:

    @ResponseBody
    @PostMapping(value="/reduceTcc")
    public ResultVO<Integer> reduceTcc(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Long count){
        log.info("扣减库存TCC:{}, {}", commodityCode, count);
        iStorageService.reduceTcc(commodityCode, count);
        ResultVO result = ResultVO.success("OK");
        return result;
    }

StorageService 接口增加 TCC 事务:

@LocalTCC
public interface StorageService extends IService<Storage> {

    void reduce(String commodityCode, Long count);

    /**
     * @TwoPhaseBusinessAction 描述⼆阶段提交
     * name: 为 tcc⽅法的 bean 名称,需要全局唯⼀,⼀般写⽅法名即可
     * commitMethod: Commit⽅法的⽅法名
     * rollbackMethod:Rollback⽅法的⽅法名
     * @BusinessActionContextParamete 该注解⽤来修饰 Try⽅法的⼊参,
     * 被修饰的⼊参可以在 Commit ⽅法和 Rollback ⽅法中通过 BusinessActionContext 获取。
     */
    @TwoPhaseBusinessAction(name = "reduceTcc", commitMethod = "reduceTccCommit", rollbackMethod = "reduceTccRollback")
    void reduceTcc(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode
            ,@BusinessActionContextParameter(paramName = "count") Long count);
    public boolean reduceTccCommit(BusinessActionContext context);
    public boolean reduceTccRollback(BusinessActionContext context);
}

注意:@LocalTCC 需要添加到接口上,对需要走 TCC 的方法配置 @TwoPhaseBusinessAction 两段提交的注解;同时实现相关 commit 和 rollback 接口;BusinessActionContextParameter 注解用于将参数添加到 BusinessActionContext 上下文中,方便 commit 和 rollback 方法能获取到;另外注意获取到的变量都被转换成了字符串,对象变成了 json 字符串,Long 变成了 String。

测试提交

调用:http://localhost:6318/order/addTcc

{
  "commodityCode": "product01",
  "count": 12,
  "flag": 0,
  "money": 100,
  "name": "订单",
}

Storage 中能看到对应的回滚日志:

[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.397 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl      : reduceTccCommit事务回滚
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.398 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl      : commodityCode=product01, count=12
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.404 INFO 2468 [rpcDispatch_RMROLE_1_2_8] c.u.p.d.s.s.impl.StorageServiceImpl      : 更新库存信息TCC:Storage{uri=product01, commodityCode=product01, count=1000, extend=null, flag=0, delFlag=0, createBy=null, updateBy=null, createDate=Tue May 31 14:44:31 CST 2022, updateDate=Tue May 31 18:00:06 CST 2022, fkDomain=null, fkGroup=null}
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.411 INFO 2468 [rpcDispatch_RMROLE_1_2_8] io.seata.rm.AbstractResourceManager      : TCC resource rollback result : true, xid: 192.168.3.79:8091:8043697401397272628, branchId: 8043697401397272629, resourceId: reduceTcc
[service-storage:192.168.3.79:6319] 2022-05-31 18:00:06.411 INFO 2468 [rpcDispatch_RMROLE_1_2_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked

提交过程:Order -> 本地保存 -> 调用 Storage 服务 -> Storage 保存 -> 返回 Order 模拟抛出异常

事务过程:Order 业务 SQL 执行 (不提交) -> Storage 业务 SQL 执行 -> Storage 提交 Commit -> Order 异常后通过 TC 调用 Storage 的 TCCrollBack 方法 -> Order 本地 RM 回滚

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值