本文目录导读:

我为您整理一个完整的Spring Cloud Alibaba Seata分布式事务实战案例,涵盖环境搭建、代码实现和核心原理。
案例场景设计
业务场景:电商下单服务
- 订单服务:创建订单
- 库存服务:扣减库存
- 账户服务:扣减余额
三个服务需要通过分布式事务保证数据一致性。
环境准备
技术栈版本
Spring Boot: 2.3.12.RELEASE
Spring Cloud: Hoxton.SR12
Spring Cloud Alibaba: 2.2.5.RELEASE
Seata: 1.4.2
MySQL: 8.0
Nacos: 1.4.1
数据库初始化
-- 订单库
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
product_id BIGINT,
count INT,
money DECIMAL(10,2),
status INT DEFAULT 0
);
-- 库存库
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE t_storage (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT,
total INT,
used INT,
residue INT
);
-- 账户库
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
total DECIMAL(10,2),
used DECIMAL(10,2),
residue DECIMAL(10,2)
);
-- 每个业务库都需要创建undo_log表
CREATE TABLE undo_log (
id BIGINT NOT NULL AUTO_INCREMENT,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
);
核心代码实现
父POM配置
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
订单服务(Order Service)
pom.xml
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 8001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: root
password: root
logging:
level:
io.seata: debug
OrderController.java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/order/create")
public String createOrder(@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("count") Integer count) {
orderService.createOrder(userId, productId, count);
return "订单创建成功";
}
}
OrderService.java
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private StorageFeignClient storageFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Long userId, Long productId, Integer count) {
// 1. 创建订单
BigDecimal money = new BigDecimal("10").multiply(new BigDecimal(count));
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setMoney(money);
order.setStatus(0);
orderDao.createOrder(order);
// 2. 扣减库存(远程调用)
storageFeignClient.decreaseStock(productId, count);
// 3. 扣减账户余额(远程调用)
accountFeignClient.decreaseBalance(userId, money);
// 4. 模拟异常测试回滚
// 取消下面注释测试分布式事务回滚
// int i = 1 / 0;
// 5. 修改订单状态为已完成
orderDao.updateOrderStatus(order.getId(), 1);
}
}
StorageFeignClient.java
@FeignClient(name = "seata-storage-service", path = "/storage")
public interface StorageFeignClient {
@GetMapping("/decrease")
String decreaseStock(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
AccountFeignClient.java
@FeignClient(name = "seata-account-service", path = "/account")
public interface AccountFeignClient {
@GetMapping("/decrease")
String decreaseBalance(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
库存服务(Storage Service)
StorageService.java
@Service
public class StorageService {
@Autowired
private StorageDao storageDao;
public void decreaseStock(Long productId, Integer count) {
// 检查库存并扣减
Storage storage = storageDao.findByProductId(productId);
if (storage == null) {
throw new RuntimeException("商品不存在");
}
if (storage.getResidue() < count) {
throw new RuntimeException("库存不足");
}
storageDao.decreaseStock(productId, count);
}
}
StorageController.java
@RestController
@RequestMapping("/storage")
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/decrease")
public String decreaseStock(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count) {
storageService.decreaseStock(productId, count);
return "扣减库存成功";
}
}
账户服务(Account Service)
AccountService.java
@Service
public class AccountService {
@Autowired
private AccountDao accountDao;
public void decreaseBalance(Long userId, BigDecimal money) {
// 检查余额并扣减
Account account = accountDao.findByUserId(userId);
if (account == null) {
throw new RuntimeException("用户不存在");
}
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
accountDao.decreaseBalance(userId, money);
}
}
AccountController.java
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/decrease")
public String decreaseBalance(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
accountService.decreaseBalance(userId, money);
return "扣减余额成功";
}
}
Seata Server配置
registry.conf
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "localhost:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
file.conf
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata_server?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC"
user = "root"
password = "root"
minConn = 5
maxConn = 20
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
事务配置
每个服务添加配置类
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
}
Seata配置(每个服务的bootstrap.yml)
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_test_tx_group
config:
type: nacos
nacos:
serverAddr: localhost:8848
group: SEATA_GROUP
registry:
type: nacos
nacos:
serverAddr: localhost:8848
group: SEATA_GROUP
启动与测试
启动顺序
启动Nacos
2. 启动Seata Server(执行seata-server.sh)
3. 启动数据库服务
4. 启动三个业务服务
5. 初始化测试数据
测试数据初始化
-- 库存表 INSERT INTO t_storage VALUES (1, 1, 100, 0, 100); -- 账户表 INSERT INTO t_account VALUES (1, 1, 1000.00, 0, 1000.00);
测试接口
# 正常下单 GET http://localhost:8001/order/create?userId=1&productId=1&count=2 # 异常场景测试(在OrderService中取消注释异常代码) # 观察库存和余额是否回滚
核心原理说明
AT模式工作原理
一阶段:
- 业务SQL执行前获取全局锁
- 执行业务SQL
- 生成undo_log快照
2. 二阶段提交:
- 删除undo_log
- 释放全局锁
3. 二阶段回滚:
- 根据undo_log回滚数据
- 释放全局锁
事务传播机制
@GlobalTransactional开启全局事务- 所有参与方通过同一XID关联
- Seata通过拦截器自动处理分支事务
常见问题及解决方案
数据源代理问题
// 必须使用DataSourceProxy包装
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
事务超时设置
seata:
service:
vgroup-mapping:
my_test_tx_group: default
enable-degrade: false
disable-global-transaction: false
client:
tm:
commit-retry-count: 5
rollback-retry-count: 5
性能优化建议
seata:
client:
rm:
report-success-enable: false
table-meta-check-enable: true
report-retry-count: 5
async-commit-buffer-limit: 10000
这个案例完整展示了如何使用Seata实现分布式事务,需要注意的是,在实际生产环境中,还需要考虑性能优化、高可用部署、监控告警等方面的问题。