Spring Cloud 微服务系列文章,点击上方合集↑
1. 简介
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
事务是保障一系列操作要么都成功,要么都失败。就比如转账:A转账100元给B,先从A账户扣除100元,然后从B账户增加100元,假如从A账户里面已经成功扣除了100元,但是增加B账户的钱的过程中发生了异常,导致没有增加成功。这里就需要恢复A账户里面的钱(回滚)。整个转账过程就必须是事务操作。
官网地址:https://seata.io/zh-cn/
2. 下载运行
可以直接下载二进制包或通过源码编译打包。
2.1 直接下载(推荐)
从官网 https://github.com/seata/seata/releases下载服务器软件包,将其解压缩。
官网下载很慢,网盘下载(推荐):「seata-server-1.6.1.zip」来自UC网盘分享
https://drive.uc.cn/s/2cfffd43e8fc4
2.2 编译安装
# 下载源码
git clone https://gitee.com/seata-io/seata.git
cd seata
# 切换分支
git checkout v1.6.1
# 编译打包
mvn -Prelease-seata -Dmaven.test.skip=true clean install -U
位置:distribution/target/seata-server-1.6.1
2.3 运行
# windows
seata-server.bat -p 8091 -h 127.0.0.1 -m file
# mac/linux
sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
-p 8091
指定端口-h 127.0.0.1
指定地址
3. SpringCloud 集成 Seata
3.1 业务说明
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 商品服务:扣除商品数量
- 订单服务:创建订单
- 账户服务:扣除账户余额
3.2 架构图
业务调用订单服务(创建订单)和商品服务(减少库存),订单服务再调用账户服务(减少余额)。
这个过程必须是事务性的,要么都成功,要么都失败。
3.3 创建 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;
undo_log表中记录相关的操作日志,主要包含被修改数据的原始值和相应的逆向操作。
3.4 创建业务表
account 账户表
- id
- username 用户名
- money 余额
product 商品表
- id
- product_name 商品名称
- product_number 商品数量
product_order 订单表
- id
- user_id 用户id
- product_id 商品id
- purchase_number 购买数量
- purchase_money 购买金额
sql脚本如下:
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(50) DEFAULT NULL COMMENT '用户名',
`money` int(11) DEFAULT 0 COMMENT '余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`product_name` varchar(50) DEFAULT NULL COMMENT '商品名称',
`product_number` int(11) DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
DROP TABLE IF EXISTS `product_order`;
CREATE TABLE `product_order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`purchase_number` int(11) DEFAULT 0 COMMENT '购买数量',
`purchase_money` int(11) DEFAULT 0 COMMENT '购买金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account` VALUES (1, '老王', 10000);
INSERT INTO `product` VALUES (1, '贵州茅台', 10);
- 账户表插入一条用户信息
- 商品表插入一条商品信息
实际情况下账户表、商品表、订单表可能在不同的数据库中,这里就模拟放在一个库里。
3.5 pom.xml
添加如下依赖包:
spring-boot-starter-web
spring boot包依赖spring-cloud-starter-alibaba-nacos-discovery
nacos服务注册与发现包依赖spring-cloud-starter-openfeign
openfeign服务调用依赖spring-cloud-loadbalancer
负载均衡包依赖spring-cloud-starter-alibaba-seata
seata分布式事务包依赖mysql-connector-java
mysql包依赖spring-boot-starter-data-jpa
jpa包依赖lombok
lombok包依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
3.6 application.properties
- nacos服务注册与发现地址配置
- 数据库连接信息配置
- seata地址、服务组等配置
# nacos
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
spring.cloud.nacos.discovery.server-addr=http://localhost:8848
# 数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/seata_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# seata 相关
seata.config.type=file
seata.service.grouplist.default=127.0.0.1:8091
seata.tx-service-group=test_group
seata.service.vgroup-mapping.test_group=default
启动类上要@EnableFeignClients()
注解开启服务调用。
3.7 seata-product 服务
创建seata-product
商品服务模块,提供decreaseNumber
扣除库存数量方法。
@RestController
@RequestMapping("product")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping("decreaseNumber")
public void decreaseNumber(@RequestParam Long id,
@RequestParam int number) {
productService.decreaseNumber(id, number);
}
}
public interface ProductService {
/**
* 减少库存数量
*/
void decreaseNumber(Long id, int number);
}
@Service
public class ProductServiceImpl implements ProductService {
@Resource
private ProductRepository productRepository;
@Override
public void decreaseNumber(Long id, int number) {
Product product = productRepository.getById(id);
product.setProductNumber(product.getProductNumber() - number);
productRepository.save(product);
}
}
3.8 seata-account 服务
创建seata-account
账户服务模块,提供decreaseMoney
扣除账户余额方法。
@RestController
@RequestMapping("account")
public class AccountController {
@Resource
private AccountService accountService;
@GetMapping("decreaseMoney")
public void decreaseMoney(@RequestParam Long userId,
@RequestParam int money) {
accountService.decreaseMoney(userId, money);
}
}
public interface AccountService {
/**
* 减少用户余额
*/
void decreaseMoney(Long userId, int money);
}
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountRepository accountRepository;
@Override
public void decreaseMoney(Long userId, int money) {
Account account = accountRepository.getById(userId);
account.setMoney(account.getMoney() - money);
accountRepository.save(account);
}
}
3.9 seata-order 服务
创建seata-order
订单服务模块,提供createOrder
创建订单方法,它远程调用了账户服务decreaseMoney
扣除余额方法。
@RestController
@RequestMapping("order")
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("createOrder")
public void createOrder(@RequestParam Long userId,
@RequestParam Long productId,
@RequestParam int number,
@RequestParam int money) {
orderService.createOrder(userId, productId, number, money);
}
}
public interface OrderService {
/**
* 创建订单
*/
void createOrder(Long userId, Long productId, int number, int money);
}
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private AccountService accountService;
@Override
public void createOrder(Long userId, Long productId, int number, int money) {
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setPurchaseNumber(number);
order.setPurchaseMoney(money);
// 创建订单
orderRepository.save(order);
// 调用账户服务 扣除账户余额
accountService.decreaseMoney(userId, money);
}
}
@FeignClient(name = "seata-account")
public interface AccountService {
@GetMapping("/account/decreaseMoney")
void decreaseMoney(@RequestParam Long userId,
@RequestParam int money);
}
3.10 seata-business
创建seata-business
业务服务模块,提供purchase
购买商品方法,它远程调用了订单服务createOrder
创建订单方法和商品服务decreaseNumber
扣除库存数量方法。
在purchase()
方法加上@GlobalTransactional
注解开启事务,当有异常发生时会进行回滚,这里通过int i = 1 / 0 ;
模拟异常。
@RestController
@RequestMapping("business")
public class BusinessController {
@Resource
private BusinessService businessService;
@GetMapping("purchase")
public String purchase() {
businessService.purchase();
return "操作成功";
}
}
public interface BusinessService {
void purchase();
}
@Service
public class BusinessServiceImpl implements BusinessService {
@Resource
private ProductService productService;
@Resource
private OrderService orderService;
@GlobalTransactional
@Override
public void purchase() {
// 调用订单服务创建订单
orderService.createOrder(1L, 1L, 1, 1499);
// 模拟异常
int i = 1 / 0 ;
// 调用商品服务扣除库存
productService.decreaseNumber(1L, 1);
}
}
// 远程Order服务接口
@FeignClient(name = "seata-order")
public interface OrderService {
@GetMapping("/order/createOrder")
void createOrder(@RequestParam Long userId,
@RequestParam Long productId,
@RequestParam int number,
@RequestParam int money);
}
// 远程product服务接口
@FeignClient(name = "seata-product")
public interface ProductService {
@GetMapping("/product/decreaseNumber")
void decreaseNumber(@RequestParam Long id,
@RequestParam int number);
}
3.11 测试
访问接口地址:http://localhost:8304/business/purchase
当BusinessServiceImpl#purchase
方法不加@GlobalTransactional
注解:订单表已经创建新的订单并且账户余额已扣除,但是商品库存数量没有减少。
当BusinessServiceImpl#purchase
方法加上@GlobalTransactional
注解:订单表和账号表会回滚,也就是订单记录和账户余额都没有发生变化。
这里只在
seata-business
服务模拟发生异常,实际上不管在那个服务上发生异常(订单服务、商品服务、账户服务),数据都会回滚到之前的状态。
Seata将被修改数据的原始值和相应的逆向操作记录在undo_log
表中,如果发生异常通过undo_log
表中的内容进行回滚,我们可以通过调试模式打个断点,然后去查看数据库的undo_log
表就可以查看到相关数据。
- 在事务方法执行过程中会新增undo_log表数据记录,并在事务方法执行结束后清除记录,所以只能通过断点去查看。
4. 结语
本文通过用户购买商品的案例来使用Seata分布式事务:创建订单、扣除余额、减少库存,可以看出通过Seata使用分布式事务非常的简单方便,只需要一个@GlobalTransactional
注解。
Spring Cloud 微服务系列 完整的代码在仓库的sourcecode/spring-cloud-demo
目录下。
gitee(推荐):https://gitee.com/cunzaizhe/xiaohuge-blog
github:https://github.com/tigerleeli/xiaohuge-blog
关注微信公众号:“小虎哥的技术博客”,让我们一起成为更优秀的程序员❤️!