分布式事务~seata的使用

发布时间 2023-12-04 17:18:44作者: 张占岭

springcloud-nacos-seata

分布式事务组件seata的使用demo,AT模式,集成nacos、springboot、springcloud、mybatis-plus,数据库采用mysql

demo中使用的相关版本号,具体请看代码。如果搭建个人demo不成功,验证是否是由版本导致,由于目前这几个项目更新比较频繁,版本稍有变化便会出现许多奇怪问题

  • seata 1.4.2
  • spring-cloud-alibaba-seata 2.1.0.RELEASE
  • spring-cloud-starter-alibaba-nacos-discovery 0.2.1.RELEASE
  • springboot 2.0.6.RELEASE
  • springcloud Finchley.RELEASE

1. 服务端配置

1.1 Nacos-server

版本为nacos-server-2.0.4,demo采用本地单机部署方式,请参考 Nacos 快速开始

1.2 Seata-server

seata-server为release版本1.4.2,demo采用本地单机部署,从此处下载 https://github.com/seata/seata/releases
并解压

1.2.1 修改conf/registry.conf 配置

设置type、设置serverAddr为你的nacos节点地址。

注意这里有一个坑,serverAddr不能带‘http://’前缀

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1"
    namespace = ""
    cluster = "default"
  }
}
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1"
    namespace = ""
    cluster = "default"
  }
}

1.2.2 修改conf/nacos-config.txt 配置

service.vgroup_mapping.${your-service-gruop}=default,中间的${your-service-gruop}为自己定义的服务组名称,服务中的application.properties文件里配置服务组名称。

demo中有两个服务,分别是stock-service和order-service,所以配置如下

service.vgroup_mapping.stock-service-group=default
service.vgroup_mapping.order-service-group=default

** 注意这里,高版本中应该是vgroupMapping 同时后面的如: order-service-group 不能定义为 order_service_group**

1.3 启动seata-server

分两步,如下

# 初始化seata 的nacos配置
cd conf
sh nacos-config.sh 127.0.0.1

# 启动seata-server
cd bin
sh seata-server.sh -p 8091 -m file

2. 应用配置

2.1 数据库初始化

-- 创建 order库、业务表、undo_log表
create database seata_order;
use seata_order;

DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  `money` int(11) DEFAULT 0,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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;


-- 创建 stock库、业务表、undo_log表
create database seata_stock;
use seata_stock;

DROP TABLE IF EXISTS `stock_tbl`;
CREATE TABLE `stock_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT 0,
  PRIMARY KEY (`id`),
  UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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;

-- 初始化库存模拟数据
INSERT INTO seata_stock.stock_tbl (id, commodity_code, count) VALUES (1, 'product-1', 9999999);
INSERT INTO seata_stock.stock_tbl (id, commodity_code, count) VALUES (2, 'product-2', 0);

2.2 应用配置

见代码

几个重要的配置

  1. 每个应用的resource里需要配置一个registry.c ,demo中与seata-server里的配置相同
  2. application.propeties 的各个配置项,注意spring.cloud.alibaba.seata.tx-service-group 是服务组名称,与nacos-config.txt
    配置的service.vgroup_mapping.${your-service-gruop}具有对应关系

3. 测试

  1. 分布式事务成功,模拟正常下单、扣库存

    localhost:9091/order/placeOrder/commit

  2. 分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚

    localhost:9091/order/placeOrder/rollback

相关代码

order模块

    /**
     * 下单:创建订单、减库存,涉及到两个服务
     *
     * @param userId
     * @param commodityCode
     * @param count
     */
    @GlobalTransactional
    @Transactional(rollbackFor = Exception.class)
    public void placeOrder(String userId, String commodityCode, Integer count) {
        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
            orderMoney);
        orderDAO.insert(order);
        stockFeignClient.deduct(commodityCode, count);

    }

stock模块

    /**
     * 减库存
     *
     * @param commodityCode
     * @param count
     */
    @Transactional(rollbackFor = Exception.class)
    public void deduct(String commodityCode, int count) {
        if (commodityCode.equals("product-2")) {
            throw new RuntimeException("异常:模拟业务异常:stock branch exception");
        }

        QueryWrapper<Stock> wrapper = new QueryWrapper<>();
        wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
        Stock stock = stockDAO.selectOne(wrapper);
        stock.setCount(stock.getCount() - count);

        stockDAO.updateById(stock);
    }