分布式事务解决方案Seata

153 阅读3分钟

1 环境准备

  1. 修改/conf/registry.conf文件,把seata的注册中心和配置中心都改为nacos
registry {
  # 修改注册中心
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "f408da5f-2a7e-478f-951a-6a32ce8475f8"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  ......
}

config {
  # 修改配置中心
  type = "nacos"
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "f408da5f-2a7e-478f-951a-6a32ce8475f8"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
  ......
}
  1. 修改/conf/file.conf文件,修改seata的存储模式,改为用数据库存储
## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    ## MySQL8的驱动与5不同
    driverClassName = "com.mysql.cj.jdbc.Driver"
    ## MySQL8要加timeZone
    url = "jdbc:mysql://127.0.0.1:3306/my_seata?rewriteBatchedStatements=true&serverTimezone=GMT%2B8"
    user = "root"
    password = "persona5"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
  ......
}
  1. 先启动nacos

  2. 拷贝官方提供的配置信息,并修改以下内容

store.mode=db

store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/my_seata?rewriteBatchedStatements=true&serverTimezone=GMT%2B8
store.db.user=root
store.db.password=persona5
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
  1. 在nacos对应的命名空间中,新建配置,Data ID为seataServer.properties,Group为SEATA_GROUP(与registry.conf配置的保持一致),并发布上述修改后的配置信息,注意删除空值配置项
  2. 启动seata

2 项目准备

  1. 有以下微服务
服务名地址数据库地址
order-servicelocalhost:880110.168.6.233:3306/db_order
stock-servicelocalhost:880210.168.6.69:3306/db_stock
  1. 各个微服务都视为一个RM,在各个服务对应的数据库中建立以下表
-- 该表用于数据的回滚
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;
  1. 都导入以下依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <!-- seata -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <!-- nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>
    <!-- openfeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.1</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.18</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>
  1. 都配置以下application.yml
server:
  port: 8802

spring:
  application:
    # 具体服务的名称
    name: stock-service
  # 具体服务的数据源
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://10.168.6.69:3306/db_stock?serverTimezone=GMT%2B8&useSSL=false&useAffectedRows=true&allowPublicKeyRetrieval=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

mybatis:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: pers.ljc.seata.stock.model
  configuration:
    map-underscore-to-camel-case: true

seata:
  # 事务组名称,与配置项service.vgroupMapping.xxx中的xxx一致
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
  1. 业务场景如下:用户下单时调用order-service创建订单,创建订单后远程调用stock-service减少库存
  2. 各微服务业务代码如下
// order-service的业务代码
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderMapper orderMapper;

    private final StockClient stockClient;

    public OrderServiceImpl(OrderMapper orderMapper, StockClient stockClient) {
        this.orderMapper = orderMapper;
        this.stockClient = stockClient;
    }

    @Override
    @GlobalTransactional
    public Boolean createOrder(Order order) {
        // 创建订单
        int i = orderMapper.insertSelective(order);
        if (i == 0) {
            throw new IllegalStateException("创建订单失败");
        }
        // 远程调用库存服务减少库存
        return stockClient.reduceStock(order.getProductId(), order.getCount());
    }
}
// stock-service的业务代码
@Service
public class StockServiceImpl implements StockService {

    private final StockMapper stockMapper;

    public StockServiceImpl(StockMapper stockMapper) {
        this.stockMapper = stockMapper;
    }

    @Override
    public Boolean reduceStock(Long productId, Integer count) {
        Stock stock = stockMapper.selectByPrimaryKey(productId);
        validateReducingStock(stock, count);
        stock.setCount(stock.getCount() - count);
        // 模拟减库存的业务时间
        //try {
        //    Thread.sleep(1000 * 10);
        //} catch (InterruptedException e) {
        //    e.printStackTrace();
        //}
        int i = stockMapper.updateByPrimaryKeySelective(stock);
        if (i == 0) {
            throw new IllegalStateException("更新商品库存失败");
        }
        return true;
    }

    /**
     * 校验是否能减少商品库存
     */
    private void validateReducingStock(Stock stock, Integer count) {
        if (Objects.isNull(stock)) {
            throw new IllegalStateException("不存在该商品");
        }
        if (stock.getCount() < count) {
            throw new IllegalStateException("该商品库存数量不足");
        }
    }
}

3 案例演示

3.1 AT模式

添加注解@GlobalTransactional即可实现

@Service
public class OrderServiceImpl implements OrderService {
    // 其他代码...

    @Override
    @GlobalTransactional
    public Boolean createOrder(Order order) {
        // 创建订单
        int i = orderMapper.insertSelective(order);
        if (i == 0) {
            throw new IllegalStateException("创建订单失败");
        }
        // 远程调用库存服务减少库存
        return stockClient.reduceStock(order.getProductId(), order.getCount());
    }
}

4 参考资料