案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples
Spring Cloud Alibaba Seata
一、Seata 简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双 11,对各 BU 业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。
二、Seata-Server 的安装
在使用 Seata 之前,我们首先要安装 Seata-Server 服务器。
2.1 下载 Seata
由于我们使用的 spring cloud alibaba 版本为 2.2.0.RELEASE,他里面控制了 seata 的版本为1.0.0,故我们在此下载 1.0.0 版本的 seata。访问:https://github.com/seata/seata/releases/tag/v1.0.0
由于我使用的是 windows 的电脑,故选择 seata-server-1.0.0.zip 该版本。
点击该文件下载。
当下载速度较慢时,大家可以从百度云里面去下载。
链接:https://pan.baidu.com/s/1w4aMASZ3AeREbJSUR5se0Q 提取码:o041
2.2 Seata-Server 目录分析
将 seata-server 复制到软件的目录里面,使用解压工具解压该文件。
Bin:可执行文件目录
Conf:配置文件目录
lib:依赖的 jar
LICENSE:授权文件
2.3 Seata 启动
进入{seata}/bin 目录里面,双击:
代表 seata-server 已经启动成功。
三、框架的搭建
在本示例中,我们模拟了一个用户购买货物的场景:
- StorageService 负责扣减库存数量;
- OrderService 负责保存订单;
- AccountService 负责扣减用户账户余额;
- Business 负责用户下单的整个流程处理;
3.1 搭建 seata-examples 项目
seata-examples 用来控制所有项目的依赖版本号,以及去除公共的依赖 seata。
3.1.1 使用 IDEA 创建 Maven 项目
选择 Maven 项目:
点击 Next,添加以下的信息:
Parent:选择 spring-cloud-alibaba-examples
Name:seata-examples
其他的信息保持默认即可。然后点击 Finish即可完成创建。
3.1.2 添加依赖
打开项目的 pom.xml 文件,添加以下的依赖。
<dependencies>
<!-- 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- web 项目的基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.1.3 完整的 pom.xml
<?xmlversion="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">
<parent>
<artifactId>spring-cloud-alibaba-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-examples</artifactId>
<dependencies>
<!-- 服务注册 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- web 项目的基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
最后,我们的项目的依赖关系如下:
可以看见,我们的 seata 版本为 1.0.0 。
3.2 搭建 account-service 项目
Account-service 项目将负责扣减用户账户余额
3.2.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:account-service
其他的值保持默认即可。
3.2.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.2.3 完整的 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">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>account-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.3 搭建 business-service 项目
在 business 将主要完成下单逻辑,包含库存的扣减,订单的创建。
3.3.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:business-service
其他的值保持默认即可。
3.3.2 添加依赖
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.2.3 完整的 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">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>account-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.4 搭建 order-service 项目
order-service 项目将负责保存用户订单。
3.4.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:order-service
其他的值保持默认即可。
3.4.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.4.3 完整的 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">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.5 搭建 storage-service 项目
storage-service 将负责扣除商品的库存。
3.5.1 使用 IDEA 创建一个 Maven 项目
选择 Maven 项目:
点击 Next 后,填写以下的信息:
Parent:seata-examples
Name:storage-service
其他的值保持默认即可。
3.5.2 添加依赖
我们需要使用 ORM 框架来完成对数据库的操作。在次我们需要使用 Mybatis-Plus 来操作数据库。
打开 pom.xml ,在里面添加如下内容:
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
为了以后我们打包发布我们的项目,在此我们添加 boot-maven 的打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3.5.3 完整的 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">
<parent>
<artifactId>seata-examples</artifactId>
<groupId>com.bjsxt</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>2.3</version>
</dependency>
<!--MySQL 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目的依赖关系如下:
3.6 完整的项目的案例为
四、代码的完善
4.1 数据库表导入
sql文件在gitee源代码里下载
案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples
在测试分布式事务之前,我们需要先设计数据库,以及准备测试数据。
新建数据库,命名为:seata
导入 Sql:
Sql 文件在今天的软件文件夹里面:
导入该 sql:
点击开始,进行导入。
成功后,发现成功的条数为:
表有如下:
account:用户的账号表
Order:订单表;
Stoage:商品的库存表;
undo_log:回滚事务表,SEATA AT 模式需要 UNDO_LOG 表。
4.2 模型对象和 Mapper 对象生成
使用 IDEA 连接数据库:
成功后,如图所示:
执行代码的生成:
提示:若大家没有安装 mybatis 的代码生成插件,可以在今天的软件文件夹里面下载安装。
- Account_tbl:
- Order_tbl:
- Storage_tbl:
代码生成完毕后:
4.3 storage-service 代码的完善
4.3.1 接口设计
在 storage-service 里面,主要完成对库存的扣减。
新建一个接口:
命名为:StorageService,代码如下:
代码如下:
public interface StorageService {
/**
* 扣减商品的库存
*@param commodityCode
* 商品的编码
*@param count
* 扣减商品的数量
*/
void deduct(String commodityCode, int count);
}
4.3.2 实现该接口
名称为:impl.StorageService,代码的实现如下:
@Service
public class StorageServiceImpl implements StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class);
@Autowired
private StorageTblMapper storageTblMapper;
@Override
public void deduct(String commodityCode, int count) {
logger.info("开始扣减库存,商品编码:{},数量:{}",commodityCode, count);
StorageTbl storageTbl = storageTblMapper.selectOne(new LambdaQueryWrapper<StorageTbl>().eq(StorageTbl::getCommodityCode, commodityCode));
int idleCount = storageTbl.getCount() - count;
if (idleCount < 0) {
throw new RuntimeException("库存不足");
}
storageTbl.setCount(idleCount);
storageTblMapper.updateById(storageTbl);
logger.info("库存扣减成功,商品编码:{},剩余数量:{}", commodityCode, idleCount);
}
}
4.3.3 使用 Restful 暴露此接口
添加一个 Controller
代码如下:
@RestController
public class StorageController {
private static Logger logger = LoggerFactory.getLogger(StorageController.class) ;
@Autowired
private StorageService storageService ;
/**
* 扣减商品的库存
* @param commodityCode 商品的编码
* @param count 商品的数量
* @return
*/
@GetMapping("/deduct/{commodityCode}/{count}")
public ResponseEntity<Void> deduct(@PathVariable("commodityCode") String commodityCode,@PathVariable("count") Integer count){
logger.info("Account Service ... xid: " + RootContext.getXID());
// 开始扣减库存
storageService.deduct(commodityCode , count);
return ResponseEntity.ok().build() ;
}
}
4.3.4 添加配置文件
在 resource 目录里面新建配置文件:
内容如下:
server:
port: 18084
spring:
application:
name: storage-service
cloud:
alibaba:
seata:
tx-service-group: storage-service
nacos:
discovery:
server-addr:localhost:8848
datasource:
name: storageDataSource
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle:2
initial-size: 2
seata:
service:
vgroup-mapping:
account-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
4.3.5 添加启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class StorageServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StorageServiceApplication.class ,args) ;
}
}
4.3.6 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.4 account-service 代码的完善
4.4.1 接口设计
在 account-service 里面,主要完成对用户余扣减。
新建一个接口:
命名为:AccountService,代码如下:
代码如下:
public interface AccountService {
/**
* 从用户的账号扣减金额
*@param userId
* 用户的 Id
*@param money
* 金额
*/
void debit(String userId, int money);
}
4.4.2 实现该接口
名称为:impl.StorageService,代码的实现如下:
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountTblMapper accountTblMapper;
private static Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
@Override
public void debit(String userId, intmoney) {
logger.info("准备扣减用户:{} 余额,扣减的数目为:{}", userId, money);
AccountTbl accountTbl = accountTblMapper.selectOne(new LambdaQueryWrapper<AccountTbl>().eq(AccountTbl::getUserId, userId));
int idleMoney = accountTbl.getMoney() - money;
if (idleMoney < 0) {
throw new RuntimeException("用户余额不足");
}
accountTbl.setMoney(idleMoney);
accountTblMapper.updateById(accountTbl);
logger.info("扣减用户{}金额成功,剩余金额为{}",userId, money);
}
}
4.4.3 使用 Restful 暴露此接口
添加一个 Controller
名称为:
代码如下:
@RestController
public class AccountController {
@Autowired
private AccountService accountService ;
private static Loggerlogger =
LoggerFactory.getLogger(AccountController.class) ;
@GetMapping("/debit/{userId}/{money}")
public ResponseEntity<Void> debit( @PathVariable("userId")String userId, @PathVariable("money") Integer money){
logger.info("Account Service ... xid: " + RootContext.getXID());
// 开始扣减余额
accountService.debit(userId , money);
return ResponseEntity.ok().build() ;
}
}
4.4.4 添加配置文件
在 resource 目录里面新建配置文件:
内容如下:
server:
port: 18085
spring:
application:
name: account-service
cloud:
alibaba:
seata:
tx-service-group: account-service
nacos:
discovery:
server-addr: localhost:8848
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username:root
password:123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active:20
min-idle: 2
initial-size: 2
seata:
service:
vgroup-mapping:
account-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations:classpath:/mapper/*.xml
4.4.5 添加启动类
命名为 AccountServiceApplication ,代码如下 :
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class AccoutServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AccoutServiceApplication.class ,args) ;
}
}
4.4.6 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.5 order-service 代码的完善
4.5.1 接口设计
在 order-service 里面,主要完成保存用户订单的操作。
新建一个接口:
命名为:OrderService,代码如下:
代码如下:
public interface OrderService {
/**
* 创建一个订单
*@param userId 用户 id
*@param commodityCode 商品的编号
*@param orderCount 商品的数量
*@return OrderTbl
*/
OrderTbl create(String userId, String commodityCode, int orderCount) ;
}
4.5.4 Ribbon 集成
创建一个配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
4.5.2 实现该接口
名称为:impl.OrderService,代码的实现如下:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderTblMapper orderTblMapper;
@Autowired
private AccountService accountService;
private static Loggerlogger =
LoggerFactory.getLogger(OrderServiceImpl.class);
@Override
public OrderTbl create(String userId, String commodityCode, int orderCount)
{
logger.info("准备为{}创建一个订单,商品编号为{},数量为{}", userId,commodityCode, orderCount);
// 1 计算总金额
int orderMoney = calculate(commodityCode, orderCount);
accountService.debit(userId, orderMoney);
OrderTbl order = new OrderTbl();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setCount(orderCount);
order.setMoney(orderMoney);
orderTblMapper.insert(order);
// INSERT INTO orders ...
return order;
}
private int calculate(String commodityCode, int orderCount) {
// 我们现在没有商品的表,在此我们把商品的价格定死
int prodPrice= 0 ;
if("HUAWEI_0001".equals(commodityCode)){ // 华为时 100
prodPrice =100;
}else if ("XIAOMI_002".equals(commodityCode)){ // 小米时 200
prodPrice =200 ;
}else {
prodPrice =1000 ; // 其他为 1000
}
return orderCount *prodPrice ;
}
}
4.5.3 远程调用 account-service 的实现
创建一个 AccountService 的类,该类里面主要完成对 accout-servic 的远程调用。
名称为:
/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
private static Logger logger = LoggerFactory.getLogger(AccountService.class);
/**
* 1 ribbon 的方式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 feign 的方式
*/
public void debit(String userId, intorderMoney) {
ResponseEntity<Void> entity = restTemplate.
getForEntity(
"http://accout-service/debit/{userId}/{orderMoney}",
Void.class,
userId,
orderMoney
);
if(entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
return ;
}
logger.info("扣减用户{}金额失败",userId);
throw newRuntimeException("扣减金额失败") ;
}
}
我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。
4.5.5 使用 Restful 暴露此接口
添加一个 Controller
命名为:
代码如下:
@RestController
public class OrderController {
private static Logger logger = LoggerFactory.getLogger(OrderController.class) ;
@Autowired
private OrderService orderService ;
/**
* 创建订单
* @param userId
* 用户 Id
* @param commodityCode
* 商品的编号
* @param orderCount
* 商品的数量
* @return
*/
@GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
public ResponseEntity<Void> create(
@PathVariable("userId") String userId,
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") int orderCount){
logger.info("Order Service ... xid: " + RootContext.getXID());
orderService.create(userId, commodityCode, orderCount) ;
return ResponseEntity.ok().build() ;
}
}
4.5.6 添加配置文件
在 resource 目录里面新建配置文件:
命名为:application.yml
内容如下:
server:
port: 18086
spring:
application:
name: order-service
cloud:
alibaba:
seata:
tx-service-group: order-service
nacos:
discovery:
server-addr:localhost:8848
datasource:
name: orderDataSource
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC
druid:
max-active: 20
min-idle: 2
initial-size: 2
seata:
service:
vgroup-mapping:
order-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
4.5.7 添加启动类
命名为:OrderServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.bjsxt.mapper")
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class ,args) ;
}
}
4.5.8 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.6 business-service 代码的完善
4.6.1 接口设计
在 business-service 里面,主要完成下单的逻辑,包含 2 个主要的步骤,就是对库存服务和订单服务的远程调用。
新建一个接口:
命名为:com.bjsxt.service.BusinessService
代码如下:
public interface BusinessService {
/**
* 采购 / 下单的过程
*@param userId
* 用户的 Id
*@param commodityCode
* 商品的编码
*@param orderCount
* 商品的数量
*/
void purchase(String userId, String commodityCode, int orderCount) ;
}
4.6.2 实现该接口
名称为:impl.BusinessServiceImpl,代码的实现如下:
@Service
public class BusinessServiceImpl implements BusinessService {
private static Logger logger = LoggerFactory.getLogger(BusinessServiceImpl.class) ;
@Autowired
private StorageService storageService;
@Autowired
private OrderService orderService;
@Override
public void purchase(String userId, String commodityCode, int orderCount) {
logger.info("准备下单,用户:{},商品:{},数量:{}",userId,commodityCode,orderCount);
storageService.deduct(commodityCode,orderCount);
orderService.create(userId, commodityCode, orderCount) ;
logger.info("下单完成");
}
}
4.6.3 远程调用 storage-service 的实现
创建一个 StorageService 的类,该类里面主要完成对 storage-servic 的远程调用。
名称为:
@Service
public class StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class) ;
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 采用 Feign 的形式
*/
public void deduct(String commodityCode, int orderCount) {
ResponseEntity<Void> entity = restTemplate.getForEntity(
"http://storage-service/debut/{commodityCode}/{orderCount}",
Void.class,
commodityCode,
orderCount
);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}",commodityCode,orderCount);
return;
}
throw new RuntimeException("扣减库存失败") ;
}
}
我们在此使用的时 Ribbon 做远程调用,下面的章节我们也会测试 Feign 。
4.6.4 远程调用 order-service 的实现
新建一个类:
代码如下:
@Service
public class OrderService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class);
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
/**
* 2 采用 Feign 的形式
*/
public void create(String userId, String commodityCode, int orderCount) {
ResponseEntity<Void> entity = restTemplate.getForEntity(
"http://order-service/create/{userId}/{commodityCode}/{orderCount}",
Void.class,
userId ,
commodityCode,
orderCount
);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,
commodityCode,orderCount);
return;
}
throw new RuntimeException("订单创建失败") ;
}
}
4.6.5 集成 Ribbon
添加一个 HttpUtilConfig 的配置类:
代码如下:
@Configuration
public class HttpUtilConfig {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate() ;
}
}
4.6.6 添加配置文件
在 resource 目录里面新建配置文件:
命名为:application.yml
内容如下:
server:
port: 18087
spring:
application:
name: business-service
cloud:
alibaba:
seata:
tx-service-group: business-service
nacos:
discovery:
server-addr:localhost:8848
seata:
service:
vgroup-mapping:
business-service: default
grouplist:
default: 127.0.0.1:8091
disable-global-transaction: false
enabled: true
4.6.7 添加启动类
命名为: BusinessServiceApplication
代码如下:
@SpringBootApplication
@EnableDiscoveryClient
public class BusinessServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BusinessServiceApplication.class ,args) ;
}
}
4.6.8 暴露下单接口
继续改造启动类:
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class BusinessServiceApplication {
@Autowired
private BusinessService businessService ;
public static void main(String[] args) {
SpringApplication.run(BusinessServiceApplication.class ,args) ;
}
/**
* 开始下单
* @param userId
* 用户的 Id
* @param commodityCode
* 商品的编号
* @param orderCount
* 商品的数量
* @return
*/
@GetMapping("/purchase/{userId}/{commodityCode}/{orderCount}")
public ResponseEntity<Void> purchase(
@PathVariable("userId") String userId,
@PathVariable("commodityCode")StringcommodityCode,
@PathVariable("orderCount")Integer orderCount){
businessService.purchase(userId,commodityCode,orderCount);
return ResponseEntity.ok().build() ;
}
}
4.6.9 启动项目测试
启动项目后,打印该日志,说明连接 seata-server 成功。
4.7 总体的调用流程如下
都启动完成后:
Nacos-Server:
4.8 正常下单测试
在浏览器里面访问:
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
代表 SXT_USER_1 购买 HUAWEI_0001 产品 1 件。
数据库里面:
- Accout_tbl 里面,SXT_USER_1 用户的金额减少100;
- Storage_tbl 里面,HUAWEI_0001 的库存减少了 1;
- Order_Tbl 里面,创建了一条订单记录;
说明,此时远程调用时正常的。
4.9 分布式事务的演示
我们演示如图的异常:
我们可以发现,远程调用共有 3 处。
4.9.1 在 accout-service 服务扣减余额触发异常
4.9.2 重启 accout-service
4.9.3 还原数据库里面的数据
Account_Tbl:
Storage_Tbl:
4.9.4 重新下单测试
http://localhost:18087/purchase/SXT_USER_1/HUAWEI_0001/1
数据库的数据:
Account_Tbl:
Storage_Tbl:
我们发现,分布式事务产生了,accout-service 内部的异常,导致 accout_tbl 表数据回滚了。
但是,在 storage_tbl :位于 stoage-service 的事务却没有回滚。
4.10 使用 Seata 解决分布式问题
4.10.1 改造 accout-service 里面的 AccountServiceImpl
当用户的 ID 为:SXT_USER_2 时,我们抛出异常,当为其他用户时,我们正常的下单。
4.10.2 改造 BusinessServiceImpl
添加一个注解,看他是否能解决分布式事务的问题
4.10.3 重启测试
重启 accout-service,business-service 测试
使用 SXT_USER_1 正常的下单测试:
Stoage_tbl:库存正常
Accout_Tbl:余额正常
使用 SXT_USER_2 下单测试:
发现发生异常后,
stoage_tbl 里面的没有发生改变,数据正常
Accout_tbl 里面的数据也没有发生改变,数据正常
分布式事务测试成功了
五、集成 Feign 测试 Seata
在上面的章节中,我们使用的时 Ribbon + RestTemplate 的形式做的远程调用。下面我们来演
示 Feign 的调用方式。
5.1 改造 business-service
5.1.1 添加依赖
修改 business-service 项目里面的 pom.xml 文件,在里面添加依赖。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
5.1.2 添加 OrderServiceFeign
里面的代码如下:
@ FeignClient ("order-service")
public interface OrderServiceFeign {
@GetMapping("/create/{userId}/{commodityCode}/{orderCount}")
ResponseEntity<Void> create(
@PathVariable("userId") String userId,
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") Integer orderCount);
}
5.1.3 添加 StorageServiceFeign
@FeignClient ("storage-service")
public interface StorageServiceFeign {
@GetMapping("/deduct/{commodityCode}/{orderCount}")
ResponseEntity<Void> deduct(
@PathVariable("commodityCode") String commodityCode,
@PathVariable("orderCount") Integer orderCount
) ;
}
5.1.5 改造 OrderService
@Service
public class OrderService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class);
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate ;
@Autowired
private OrderServiceFeign orderServiceFeign ;
/**
* 2 采用 Feign 的形式
*/
public void create(String userId, String commodityCode, int orderCount){
// Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity("http://order-service/create/{userId}/{commodityCode}/{orderCount}",
// Void.class,
// userId ,
// commodityCode,
// orderCount
// );
//Feign
ResponseEntity<Void> entity = orderServiceFeign.create(userId, commodityCode,orderCount);
if (entity.getStatusCode()== HttpStatus.OK){
logger.info("订单创建成功,用户为{} ,商品编号为{},本次扣减的数量为{}",userId ,
commodityCode,orderCount);
return;
}
throw new RuntimeException("订单创建失败") ;
}
}
5.1.7 改造 StorageService
代码如下:
@Service
public class StorageService {
private static Logger logger = LoggerFactory.getLogger(StorageService.class);
/**
* 1 采用 Ribbon 的形式
*/
@Autowired
private RestTemplate restTemplate;
@Autowired
private StorageServiceFeign storageServiceFeign;
/**
* 2 采用 Feign 的形式
*/
public void deduct(String commodityCode, int orderCount){
// Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity(
// "http://storage-service/deduct/{commodityCode}/{orderCount}",
// Void.class,
// commodityCode,
// orderCount
// );
//Feign
ResponseEntity<Void> entity = storageServiceFeign.deduct(commodityCode,orderCount);
if (entity.getStatusCode() == HttpStatus.OK) {
logger.info("扣减库存成功,商品编号为{},本次扣减的数量为{}",commodityCode,orderCount);
return;
}
throw new RuntimeException("扣减库存失败");
}
}
5.1.6 在启动类里面开启对 Feign 的支持
5.2 改造 order-service
5.2.1 添加依赖
在 dependencies 添加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
5.2.2 添加接口
里面的代码如下:
@FeignClient("account-service")
public interface AccountServiceFeign{
@GetMapping("/debit/{userId}/{orderMoney}")
ResponseEntity<Void> debit(
@PathVariable("userId") String userId,
@PathVariable("orderMoney") Integer orderMoney
) ;
}
5.2.3 修改 AccoutService
/**
* 实现对账号服务的远程调用
*/
@Service
public class AccountService {
private static Logger logger = LoggerFactory.getLogger(AccountService.class);
/**
* 1 ribbon 的方式
*/
@Autowired
private RestTemplate restTemplate ;
@Autowired
private AccountServiceFeign accountServiceFeign ;
/**
* 2 feign 的方式
*/
public void debit(String userId, intorderMoney) {
//Ribbon
// ResponseEntity<Void> entity = restTemplate.
// getForEntity(
// "http://accout-service/debit/{userId}/{orderMoney}",
// Void.class,
// userId,
// orderMoney
// );
ResponseEntity<Void> entity = accountServiceFeign.debit(userId, orderMoney);
if(entity.getStatusCode()== HttpStatus.OK){
logger.info("扣减用户{}金额成功,本次扣减的数目为{}",userId,orderMoney);
return ;
}
logger.info("扣减用户{}金额失败",userId);
throw newRuntimeException("扣减金额失败") ;
}
}
5.2.4 在启动类里面添加对 Feign 的支持
5.3 重启测试
重启 order-service ,business-service
还原数据库数据,开始测试。
正常下单测试:
使用 SXT_USER_2 下单:
出错了,但是数据库的各个表都正常。
Seata 测试成功了。
案例源码gitee地址:https://gitee.com/BanSheng/spring-cloud-alibaba-examples/tree/master/seata-examples