Seata AT模式详细实例:电商下单场景
1. Seata AT模式核心原理
1.1 核心概念
- AT模式:Automatic Transaction(自动事务),非侵入式的分布式事务解决方案
- TC(Transaction Coordinator):事务协调器,管理全局事务状态
- TM(Transaction Manager):事务管理器,发起和结束全局事务
- RM(Resource Manager):资源管理器,管理分支事务,与TC交互
- 全局事务ID(XID):唯一标识一个全局事务
- Undo Log:回滚日志,用于事务回滚
1.2 工作流程
- 全局事务开启:TM向TC请求开启全局事务,获取XID
- 分支事务注册:RM向TC注册分支事务,关联XID
- SQL执行:RM执行SQL,生成Undo Log并写入数据库
- 分支事务提交:RM向TC报告分支事务状态
- 全局事务提交/回滚:TC根据分支事务状态,决定全局提交或回滚
- 分支事务提交/回滚:TC通知RM执行分支事务的提交或回滚
2. 项目结构设计
2.1 业务场景
电商下单流程:
- 创建订单(订单服务)
- 扣减库存(库存服务)
- 扣减账户余额(账户服务)
2.2 项目结构
seata-at-demo/ # 父项目
├── order-service/ # 订单服务(8081)
├── inventory-service/ # 库存服务(8082)
└── account-service/ # 账户服务(8083)
3. 环境准备
3.1 组件版本
| 组件 | 版本 |
|---|---|
| Spring Boot | 2.7.18 |
| Spring Cloud | 2021.0.8 |
| Seata | 1.7.1 |
| MySQL | 8.0+ |
| Nacos | 2.2.3 |
3.2 数据库初始化
3.2.1 创建Seata所需数据库和表
sql
-- 创建seata数据库
CREATE DATABASE seata;
USE seata;
-- 创建undo_log表(Seata AT模式必须)
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
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,
UNIQUE KEY ux_undo_log (xid, branch_id)
);
3.2.2 业务数据库初始化
order-service数据库:
sql
CREATE DATABASE order_db;
USE order_db;
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
count INT NOT NULL,
money DECIMAL(10,2) NOT NULL,
status INT DEFAULT 0 COMMENT '0:创建中,1:已创建,2:已取消'
);
CREATE TABLE undo_log (
-- 同上,每个业务库都需要undo_log表
);
inventory-service数据库:
sql
CREATE DATABASE inventory_db;
USE inventory_db;
CREATE TABLE inventory (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_id BIGINT NOT NULL,
count INT NOT NULL DEFAULT 0
);
INSERT INTO inventory (product_id, count) VALUES (1, 100);
CREATE TABLE undo_log (
-- 同上
);
account-service数据库:
sql
CREATE DATABASE account_db;
USE account_db;
CREATE TABLE account (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
money DECIMAL(10,2) NOT NULL DEFAULT 0.00
);
INSERT INTO account (user_id, money) VALUES (1, 1000.00);
CREATE TABLE undo_log (
-- 同上
);
4. 代码实现
4.1 父项目POM.xml
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<properties>
<spring-cloud.version>2021.0.8</spring-cloud.version>
<seata.version>1.7.1</seata.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
4.2 订单服务实现
4.2.1 POM.xml
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
4.2.2 application.yml
yaml
server:
port: 8081
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
jpa:
hibernate:
ddl-auto: update
show-sql: true
seata:
enabled: true
tx-service-group: my_test_tx_group
registry:
type: nacos
nacos:
application: seata-server
server-addr: localhost:8848
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
data-source-proxy-mode: AT
4.2.3 订单实体
java
@Entity
@Table(name = "orders")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status = 0;
}
4.2.4 Feign客户端
java
@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {
@PostMapping("/inventory/deduct")
String deduct(@RequestParam Long productId, @RequestParam Integer count);
}
@FeignClient(name = "account-service")
public interface AccountFeignClient {
@PostMapping("/account/deduct")
String deduct(@RequestParam Long userId, @RequestParam BigDecimal money);
}
4.2.5 订单服务实现(核心)
java
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private InventoryFeignClient inventoryFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
/**
* 创建订单,Seata AT模式分布式事务
* @GlobalTransactional:标记为全局事务
*/
@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public String createOrder(Order order) {
System.out.println("开始创建订单...");
// 1. 创建订单
orderRepository.save(order);
System.out.println("订单创建成功:" + order.getId());
// 2. 扣减库存
String inventoryResult = inventoryFeignClient.deduct(order.getProductId(), order.getCount());
System.out.println("库存扣减结果:" + inventoryResult);
// 3. 扣减账户余额
BigDecimal totalMoney = order.getMoney().multiply(new BigDecimal(order.getCount()));
String accountResult = accountFeignClient.deduct(order.getUserId(), totalMoney);
System.out.println("账户扣减结果:" + accountResult);
// 模拟异常,测试回滚
// if (true) throw new RuntimeException("模拟异常,测试回滚");
System.out.println("订单创建完成!");
return "订单创建成功:" + order.getId();
}
}
4.2.6 订单控制器
java
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(@RequestBody Order order) {
return orderService.createOrder(order);
}
}
4.3 库存服务实现
4.3.1 application.yml
yaml
server:
port: 8082
spring:
application:
name: inventory-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/inventory_db?useSSL=false&serverTimezone=UTC
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848
jpa:
hibernate:
ddl-auto: update
show-sql: true
seata:
enabled: true
tx-service-group: my_test_tx_group
# 其他配置同order-service
4.3.2 库存服务实现
java
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
/**
* 扣减库存,分支事务
*/
@Transactional(rollbackFor = Exception.class)
public String deduct(Long productId, Integer count) {
System.out.println("开始扣减库存...");
// 查询库存
Inventory inventory = inventoryRepository.findByProductId(productId);
if (inventory == null) {
throw new RuntimeException("库存不存在");
}
// 检查库存是否充足
if (inventory.getCount() < count) {
throw new RuntimeException("库存不足");
}
// 扣减库存
inventory.setCount(inventory.getCount() - count);
inventoryRepository.save(inventory);
System.out.println("库存扣减成功:" + productId);
return "库存扣减成功";
}
}
4.4 账户服务实现
4.4.1 application.yml
yaml
server:
port: 8083
spring:
application:
name: account-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/account_db?useSSL=false&serverTimezone=UTC
username: root
password: root
cloud:
nacos:
discovery:
server-addr: localhost:8848
jpa:
hibernate:
ddl-auto: update
show-sql: true
seata:
enabled: true
tx-service-group: my_test_tx_group
# 其他配置同order-service
4.4.2 账户服务实现
java
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
/**
* 扣减账户余额,分支事务
*/
@Transactional(rollbackFor = Exception.class)
public String deduct(Long userId, BigDecimal money) {
System.out.println("开始扣减账户余额...");
// 查询账户
Account account = accountRepository.findByUserId(userId);
if (account == null) {
throw new RuntimeException("账户不存在");
}
// 检查余额是否充足
if (account.getMoney().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
// 扣减余额
account.setMoney(account.getMoney().subtract(money));
accountRepository.save(account);
System.out.println("账户余额扣减成功:" + userId);
return "账户余额扣减成功";
}
}
5. Seata Server部署
5.1 下载Seata Server
从Seata官网下载Seata Server 1.7.1:https://github.com/seata/seata/releases
5.2 配置Seata Server
5.2.1 registry.conf
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"
dataId = "seataServer.properties"
}
}
5.2.2 seataServer.properties
在Nacos中创建配置:
-
Data ID: seataServer.properties
-
Group: SEATA_GROUP
-
配置内容:
propertiesstore.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://localhost:3306/seata?useSSL=false&serverTimezone=UTC store.db.user=root store.db.password=root store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.lockTable=lock_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000
5.3 启动Seata Server
bash
# 解压后进入bin目录
./seata-server.sh -h 127.0.0.1 -p 8091 -m db
6. 启动Nacos
bash
# 解压后进入bin目录
./startup.sh -m standalone
7. 测试Seata AT模式
7.1 启动服务
- 启动Nacos
- 启动Seata Server
- 启动order-service
- 启动inventory-service
- 启动account-service
7.2 测试正常流程
7.2.1 发送请求
bash
curl -X POST -H "Content-Type: application/json" -d '{"userId":1,"productId":1,"count":10,"money":100.00}' http://localhost:8081/order/create
7.2.2 预期结果
- 订单服务:创建订单成功
- 库存服务:库存从100减到90
- 账户服务:账户余额从1000减到0
- 全局事务提交成功
7.3 测试回滚流程
7.3.1 修改订单服务代码
在OrderService.createOrder方法中添加异常:
java
// 模拟异常,测试回滚
if (true) throw new RuntimeException("模拟异常,测试回滚");
7.3.2 发送请求
同7.2.1
7.3.3 预期结果
- 订单服务:订单创建后回滚
- 库存服务:库存从100减到90后回滚到100
- 账户服务:账户余额从1000减到0后回滚到1000
- 全局事务回滚成功
8. Seata AT模式核心机制
8.1 SQL执行与Undo Log生成
8.1.1 执行SQL
sql
UPDATE inventory SET count = count - 10 WHERE product_id = 1;
8.1.2 生成Undo Log
Seata会自动拦截SQL,生成Undo Log并写入数据库:
sql
INSERT INTO undo_log (
branch_id, xid, context, rollback_info, log_status, log_created, log_modified
) VALUES (
123456, 'xid-123', 'serializable', '{"beforeImage":{"count":100},"afterImage":{"count":90}}', 1, NOW(), NOW()
);
8.2 事务回滚机制
8.2.1 回滚流程
- TC通知RM执行回滚
- RM读取Undo Log
- RM执行回滚SQL:
UPDATE inventory SET count = 100 WHERE product_id = 1 AND count = 90; - RM删除Undo Log
8.2.2 回滚条件
- 乐观锁机制:回滚SQL包含当前值条件,确保数据未被其他事务修改
- 幂等性:多次回滚操作结果一致
9. 监控与管理
9.1 Seata控制台
Seata提供了Web控制台,用于监控全局事务状态:
- 访问地址:http://localhost:7091
- 默认用户名/密码:seata/seata
9.2 核心监控指标
- 全局事务数量:当前活跃的全局事务数量
- 分支事务数量:当前活跃的分支事务数量
- 事务成功率:全局事务成功率
- 事务平均耗时:全局事务平均执行时间
- 回滚事务数量:回滚的全局事务数量
10. 最佳实践
10.1 配置优化
- 数据库连接池:增大连接池大小,支持更多并发事务
- Undo Log清理:定期清理过期的Undo Log,避免磁盘空间占用过大
- 超时设置:合理设置全局事务超时时间,避免事务长时间占用资源
10.2 开发注意事项
- @GlobalTransactional:仅在事务发起方添加,分支事务无需添加
- 本地事务 :分支事务仍需添加
@Transactional注解 - 幂等性:业务逻辑需考虑幂等性,避免重复执行
- 异常处理:合理处理异常,确保事务能正确回滚
- 避免长事务:全局事务应尽可能短,减少资源占用
10.3 部署建议
- Seata Server高可用:部署多个Seata Server实例,避免单点故障
- 数据库分离:Seata数据库与业务数据库分离,提高性能
- 网络优化:确保Seata Server与业务服务网络通畅,减少网络延迟
11. 常见问题与解决方案
11.1 问题:全局事务未生效
解决方案:
- 检查@GlobalTransactional注解是否添加
- 检查XID是否正确传递(通过Feign拦截器)
- 检查Seata配置是否正确
11.2 问题:事务回滚失败
解决方案:
- 检查Undo Log表是否正确创建
- 检查数据库权限是否足够
- 检查乐观锁条件是否满足(数据是否被其他事务修改)
11.3 问题:性能问题
解决方案:
- 优化SQL,减少执行时间
- 增大Seata Server的线程池大小
- 考虑拆分大事务为多个小事务
12. 总结
Seata AT模式是一种非侵入式的分布式事务解决方案,通过自动生成Undo Log和两阶段提交,实现了分布式事务的自动管理。其核心优势包括:
- 非侵入式:无需修改业务代码,只需添加@GlobalTransactional注解
- 自动回滚:自动生成Undo Log,支持事务回滚
- 高性能:异步提交,减少锁竞争
- 易于集成:与Spring Cloud、Dubbo等框架无缝集成
- 可靠性高:支持事务回滚,确保数据一致性
通过本实例,您可以深入理解Seata AT模式的工作原理和实现细节,并在实际项目中应用Seata AT模式解决分布式事务问题。