Seata分布式事务组件案例演示
一、案例演示:业务系统集成 Seata
以电商系统为例,来演示下业务系统是如何集成 Seata 的。在电商系统中,用户下单购买一件商品,需要以下 3 个服务提供支持:
- Order(订单服务):创建和修改订单。
- Storage(库存服务):对指定的商品扣除仓库库存。
- Account(账户服务) :从用户帐户中扣除商品金额。
这三个微服务分别使用三个不同的数据库,架构图:

当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:
- 调用 Order 服务,创建一条订单数据,订单状态为“未完成”;
- 调用 Storage 服务,扣减商品库存;
- 调用 Account 服务,从用户账户中扣除商品金额;
- 调用 Order 服务,将订单状态修改为“已完成”。
二、库存服务
1. 在 MySQL 数据库中,新建一个名为 seata-storage 的数据库实例
通过SQL 语句创建 2 张表:t_storage(库存表)和 undo_log(回滚日志表),代码:
-- ---------------------------- -- Table structure for t_storage -- ---------------------------- DROP TABLE IF EXISTS `t_storage`; CREATE TABLE `t_storage` ( `id` bigint NOT NULL AUTO_INCREMENT, `product_id` bigint DEFAULT NULL COMMENT '产品id', `total` int DEFAULT NULL COMMENT '总库存', `used` int DEFAULT NULL COMMENT '已用库存', `residue` int DEFAULT NULL COMMENT '剩余库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_storage -- ---------------------------- INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100'); -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';
2. 创建一个名为 cloud-alibaba-seata-storage-8006 的 Spring Boot 模块
在其 pom.xml 中添加以下依赖,内容如下:
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<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>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.augus.springcloud</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
3. 在 cloud-alibaba-seata-storage-8006 的类路径(/resources 目录)下
创建一个配置文件 application.yml,配置内容如下:
#端口 server: port: 8006 spring: application: name: cloud-alibabab-seata-storage-8006 cloud: # 指定 nacos nacos: discovery: server-addr: localhost:8848 # nacos 服务器地址 username: nacos password: nacos sentinel: transport: dashboard: 127.0.0.1:8080 #Sentinel 控制台地址 port: 8719 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata-storage?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 123456 #开启 OpenFeign 功能 feign: hystrix: enabled: false logging: level: io: seata: info seata: tx-service-group: fsp_tx_group #这里每个服务都是对应不同的映射名,在配置中心可以看到 # 注册方式为nacos registry: type: nacos nacos: server-addr: localhost:8848 group: SEATA_GROUP service: vgroup-mapping: #这里也要注意 key为映射名,事务分组 fsp_tx_group: default ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: com.augus.cloud.pojo configuration: #默认开启驼峰命名法,可以不用设置该属性 map-underscore-to-camel-case: true
4. 创建主启动类
主启动类:CloudAlibabaSeataStorage8006Application 代码如下:
package com.augus.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient //让注册中心能够发 @EnableFeignClients //启用openFeign客户端 public class CloudAlibabaSeataStorage8006Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaSeataStorage8006Application.class, args); } }
5. 在 com.augus.cloud.pojo包下,创建一个名为 Storage 的实体类
对应之前创建的表storage
package com.augus.cloud.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class Storage { private Long id; private Long productId; private Integer total; private Integer used; private Integer residue; }
6. 在 com.augus.cloud.dao包下,创建一个名为 StorageMapper 的接口
在其中创建 减扣库存的服务:
package com.augus.cloud.dao; import com.augus.cloud.pojo.Storage; import org.apache.ibatis.annotations.Mapper; @Mapper public interface StorageMapper { Storage selectByProductId(Long productId); int decrease(Storage record); }
7. 在 /resouces 目录下创建mapper 目录
在其中创建一个名为 StorageMapper.xml 的 MyBatis 映射文件,代码如下:
<?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="com.augus.cloud.dao.StorageMapper">
<resultMap id="BaseResultMap" type="com.augus.cloud.pojo.Storage">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="product_id" jdbcType="BIGINT" property="productId"/>
<result column="total" jdbcType="INTEGER" property="total"/>
<result column="used" jdbcType="INTEGER" property="used"/>
<result column="residue" jdbcType="INTEGER" property="residue"/>
</resultMap>
<sql id="Base_Column_List">
id
, product_id, total, used, residue
</sql>
<update id="decrease" parameterType="com.augus.cloud.pojo.Storage">
update t_storage
<set>
<if test="total != null">
total = #{total,jdbcType=INTEGER},
</if>
<if test="used != null">
used = #{used,jdbcType=INTEGER},
</if>
<if test="residue != null">
residue = #{residue,jdbcType=INTEGER},
</if>
</set>
where product_id = #{productId,jdbcType=BIGINT}
</update>
<select id="selectByProductId" parameterType="java.lang.Long" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from t_storage
where product_id = #{productId,jdbcType=BIGINT}
</select>
</mapper>
8. 在 com.augus.cloud.service 包下,创建一个名为 StorageService 的 Service 接口
实现扣减库存,代码如下:
package com.augus.cloud.service; public interface StorageService { int decrease(Long productId, Integer count); }
9. 在 com.augus.cloud.service.impl 包下创建 StorageService 的实现类 StorageServiceImpl,代码如下:
package com.augus.cloud.service.impl; import com.augus.cloud.dao.StorageMapper; import com.augus.cloud.pojo.Storage; import com.augus.cloud.service.StorageService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class StorageServiceImpl implements StorageService { @Resource StorageMapper storageMapper; @Override public int decrease(Long productId, Integer count) { log.info("------->storage-service中扣减库存开始"); log.info("------->storage-service 开始查询商品是否存在"); Storage storage = storageMapper.selectByProductId(productId); if (storage != null && storage.getResidue() >= count) { Storage storage2 = new Storage(); storage2.setProductId(productId); storage.setUsed(storage.getUsed() + count); storage.setResidue(storage.getTotal() - storage.getUsed()); int decrease = storageMapper.decrease(storage); log.info("------->storage-service 扣减库存成功"); return decrease; } else { log.info("------->storage-service 库存不足,开始回滚!"); throw new RuntimeException("库存不足,扣减库存失败!"); } } }
10. 在 com.augus.cloud.controller 包下,创建一个名为 StorageController 的 Controller 类,代码如下。
package com.augus.cloud.controller; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.service.StorageService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class StorageController { @Resource private StorageService storageService; @Value("${server.port}") private String serverPort; @PostMapping(value = "/storage/decrease") public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) { int decrease = storageService.decrease(productId, count); CommonResult result; if (decrease > 0) { result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease); } else { result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "库存扣减失败"); } return result; } }
三、账户服务
1. 在 MySQL 数据库中,新建 seata-account 的数据库,通过SQL 语句创建 2 张表:t_account(账户表)和 undo_log(回滚日志表)
DROP TABLE IF EXISTS `t_account`; CREATE TABLE `t_account` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint DEFAULT NULL COMMENT '用户id', `total` decimal(10,0) DEFAULT NULL COMMENT '总额度', `used` decimal(10,0) DEFAULT NULL COMMENT '已用余额', `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用额度', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of t_account -- ---------------------------- INSERT INTO `t_account` VALUES ('1', '1', '1000', '0', '1000'); -- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8;
2. 创建一个名为 cloud-alibaba-seata-account-8007 的 Spring Boot 模块,并在其 pom.xml 中添加以下依赖
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<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>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.augus.springcloud</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
3. 在 cloud-alibaba-seata-account-8007 的resources 目录下,创建一个配置文件 application.yml,配置内容如下
#端口 server: port: 8007 spring: application: name: cloud-alibaba-seata-account-8007 #服务名 cloud: # 指定 nacos nacos: discovery: server-addr: localhost:8848 # nacos 服务器地址 username: nacos password: nacos sentinel: transport: dashboard: 127.0.0.1:8080 #Sentinel 控制台地址 port: 8719 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata-account?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 123456 #开启 OpenFeign 功能 feign: hystrix: enabled: false logging: level: io: seata: info seata: tx-service-group: fsp_tx_group #这里每个服务都是对应不同的映射名,在配置中心可以看到 # 注册方式为nacos registry: type: nacos nacos: server-addr: localhost:8848 group: SEATA_GROUP service: vgroup-mapping: #这里也要注意 key为映射名,事务分组 fsp_tx_group: default ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: com.augus.cloud.pojo configuration: #默认开启驼峰命名法,可以不用设置该属性 map-underscore-to-camel-case: true
4. 创建主启动类
名为:CloudAlibabaSeataAccount8007Application
package com.augus.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableDiscoveryClient @EnableFeignClients @SpringBootApplication public class CloudAlibabaSeataAccount8007Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaSeataAccount8007Application.class,args); } }
5. 在 cloud.augus.cloud.pojo包下,创建一个名为 Account 的实体类,代码如下。
package com.augus.cloud.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data @AllArgsConstructor @NoArgsConstructor public class Account { private Long id; private Long userId; private BigDecimal total; private BigDecimal used; private BigDecimal residue; }
6. 在 com.augus.cloud.dao包下,创建一个名为 AccountMapper 的接口,代码如下。
package com.augus.cloud.dao; import com.augus.cloud.pojo.Account; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; @Mapper public interface AccountMapper { Account selectByUserId(Long userId); int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money); }
7. 在 resouces下创建mapper 目录,载里面创建一个名为 AccountMapper.xml 的 MyBatis 映射文件,代码如下:
<?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="com.augus.cloud.dao.AccountMapper">
<resultMap id="BaseResultMap" type="com.augus.cloud.pojo.Account">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="user_id" jdbcType="BIGINT" property="userId"/>
<result column="total" jdbcType="DECIMAL" property="total"/>
<result column="used" jdbcType="DECIMAL" property="used"/>
<result column="residue" jdbcType="DECIMAL" property="residue"/>
</resultMap>
<sql id="Base_Column_List">
id
, user_id, total, used, residue
</sql>
<select id="selectByUserId" resultType="com.augus.cloud.pojo.Account">
select
<include refid="Base_Column_List"/>
from t_account
where user_id = #{userId,jdbcType=BIGINT}
</select>
<update id="decrease">
UPDATE t_account
SET residue = residue - #{money},
used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
8. 在 com.augus.cloud.service 包下,创建一个名为 AccountService 的接口,代码如下:
package com.augus.cloud.service; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; public interface AccountService { /** * 扣减账户余额 * * @param userId 用户id * @param money 金额 */ int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
9. 在 com.augus.cloud.service.impl 包下,创建 AccountService 的实现类 AccountServiceImpl,代码如下:
package com.augus.cloud.service.impl; import com.augus.cloud.dao.AccountMapper; import com.augus.cloud.pojo.Account; import com.augus.cloud.service.AccountService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; @Service @Slf4j public class AccountServiceImpl implements AccountService { @Resource AccountMapper accountMapper; @Override public int decrease(Long userId, BigDecimal money) { log.info("------->account-service 开始查询账户余额"); Account account = accountMapper.selectByUserId(userId); log.info("------->account-service 账户余额查询完成," + account); if (account != null && account.getResidue().intValue() >= money.intValue()) { log.info("------->account-service 开始从账户余额中扣钱!"); int decrease = accountMapper.decrease(userId, money); log.info("------->account-service 从账户余额中扣钱完成"); return decrease; } else { log.info("账户余额不足,开始回滚!"); throw new RuntimeException("账户余额不足!"); } } }
10. 在 com.augus.cloud.controller 包下,创建一个名为AccountController的 Controller 类,代码如下。
package com.augus.cloud.controller; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.service.AccountService; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; @RestController public class AccountController { @Resource private AccountService accountService; @Value("${server.port}") private String serverPort; @PostMapping(value = "/account/decrease") public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) throws InterruptedException { //保持返回值 CommonResult result;//完成扣款操作 int decrease = accountService.decrease(userId, money); if (decrease > 0) { result = new CommonResult(200, "from mysql,serverPort: " + serverPort, decrease); } else { result = new CommonResult(505, "from mysql,serverPort: " + serverPort, "金额扣减失败"); } return result; }; }
四、创建订单(Order)服务
1. 在 MySQL 数据库中,新建数据库 seata-order ,在其中创建 2 张表:t_order(订单表)和 undo_log(回滚日志表)
直接创建数据库后,执行下面SQL即可
-- ---------------------------- -- Table structure for t_order -- ---------------------------- DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL COMMENT '用户id', `product_id` bigint DEFAULT NULL COMMENT '产品id', `count` int DEFAULT NULL COMMENT '数量', `money` decimal(11,0) DEFAULT NULL COMMENT '金额', `status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for undo_log -- ---------------------------- DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `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 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 DEFAULT CHARSET=utf8 ;
2. 创建一个名为 cloud-alibaba-seata-order-8005 的 Spring Boot 模块
并在其 pom.xml 中添加以下依赖,内容如下:
<dependencies>
<!--nacos 服务注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入 OpenFeign 的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<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>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.augus.springcloud</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--添加 Spring Boot 的监控模块-->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--配置中心 nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
3. 在 spring-cloud-alibaba-seata-order-8005 的类路径(/resources 目录)下,创建一个配置文件 application.yml,
这里一定注意事务群组的名字需要和nacos配置中心创建的seata一致,如下:
#端口 server: port: 8005 spring: application: name: cloud-alibabab-seata-order-8005 cloud: # 指定 nacos nacos: discovery: server-addr: localhost:8848 # nacos 服务器地址 sentinel: transport: dashboard: 127.0.0.1:8080 #Sentinel 控制台地址 port: 8719 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata-order?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 123456 #开启 OpenFeign 功能 feign: hystrix: enabled: false logging: level: io: seata: info seata: registry: type: nacos nacos: server-addr: localhost:8848 group: SEATA_GROUP #tx-service-group: default_tx_group #这里每个服务都是对应不同的映射名,在配置中心可以看到 service: vgroup-mapping: default_tx_group: default #TC 集群(必须和seata-server保持一致) # 事务群组(可以每个应用独立名字,也可以单独的名字,注意跟配置文件中的保持一致) tx-service-group: default_tx_group ###################################### MyBatis 配置 ###################################### mybatis: # 指定 mapper.xml 的位置 mapper-locations: classpath:mapper/*.xml #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名 type-aliases-package: com.augus.cloud.pojo configuration: #默认开启驼峰命名法,可以不用设置该属性 map-underscore-to-camel-case: true
4. 在 com.augus.cloud包下,创建一个名为主启动类
创建名为:CloudAlibabaSeataOrder8005Application 的主启动类
package com.augus.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient //让注册中心能够发 @EnableFeignClients //启用openFeign客户端 public class CloudAlibabaSeataOrder8005Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaSeataOrder8005Application.class, args); } }
5. 在 com.augus.cloud.pojo包下,创建一个名为 Order 的实体类,
这个实体类对应之前创建的order表,代码如下:
package com.augus.cloud.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data @AllArgsConstructor @NoArgsConstructor public class Order { private Long id; private Long userId; private Long productId; private Integer count; private BigDecimal money; private Integer status; }
6. 在 com.augus.cloud.dao包下,创建一个名为 OrderMapper 的 Mapper 接口,代码如下。
这里定义两个接口:创建订单、修改订单状态:
package com.augus.cloud.dao; import com.augus.cloud.pojo.Order; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @Mapper public interface OrderMapper { /** * 1.创建订单 * @param order * @return */ int create(Order order); //2 修改订单状态,从零改为1 void update(@Param("userId") Long userId, @Param("status") Integer status); }
7. 在 /resouces/目录下创建/mapper 目录
创建一个名为 OrderMapper.xml 的 MyBatis 映射文件,代码如下:
<?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="com.augus.cloud.dao.OrderMapper">
<insert id="create" parameterType="com.augus.cloud.pojo.Order">
insert into t_order (user_id, product_id,
count, money, status)
values (#{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
#{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
</insert>
<update id="update">
update t_order
set status = 1
where user_id = #{userId}
and status = #{status};
</update>
</mapper>
8. 在 com.augus.cloud.service 包下,创建一个名为 OrderService 的 Service 接口,代码如下。
这个接口是用来完成创建订单的,减库存、减账户余额,这个操作的
package com.augus.cloud.service; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.pojo.Order; public interface OrderService { /** * 创建订单数据 * @param order */ CommonResult create(Order order); }
9. 在 com.augus.cloud.service 包下,创建一个名为 StorageService 的接口。
这里使用的是openFegin去调用8006库存服务,进行库存的减扣
package com.augus.cloud.service; import com.augus.cloud.cloud.pojo.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "cloud-alibabab-seata-storage-8006") //指定调用的服务 public interface StorageService { @PostMapping(value = "/storage/decrease") CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count); }
10. 在 com.augus.cloud.service 包下,创建一个名为 AccountService 的接口
这里使用的是openFegin去调用8007账户服务,进行余额的减扣:
package com.augus.cloud.service; import com.augus.cloud.cloud.pojo.CommonResult; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; @FeignClient(value = "cloud-alibaba-seata-account-8007") public interface AccountService { @PostMapping(value = "/account/decrease") CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money); }
11. 在 com.augus.cloud.service.impl 包下,创建 OrderService 接口的实现类 OrderServiceImpl
在这个实现类中就要完成创建订单、减库存、减金额、修改状态的操作
package com.augus.cloud.service.impl; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.dao.OrderMapper; import com.augus.cloud.pojo.Order; import com.augus.cloud.service.AccountService; import com.augus.cloud.service.OrderService; import com.augus.cloud.service.StorageService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说:下订单->扣库存->减余额->改订单状态 */ @Override public CommonResult create(Order order) { log.info("----->开始新建订单"); //1 新建订单 order.setUserId(new Long(1)); order.setStatus(0); orderMapper.create(order); //2 扣减库存 log.info("----->订单服务开始调用库存服务,开始扣减库存"); storageService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,扣减库存结束"); //3 扣减账户 log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,账户扣减商品金额结束"); //4 修改订单状态,从零到1,1代表已经完成 log.info("----->修改订单状态开始"); orderMapper.update(order.getUserId(), 0); log.info("----->修改订单状态结束"); log.info("----->下订单结束了------->"); return new CommonResult(200, "订单创建成功"); } }
12. 在 com.augus.cloud.controller 包下,创建一个名为 OrderController 的 Controller
代码如下:
package com.augus.cloud.controller; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.pojo.Order; import com.augus.cloud.service.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal; @RestController public class OrderController { @Resource private OrderService orderService; @GetMapping(value = "/order/create/{productId}/{count}/{money}") public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count , @PathVariable("money") BigDecimal money) { Order order = new Order(); order.setProductId(Integer.valueOf(productId).longValue()); order.setCount(count); order.setMoney(money); return orderService.create(order); } }
五、测试
5.1.环境说明
订单表:刚开始为空

库存表:目前库存100件,剩余100件商品,卖出0件

账户: 1号用户,账户总计1000元,余额为1000,消费0

然后依次启动 Nacos Server、sentinel和Seata Server,依次启动 cloud-alibaba-seata-storage-8006、cloud-alibaba-seata-account-8007、cloud-alibaba-seata-order-8005、
5.2.测试
模拟 实现1号用户,买了2个产品、花了20块
5.2.1.在浏览器访问:http://localhost:8005/order/create/1/2/20

5.2.2.查看订单表中变化
会发现创建了一条订单:

5.2.3.查看库存表中变化
会发现库存减少了两个:

5.2.4.查看金额表中变化
会发现金额减少了20:

5.3.模拟异常情况
5.3.1.在账户服务AccountController 中添加线程等待模拟超时
如下图所示:

代码如下:
//模拟超时 2000S TimeUnit.SECONDS.sleep(2000);
5.3.2.重启8006、8007和8005服务
然后 浏览器访问 http://localhost:8005/order/create/1/3/30,模拟 1号用户,买了3个产品、花了30块钱,如下图8007出现了超时异常

5.3.3.查看订单表中变化
会发现创建了一条订单,但是订单状态依然为0,并没有改变:

5.3.4.查看库存表中变化
会发现库存减了3

5.3.5.查看账户表中变化
会发现账户并没有减少

通过上面就发现,订单生成了但是状态依然是未支付,账户的金额没有变化,但是库存缺减少了,这种情况下就要求订单服务、账户服务、库存服务绑定到一起,要不全部成功,要么都不成功,如果其中一个出现了异常,就几个服务都进行回滚操作。
5.4.@GlobalTransactional 注解
在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。
@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:
- 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
- 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。
5.4.1.只需要在 cloud-alibaba-seata-order-8005 的 com.augus.cloud.service.impi 包下的 OrderServiceImpl中,创建订单的方法添加@GlobalTransactional 注解即可,代码如下。
需要注意的@GlobalTransactional 注解name属性的值随便写,只要唯一即可
package com.augus.cloud.service.impl; import com.augus.cloud.cloud.pojo.CommonResult; import com.augus.cloud.dao.OrderMapper; import com.augus.cloud.pojo.Order; import com.augus.cloud.service.AccountService; import com.augus.cloud.service.OrderService; import com.augus.cloud.service.StorageService; import io.seata.spring.annotation.GlobalTransactional; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderMapper orderMapper; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态 * 简单说:下订单->扣库存->减余额->改订单状态 */ @Override @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) public CommonResult create(Order order) { log.info("----->开始新建订单"); //1 新建订单 order.setUserId(new Long(1)); order.setStatus(0); orderMapper.create(order); //2 扣减库存 log.info("----->订单服务开始调用库存服务,开始扣减库存"); storageService.decrease(order.getProductId(), order.getCount()); log.info("----->订单微服务开始调用库存,扣减库存结束"); //3 扣减账户 log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额"); accountService.decrease(order.getUserId(), order.getMoney()); log.info("----->订单微服务开始调用账户,账户扣减商品金额结束"); //4 修改订单状态,从零到1,1代表已经完成 log.info("----->修改订单状态开始"); orderMapper.update(order.getUserId(), 0); log.info("----->修改订单状态结束"); log.info("----->下订单结束了------->"); return new CommonResult(200, "订单创建成功"); } }
5.4.2.依次重启服务8006、8007和8005
待重启完成后,浏览器再次访问http://localhost:8005/order/create/1/3/30
- 查看订单表,会发现没有再次生成订单

- 查看库存表,会发现库存并没有减少

- 查看账户表,会发现账户消费金额并没有变化


浙公网安备 33010602011771号