使用seata解决分布式事物问题

本文详细介绍了如何配置和使用Seata进行分布式事务管理,包括Seata Server的安装、Nacos配置、数据库模式的设置以及在Spring Boot、Spring Cloud项目中的应用。通过@GlobalTransactional注解实现分布式事务的处理,确保业务操作的一致性。
摘要由CSDN通过智能技术生成

seata如果不是很了解的话可以直接到官网查看官方文档说明:http://seata.io/zh-cn/docs/overview/what-is-seata.html

如果你还对SpringBootDubboNacosSeataMybatis 不是很了解的话,这里我为大家整理个它们的官网网站,如下

本例子主要模块为以下版本:
springboot版本:2.2.5.RELEASE
spring-cloud版本:Hoxton.SR3
spring-cloud-alibaba版本:2.2.1.RELEASE
seata版本:1.4.0
nacos版本:1.4.0

如果还没安装好nacos环境的可以参考文档:nacos下载与安装

整个业务逻辑由4个微服务提供支持:

  • 库存服务(storage):扣除给定商品的存储数量。
  • 订单服务(order):根据购买请求创建订单。
  • 帐户服务(account):借记用户帐户的余额。
  • 业务服务(business):处理业务逻辑。
  • 公共模块(common):公共模块。

一、下载并安装seata-server

1,下载seata
https://github.com/seata/seata/releases
在这里插入图片描述
2,解压并移动到 /usr/local/ 目录下

tar -zxvf seata-server-1.4.0.tar.gz
mv seata seata-1.4.0
mv seata-1.4.0/ /usr/local/
cd /usr/local/seata-1.4.0/

3,修改 conf/registry.conf 配置
目前seata支持如下的file、nacos 、apollo、zk、consul的注册中心和配置中心。这里我们以nacos 为例。
将 type 改为 nacos

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  nacos {
    application = "seata-server"
    serverAddr = "192.168.2.112:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "192.168.2.112:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
}
  • serverAddr = “192.168.2.112:8848” :nacos 的地址
  • namespace = “” :nacos的命名空间默认为``
  • cluster = “default” :集群设置未默认 default

4,修改 config.txt 文件
config.txt 配置文件需要自行下载,下载后放到seata根目录下面,下载地址:https://github.com/seata/seata/blob/develop/script/config-center/config.txt

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

配置的详细说明参考官网:http://seata.io/zh-cn/docs/user/configurations.html
这里主要修改了如下几项:

  • store.mode :存储模式 默认file 这里我修改为db 模式 ,并且需要三个表global_tablebranch_tablelock_table
  • store.db.driverClassName=com.mysql.jdbc.Driver
  • store.db.db-type=mysql : 存储数据库的类型为mysql
  • store.db.url=jdbc:mysql://192.168.2.112:3306/seata_config?useUnicode=true : 修改为自己的数据库urlport数据库名称
  • store.db.user=root :数据库的账号
  • store.db.password=123456 :数据库的密码
  • service.vgroupMapping.order-seata-tx-group=default
  • service.vgroupMapping.account-seata-tx-group=default
  • service.vgroupMapping.storage-seata-tx-group=default
  • service.vgroupMapping.business-seata-tx-group=default

注意:如果mysql版本为8.0以上的版本,需要使用8.x以上的驱动包,并且将 store.db.driverClassName=com.mysql.jdbc.Driver 配置改为 store.db.driverClassName=com.mysql.cj.jdbc.Driver
然后直接到seata/lib目录下,将8版本的jar包粘贴到目录下

db模式下的所需的三个表的数据库脚本下载地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql

5、将 Seata 配置添加到 Nacos 中
脚本下载地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh
下载后将脚本放到 seata bin/ 目录下,并将脚本权限改成具有可执行权限

cd bin
chmod 755 nacos-config.sh

执行 nacos-config.sh 脚本,将配置上传到nacos

sh ./nacos-config.sh

看到输出如下内容,说明脚本已经执行成功
在这里插入图片描述
登陆到nacos控制台也能够看到上传的配置内容
在这里插入图片描述
6、使用db模式启动 seata

sh seata-server.sh -h 192.168.2.112 -p 8091 -m db
Options:
    --host, -h
      The host to bind,主机绑定ip,nacos的注册地址
      Default: 0.0.0.0
    --port, -p
      The port to listen.
      Default: 8091
    --storeMode, -m
      log store mode : file、db
      Default: file
    --help

