前言
给大家分享一下阿里的分布式事务框架Seata的完整搭建教程,我感觉这篇教程已经算是很详细了,基本上每个必须的依赖和配置项都写的明明白白,为了让大家能更简单的先上手运行,注册中心和微服务框架均使用阿里系即Nacos和Dubbo,相关框架服务均使用最新稳定版进行搭建。好了,废话不多说了,开始实操。
正文
环境准备
一、Nacos注册中心的搭建,官方手册地址:点击跳转
- 首先根据文档提示,下载nacos服务包【官方GitHub下载地址】
2.下载完以后,解压压缩包,进入bin目录,执行启动脚本
Linux/Unix/Mac
启动命令(standalone代表着单机模式运行,非集群模式):
sh startup.sh -m standalone
如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:
bash startup.sh -m standalone
Windows
启动命令(standalone代表着单机模式运行,非集群模式):
startup.cmd -m standalone
启动完毕以后在浏览器中访问nacos默认后台管理地址:http://127.0.0.1:8848/nacos,默认登录账号和密码均为nacos,成功登录后表示nacos服务的搭建已完成,如图
二、Seata服务部署,官方手册地址:点击跳转
1.首先根据文档提示下载Seata服务包【官方Github下载地址】
2. 解压以后修改\conf\registry.conf配置文件,注册中心修改为nacos注册中心
配置信息使用默认的file,然后修改\conf\file.conf,将事务存储信息选用redis
3. 进去bin目录执行启动脚本
./seata-server.bat
启动成功后可在nacos的服务列表中看到seata的服务已经注册进去注册中心了
到此,环境准备工作就完毕了,接下来开始进行项目的搭建
项目构建
一、初始化SpringBoot项目【2.4.2】
使用idea直接new一个SpringBoot项目,只需要引入“Spring Web”即可,项目名称为microservice-user
二、集成nacos注册中心,【2.0.3】官方集成文档:点击跳转
- 增加nacos相关依赖:
pom文件中增加nacos依赖,因为alibaba的cloud集成依赖中nacos-client版本比较低,所以单独依赖新版nacos-client,
<!-- Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.1</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.0.3</version>
</dependency>
PS:这里需要特别注意:nacos存在对应的版本关系,所以这里项目的SpringBoot版本必须调整为2.4.2,否则可能会导致项目无法正常启动
- 增加nacos配置(使用yml):
项目创建默认的application配置文件为properties配置文件,将其删除掉,新建 application.yml,此时的配置如下
spring:
application:
name: microservice-user
cloud:
nacos:
discovery:
server-addr: localhost:8848
# namespace: public # 默认的即为public空间,为了简便,所有服务的创建配置都使用的默认项
username: nacos
password: nacos
server:
port: 8000
- 在启动类上增加@EnableDiscoveryClient注解,启用客户端发现。
- 然后启动项目,启动成功后会在nacos的服务列表中看到该微服务,服务名就是application.yml中配置的spring.application.name,如图
三、集成dubbo,【3.0.3】官方文档地址:点击跳转
- 在pom文件中引入dubbo相关依赖
<properties>
<java.version>1.8</java.version>
<dubbo.version>3.0.3</dubbo.version>
</properties>
<!-- Dubbo配置 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-nacos</artifactId>
<version>${dubbo.version}</version>
</dependency>
- application.yml配置文件中增加dubbo相关配置,nacos的配置项直接使用第二步中的配置参数
dubbo:
application:
name: ${spring.application.name}
scan:
base-packages: com.cn.lucky.morning.user.service.api
registry:
address: nacos://${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
- 在com.cn.lucky.morning.user.api包下新增接口类,
/**
* @author lucky_morning
*/
public interface AccountService {
/**
* 从用户账户中借出
*
* @param userId 用户ID
* @param money 金额
*/
void debit(String userId, int money);
}
在com.cn.lucky.morning.user.api包下新增接口实现类,实现上一步的接口类并在类上使用@DubboService将类标注为服务提供方
@DubboService
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private ITblAccountService tblAccountService;
@Override
public void debit(String userId, int money) {
TblAccount account = tblAccountService.getById(userId);
if (account == null) {
throw new RuntimeException("用户不存在");
}
account.setMoney(account.getMoney() - money);
if (account.getMoney() < 0) {
throw new RuntimeException("用户余额不足");
}
tblAccountService.updateById(account);
}
}
- 启动微服务,在nacos的服务列表中能看到该提供方类的注册信息
- 如何使用dubbo提供的服务提供方,新建一个microservice-order项目,按上述步骤集成nacos和dubbo,然后再引入microservice-user,使用@DubboReference注解注入提供方类,如图:
(图中代码为在创建订单的服务提供方方法中调用用户微服务对用户的账户金额进行减少操作)
- 在microservice-order微服务中增加一个controller来测试调用
/**
* @author lucky_morning
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
*/
@GetMapping("/create")
public String create(String userId, String commodityCode, int orderCount) {
try {
return "订单创建结果:" + orderService.create(userId, commodityCode, orderCount);
}catch (Exception e){
return "订单创建失败:" + e.getMessage();
}
}
}
访问地址:http://localhost:8001/order/create?userId=1&commodityCode=phone&orderCount=1
因为account数据表现在都是空表,并未插入数据,所以微服务调用用户减少金额时会抛出用户不存在的异常,证明已经正常在order微服务中调用了user微服务提供的方法
四、集成seata,【1.4.2】官方文档地址:点击跳转
- pom文件中引入相关依赖
<!-- Seata 配置 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
- application.yml配置文件中新增seata相关的配置,配置中心和注册中心均使用nacos
seata:
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
data-id: seataServer.properties
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
- 在nacos的配置中心新增seataServer.properties配置文件,在其中增加相关微服务的默认项
- seata的 AT 模式(默认模式)需要 UNDO_LOG 表
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
- 启动项目,正常启动控制台无错误信息即表示配置正常
- 在需要分布式事务的方法上增加@GlobalTransactional(rollbackFor = Exception.class)注解即可
- 按照相同方式配置另外两个微服务
五、示例业务微服务说明
microservice-user
- 数据表:
DROP TABLE IF EXISTS `tbl_account`;
CREATE TABLE `tbl_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 服务提供方接口:
package com.cn.lucky.morning.user.api;
/**
* @author lucky_morning
*/
public interface AccountService {
/**
* 从用户账户中借出
*
* @param userId 用户ID
* @param money 金额
*/
void debit(String userId, int money);
}
- 服务提供方接口实现:
package com.cn.lucky.morning.user.service.api;
import com.cn.lucky.morning.user.api.AccountService;
import com.cn.lucky.morning.user.entity.TblAccount;
import com.cn.lucky.morning.user.service.ITblAccountService;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@DubboService
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private ITblAccountService tblAccountService;
@Override
public void debit(String userId, int money) {
TblAccount account = tblAccountService.getById(userId);
if (account == null) {
throw new RuntimeException("用户不存在");
}
account.setMoney(account.getMoney() - money);
if (account.getMoney() < 0) {
throw new RuntimeException("用户余额不足");
}
tblAccountService.updateById(account);
}
}
microservice-order
- 数据表
DROP TABLE IF EXISTS `tbl_order`;
CREATE TABLE `tbl_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
`money` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 服务提供方接口:
package com.cn.lucky.morning.order.api;
/**
* @author lucky_morning
*/
public interface OrderService {
/**
* 创建订单
* @return
*/
boolean create(String userId, String commodityCode, int orderCount);
}
- 服务提供方实现类:
package com.cn.lucky.morning.order.service.api;
import com.cn.lucky.morning.order.api.OrderService;
import com.cn.lucky.morning.order.entity.TblOrder;
import com.cn.lucky.morning.order.service.ITblOrderService;
import com.cn.lucky.morning.user.api.AccountService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author lucky_morning
*/
@Service
@DubboService
public class OrderServiceImpl implements OrderService {
@Autowired
private ITblOrderService tblOrderService;
@DubboReference
private AccountService accountService;
@Override
public boolean create(String userId, String commodityCode, int orderCount) {
int orderMoney = calculate(commodityCode, orderCount);
accountService.debit(userId, orderMoney);
TblOrder order = new TblOrder();
order.setId(Integer.valueOf(userId));
order.setCommodityCode(commodityCode);
order.setCount(orderCount);
order.setMoney(orderMoney);
return tblOrderService.save(order);
}
/**
* 计算价格
*
* @param commodityCode 商品Code
* @param orderCount 商品数量
* @return 购买价格
*/
private int calculate(String commodityCode, int orderCount) {
return 100 * orderCount;
}
}
microservice-storage
- 数据表:
DROP TABLE IF EXISTS `tbl_storage`;
CREATE TABLE `tbl_storage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL,
`count` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY (`commodity_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 库存操作接口实现类:
package com.cn.lucky.morning.storage.service.impl;
import com.cn.lucky.morning.storage.entity.TblStorage;
import com.cn.lucky.morning.storage.mapper.TblStorageMapper;
import com.cn.lucky.morning.storage.service.ITblStorageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author lucky_morning
* @since 2021-10-20
*/
@Service
public class TblStorageServiceImpl extends ServiceImpl<TblStorageMapper, TblStorage> implements ITblStorageService {
@Override
public void deduct(String commodityCode, int count) {
TblStorage storage = this.lambdaQuery().eq(TblStorage::getCommodityCode, commodityCode).last("limit 1").one();
if (storage == null) {
throw new RuntimeException("商品不存在");
}
if (storage.getCount() < count) {
throw new RuntimeException("商品库存不足");
}
storage.setCount(storage.getCount() - count);
this.updateById(storage);
}
}
- 采购操作实现类:
package com.cn.lucky.morning.storage.service.impl;
import com.cn.lucky.morning.order.api.OrderService;
import com.cn.lucky.morning.storage.service.BusinessService;
import com.cn.lucky.morning.storage.service.ITblStorageService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author lucky_morning
*/
@Service
public class BusinessServiceImpl implements BusinessService {
@Autowired
private ITblStorageService storageService;
@DubboReference
private OrderService orderService;
@Override
public void purchase(String userId, String commodityCode, int orderCount) {
// 减库存
storageService.deduct(commodityCode,orderCount);
// 新增订单
orderService.create(userId, commodityCode, orderCount);
}
}
package com.cn.lucky.morning.storage.controller;
import com.cn.lucky.morning.storage.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author lucky_morning
* @since 2021-10-20
*/
@RestController
@RequestMapping("/business")
public class BusinessController {
@Autowired
private BusinessService businessService;
/**
* 采购
*
* @param userId 采购用户
* @param commodityCode 商品Code
* @param orderCount 商品数量
* @return 采购结果
*/
@GetMapping("/purchase")
public String purchase(String userId, String commodityCode, int orderCount) {
try {
businessService.purchase(userId, commodityCode, orderCount);
return "操作成功";
} catch (Exception e) {
return "操作失败:" + e.getMessage();
}
}
}
架构图(使用seata官方手册示例业务项目)
六、实操各种情况
前置:各数据表之间的默认数据
1.不加分布式事务注解,在金额,库存都充足的情况下:
调用成功,那么三个数据表的变化分别为,tbl_account表中,用户ID为1的账号金额减少100;tbl_order表中新增一条订单记录,tbl_storage表中,商品Code为phone的库存减少1,在这种理论正常的情况下,各个微服务之间调用都不出错,你好我好大家都好,但现实往往都不是这么美好的,会出现各种未知的情况导致中途失败,比如接下来几个例子。
2.不加分布式事务注解,在金额不足,库存充足的情况下:
继续使用上面的数据表数据,我们将请求的商品数量参数修改为3,那么此时库存有4是能正常取货,但是用户金额是200,金额不够
接口返回了用户余额不足,那我们再来看一下数据表的变化呢
因为用户减少金额是在库存减少之后,所以异常抛出之后商品库存依然减少了
3. 增加分布式事务注解,在金额不足,库存充足的情况下:
首先我们将数据库里面的数据还原到上一步,即把库存的数据修改回4,在采购方法上增加分布式事务注解后重启storage微服务
重启成功后再次调用接口
此时我们再回到数据库查看数据,会发现数据表中数据都没有发生变化,并且我们在storage微服务的控制台上还能看到seata打印的分布式事务相关的信息
此时,分布式事务的集成就算完成了,可以看出当一个方法中调用了多个微服务的提供方方式对不同的数据表进行操作的时候,在中途某一步出错了,如果没有使用分布式事务,那么出错之前对数据库的所有操作都不会被还原,就会导致数据不一致的情况出现,而使用了分布式事务之后,多个微服务的调用也像以前的数据库事务一样简单,要么都成功,要么都失败还原
PS
这就是最简单的项目集成示例,示例项目相关代码我已经放在GitHub上了,访问地址:https://github.com/luckymorning/SpringBootNacosSeataDubboDemoProject
如有不对之处,欢迎大家指正交流