一、案例演示:业务系统集成 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
- 查看订单表,会发现没有再次生成订单
- 查看库存表,会发现库存并没有减少
- 查看账户表,会发现账户消费金额并没有变化