Server端存储模式(store.mode)现有file、db、redis三种,file模式无需改动,直接启动即可,下面专门讲下db和redis启动步骤。
注: file模式为单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高;

db模式为高可用模式,全局事务会话信息通过db共享,相应性能差些;

redis模式Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置合适当前场景的redis持久化配置.

二、代码构建

项目地址:https://gitee.com/hwm0717/springcloud_seata_demo
4个微服务提供支持:

  • 库存服务(storage):扣除给定商品的存储数量。
  • 订单服务(order):根据购买请求创建订单。
  • 帐户服务(account):借记用户帐户的余额。
  • 业务服务(business):处理业务逻辑。
  • 公共模块(common):公共模块。

我们只需要在业务处理的方法handleBusiness添加一个注解 @GlobalTransactional 即可实现分布式事物的处理

请求逻辑架构:
在这里插入图片描述
pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>seata.demo</groupId>
    <artifactId>springcloud_seata_demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
        <springboot.version>2.2.5.RELEASE</springboot.version>
        <mysql.connector.java.version>8.0.21</mysql.connector.java.version>
        <druid-spring-boot-starter.version>1.1.22</druid-spring-boot-starter.version>

        <seata.version>1.4.0</seata.version>

        <!-- Spring Settings -->
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <modules>
        <module>business</module>
        <module>account</module>
        <module>order</module>
        <module>storage</module>
        <module>common</module>
    </modules>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

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

        <!--  springboot 整合web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- mybatisplus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>

        <!-- mybatis分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.12</version>
        </dependency>

        <!--引入数据库连接 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.connector.java.version}</version>
        </dependency>

        <!-- 使用 druid数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!-- swagger整合:https://github.com/SpringForAll/spring-boot-starter-swagger -->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>swagger-spring-boot-starter</artifactId>
            <version>1.7.0.RELEASE</version>
        </dependency>

        <!-- 项目中引入seata依赖 begin -->
        <!-- 如果你的微服务是dubbo引入依赖 -->
<!--        <dependency>-->
<!--            <groupId>io.seata</groupId>-->
<!--            <artifactId>seata-spring-boot-starter</artifactId>-->
<!--            <version>${seata.version}</version>-->
<!--        </dependency>-->

        <!-- 如果你是springcloud:-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-seata</artifactId>
            <version>2.2.0.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>${seata.version}</version>
        </dependency>
        <!-- 项目中引入seata依赖 end -->

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${springboot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

1,库存服务:

@Data
public class TStorage {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String commodityCode;
    private String name;
    private Integer count;

}
public interface TStorageMapper extends BaseMapper<TStorage> {

    /**
     * 扣减商品库存
     * @Param: commodityCode 商品code  count扣减数量
     * @Return:
     */
    int decreaseStorage(@Param("commodityCode") String commodityCode, @Param("count") Integer count);
}
@RestController
@RequestMapping("/storage")
@Slf4j
public class StorageServiceImpl extends ServiceImpl<TStorageMapper, TStorage> {

    @PostMapping("/decreaseStorage")
    @GlobalTransactional
    @Transactional
    public ObjectResponse decreaseStorage(@RequestBody CommodityDTO commodityDTO) {

        log.info("seata全局xid={}", RootContext.getXID());

        int storage = baseMapper.decreaseStorage(commodityDTO.getCommodityCode(), commodityDTO.getCount());

        ObjectResponse<Object> response = new ObjectResponse<>();
        if (storage > 0) {
            response.setStatus(RspStatusEnum.SUCCESS.getCode());
            response.setMessage(RspStatusEnum.SUCCESS.getMessage());
            return response;
        }

        response.setStatus(RspStatusEnum.FAIL.getCode());
        response.setMessage(RspStatusEnum.FAIL.getMessage());
        return response;
    }

}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.storage.mappers")
public class StorageApplication {

    public static void main(String[] args) {
        SpringApplication.run(StorageApplication.class, args);
    }

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="seata.demo.storage.mappers.TStorageMapper">

    <update id="decreaseStorage">
        update t_storage set count = count-${count} where commodity_code = #{commodityCode}
    </update>

</mapper>

2,订单服务

@Data
public class TOrder {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String orderNo;
    private String userId;
    private String commodityCode;
    private Integer count;
    private Double amount;

}

public interface TOrderMapper extends BaseMapper<TOrder> {

    /**
     * 创建订单
     * @Param:  order 订单信息
     * @Return:
     */
    void createOrder(@Param("order") TOrder order);
}

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> {

    @Autowired
    private AccountServiceFeign accountServiceFeign;

    @PostMapping("/createOrder")
    @GlobalTransactional
    @Transactional
    public ObjectResponse<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO) {

        log.info("seata全局xid={}", RootContext.getXID());

        ObjectResponse<OrderDTO> response = new ObjectResponse<>();
        //扣减用户账户
        AccountDTO accountDTO = new AccountDTO();
        accountDTO.setUserId(orderDTO.getUserId());
        accountDTO.setAmount(orderDTO.getOrderAmount());
        ObjectResponse objectResponse = accountServiceFeign.decreaseAccount(accountDTO);

        //生成订单号
        orderDTO.setOrderNo(UUID.randomUUID().toString().replace("-",""));
        //生成订单
        TOrder tOrder = new TOrder();
        BeanUtils.copyProperties(orderDTO,tOrder);
        tOrder.setCount(orderDTO.getOrderCount());
        tOrder.setAmount(orderDTO.getOrderAmount().doubleValue());
        try {
            baseMapper.createOrder(tOrder);
        } catch (Exception e) {
            e.printStackTrace();
            response.setStatus(RspStatusEnum.FAIL.getCode());
            response.setMessage(RspStatusEnum.FAIL.getMessage());
            return response;
        }

        if (objectResponse.getStatus() != 200) {
            response.setStatus(RspStatusEnum.FAIL.getCode());
            response.setMessage(RspStatusEnum.FAIL.getMessage());
            return response;
        }

        response.setStatus(RspStatusEnum.SUCCESS.getCode());
        response.setMessage(RspStatusEnum.SUCCESS.getMessage());
        return response;
    }

}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.order.mappers")
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="seata.demo.order.mappers.TOrderMapper">

    <!--创建订单-->
    <insert id="createOrder" parameterType="seata.demo.order.entity.TOrder">
      insert into t_order values(null,#{order.orderNo},#{order.userId},#{order.commodityCode},${order.count},${order.amount})
    </insert>

</mapper>

3,账户服务

@Data
public class TAccount {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private String userId;
    private Double amount;

}
public interface TAccountMapper extends BaseMapper<TAccount> {

    /**
     * 减少账户余额
     * @param userId
     * @param amount
     * @return
     */
    int decreaseAccount(@Param("userId") String userId, @Param("amount") Double amount);
}

@RestController
@RequestMapping("/account")
@Slf4j
public class AccountServiceImpl extends ServiceImpl<TAccountMapper, TAccount> {

    @PostMapping("/decreaseAccount")
    @GlobalTransactional
    @Transactional
    public ObjectResponse decreaseAccount(@RequestBody AccountDTO accountDTO) {

        log.info("seata全局xid={}", RootContext.getXID());

        int account = baseMapper.decreaseAccount(accountDTO.getUserId(), accountDTO.getAmount().doubleValue());
        ObjectResponse<Object> response = new ObjectResponse<>();
        if (account > 0) {
            response.setStatus(RspStatusEnum.SUCCESS.getCode());
            response.setMessage(RspStatusEnum.SUCCESS.getMessage());
            return response;
        }

        response.setStatus(RspStatusEnum.FAIL.getCode());
        response.setMessage(RspStatusEnum.FAIL.getMessage());
        return response;
    }
}
@SpringBootApplication
@EnableFeignClients
@MapperScan("seata.demo.account.mappers")
public class AccountApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="seata.demo.account.mappers.TAccountMapper">

    <update id="decreaseAccount">
      update t_account set amount = amount-${amount} where user_id = #{userId}
    </update>

</mapper>

4,业务服务

@RestController
@RequestMapping("/business")
@Slf4j
public class BusinessServiceImpl {

    @Autowired
    private StorageServiceFeign storageServiceFeign;
    @Autowired
    private OrderServiceFeign orderServiceFeign;

    /**
     * 处理业务逻辑 正常的业务逻辑
     *
     * @Param:
     * @Return:
     */
    @GlobalTransactional
    @PostMapping("/handleBusiness")
    @ApiOperation("处理业务逻辑 正常的业务逻辑")
    public ObjectResponse handleBusiness(@RequestBody BusinessDTO businessDTO) {
        log.info("开始全局事务,XID = " + RootContext.getXID());

        ObjectResponse<Object> objectResponse = new ObjectResponse<>();
        //1、扣减库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse storageResponse = storageServiceFeign.decreaseStorage(commodityDTO);
        //2、创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse<OrderDTO> response = orderServiceFeign.createOrder(orderDTO);

        if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
            throw new DefaultException(RspStatusEnum.FAIL);
        }

        objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
        objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
        objectResponse.setData(response.getData());
        return objectResponse;
    }

    /**
     * 出处理业务服务,出现异常回顾
     *
     * @param businessDTO
     * @return
     */
    @GlobalTransactional
    @PostMapping("/handleBusiness2")
    @ApiOperation("出处理业务服务,出现异常回顾")
    @Transactional
    public ObjectResponse handleBusiness2(@RequestBody BusinessDTO businessDTO) {
        log.info("开始全局事务,XID = " + RootContext.getXID());
        ObjectResponse<Object> objectResponse = new ObjectResponse<>();
        //1、扣减库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse storageResponse = storageServiceFeign.decreaseStorage(commodityDTO);
        //2、创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse<OrderDTO> response = orderServiceFeign.createOrder(orderDTO);

//        打开注释测试事务发生异常后,全局回滚功能
        if (true) {
            throw new RuntimeException("测试抛异常后,分布式事务回滚!");
        }

        if (storageResponse.getStatus() != 200 || response.getStatus() != 200) {
            throw new DefaultException(RspStatusEnum.FAIL);
        }

        objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
        objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
        objectResponse.setData(response.getData());
        return objectResponse;
    }

}
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableSwagger2Doc
@EnableFeignClients
public class BusinessApplication {

    public static void main(String[] args) {
        SpringApplication.run(BusinessApplication.class, args);
    }

}

5,公共模块

@Data
public class AccountDTO implements Serializable {

    private Integer id;

    private String userId;

    private BigDecimal amount;
}

@Data
public class BusinessDTO implements Serializable {

    private String userId;

    private String commodityCode;

    private String name;

    private Integer count;

    private BigDecimal amount;

}
@Data
public class CommodityDTO implements Serializable {

    private Integer id;

    private String commodityCode;

    private String name;

    private Integer count;
}
@Data
public class OrderDTO implements Serializable {

    private String orderNo;

    private String userId;

    private String commodityCode;

    private Integer orderCount;

    private BigDecimal orderAmount;

}
public enum RspStatusEnum {
    /**
     * SUCCESS
     */
    SUCCESS(200,"成功"),
    /**
     * Fail rsp status enum.
     */
    FAIL(999,"失败"),
    /**
     * Exception rsp status enum.
     */
    EXCEPTION(500,"系统异常");

    private int code;

    private String message;

    RspStatusEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * Gets code.
     *
     * @return the code
     */
    public int getCode() {
        return code;
    }

    /**
     * Gets message.
     *
     * @return the message
     */
    public String getMessage() {
        return message;
    }
}

public class DefaultException extends RuntimeException{

    private RspStatusEnum rspStatusEnum;

    public DefaultException(String message, Throwable cause) {
        super(message, cause);
    }

    public DefaultException(RspStatusEnum rspStatusEnum) {
        super(rspStatusEnum.getMessage());
        this.rspStatusEnum = rspStatusEnum;
    }

    public DefaultException(RspStatusEnum rspStatusEnum, Throwable cause) {
        super(rspStatusEnum.getMessage(), cause);
        this.rspStatusEnum = rspStatusEnum;
    }

    public RspStatusEnum getRspStatusEnum() {
        return rspStatusEnum;
    }

    public void setRspStatusEnum(RspStatusEnum rspStatusEnum) {
        this.rspStatusEnum = rspStatusEnum;
    }
}

@FeignClient(value = "account", path = "/account")
public interface AccountServiceFeign {

    /**
     * 从账户扣钱
     */
    @ApiOperation("从账户扣钱")
    @PostMapping("/decreaseAccount")
    ObjectResponse decreaseAccount(@RequestBody AccountDTO accountDTO);
}

@FeignClient(value = "order", path = "/order")
public interface OrderServiceFeign {

    /**
     * 创建订单
     */
    @ApiOperation("创建订单")
    @PostMapping("/createOrder")
    ObjectResponse<OrderDTO> createOrder(@RequestBody OrderDTO orderDTO);
}

@FeignClient(value = "storage", path = "/storage")
public interface StorageServiceFeign {

    /**
     * 扣减库存
     */
    @ApiOperation("扣减库存")
    @PostMapping("/decreaseStorage")
    ObjectResponse decreaseStorage(@RequestBody CommodityDTO commodityDTO);
}

@Data
public class BaseResponse implements Serializable {

    private int status = 200;

    private String message;
}

@Data
public class ObjectResponse<T> extends BaseResponse implements Serializable {

    private T data;

}

6,配置文件
主要三个配置文件,每个应用里面复制一份,内容基本一致,只需要根据自己配置改下扫描的包和应用名称即可

application-global.properties


#服务端口号
server.port=8080

spring.profiles.include=global,seata

#服务名称
spring.application.name=business

#nacos
spring.cloud.nacos.discovery.server-addr=192.168.2.112:8848

#日志配置
logging.level.root=info
logging.file.path=C:\\www\\logs\\${spring.application.name}

#数据库连接信息
spring.datasource.url=jdbc:mysql://192.168.2.112:3306/springcloud_seata_demo?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456

#swagger
swagger.enabled=true
swagger.title=业务逻辑处理
swagger.description=业务逻辑处理
swagger.version=1.0.0
swagger.base-package=seata.demo.business.service

#设置feign超时时间,解决第一次调用时超时问题
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=5000

application-seata.yml

#====================================Seata Config===============================================
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: ${spring.application.name}-seata-tx-group # 事务群组(可以每个应用独立取名,也可以使用相同的名字)
  registry:
    file:
      name: file.conf
    type: nacos
    nacos:
      server-addr: 192.168.2.112:8848
      namespace:
      cluster: default
  config:
    file:
      name: file.conf
    type: nacos
    nacos:
      namespace:
      server-addr: 192.168.2.112:8848

application.properties


#服务端口号
server.port=8080

#引入外部配置文件
spring.profiles.include=global,seata

#服务名称
spring.application.name=business

#nacos
spring.cloud.nacos.discovery.server-addr=192.168.2.112:8848

#日志配置
logging.level.root=info
logging.file.path=C:\\www\\logs\\${spring.application.name}

#数据库连接信息
spring.datasource.url=jdbc:mysql://192.168.2.112:3306/springcloud_seata_demo?useUnicode=true&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456

#swagger
swagger.enabled=true
swagger.title=业务逻辑处理
swagger.description=业务逻辑处理
swagger.version=1.0.0
swagger.base-package=seata.demo.business.service

#设置feign超时时间,解决第一次调用时超时问题
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=5000

7,准备数据库
注意: MySQL必须使用InnoDB engine.
创建数据库 并导入数据库脚本
这里为了简化我将这个三张表创建到一个库中,使用是三个数据源来实现。

DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `amount` double(14,2) DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES ('1', '1', '4000.00');

-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(255) DEFAULT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `amount` double(14,2) DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_order
-- ----------------------------

-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES ('1', 'C201901140001', '水杯', '1000');

-- ----------------------------
-- Table structure for undo_log
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
-- ----------------------------
DROP TABLE IF EXISTS `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;

-- ----------------------------
-- Records of undo_log
-- ----------------------------
SET FOREIGN_KEY_CHECKS=1;

三、测试

使用swagger发送一个下单请求,调用 /business/handleBusiness 方法:http://localhost:8080/swagger-ui.html#/business-service-impl/handleBusinessUsingPOST
参数内容:

{
  "amount": 100,
  "commodityCode": "C201901140001",
  "count": 100,
  "name": "水杯",
  "userId": "1"
}

返回结果:

{
  "status": 200,
  "message": "成功",
  "data": null
}

在这里插入图片描述

控制台打印
在这里插入图片描述

在这里插入图片描述
事物提交成功,查看数据变化
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
数据没有问题,全部按照正常流程提交了

2,测试数据回滚:
参数一样,通过swagger调用 /business/handleBusiness2 异常回滚方法
返回结果:
在这里插入图片描述
控制台输出
在这里插入图片描述
在这里插入图片描述
再次查看数据库数据,已经回滚,和上面的数据一致。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值