分布式事务怎么搞?一个Java后端老兵的实战指南
大家好,我是老张,一个写了五年 Java 后端的老兵。这几年带过不少实习生,也面试过上百位候选人。每次聊到分布式系统,“分布式事务” 几乎是必问的面试题。很多同学一听到这个词就懵了:“事务不是数据库的事吗?怎么还分布式了?”
我当初学的时候也一样——看着各种术语(两阶段提交、TCC、Saga……)头都大了。但其实,只要用对方法,分布式事务没那么可怕。今天我就用一个真实的小项目,手把手带你从零搞懂它,还会告诉你生产环境里真正管用的最佳实践。
本文所有代码都已开源在 GitHub:github.com/zhangsan/distributed-tx-demo(示例链接,实际可替换为真实仓库)
一、为什么我们需要分布式事务?
先别急着看代码,我们得先搞清楚问题在哪。
假设你正在开发一个电商系统,用户下单时要同时做两件事:
- 扣减库存(调用库存服务)
- 创建订单(调用订单服务)
这两个操作必须要么都成功,要么都失败。这就是“事务”的核心要求:原子性。
但在单体应用中,我们可以直接用 @Transactional 注解搞定。可一旦拆成微服务,两个操作跑在不同 JVM、不同数据库上,本地事务就失效了!
这时候,就需要分布式事务来保证数据一致性。
💡 小贴士:分布式事务 ≠ 多个数据库事务。它是跨服务、跨资源(如数据库、消息队列)的一致性协调机制。
二、环境准备:5分钟搭好开发环境
我们要用最主流的技术栈来演示,确保你学完就能用在工作中。
所需工具清单
| 工具 | 版本 | 用途 |
|---|---|---|
| JDK | 17 | Java 运行环境 |
| Maven | 3.8+ | 项目依赖管理 |
| MySQL | 8.0 | 数据库 |
| Docker | 最新版 | 快速启动 Seata(分布式事务中间件) |
| IDE | IDEA / VS Code | 开发工具 |
第一步:创建两个 Spring Boot 服务
我们模拟两个微服务:
order-service:处理订单inventory-service:处理库存
用 Spring Initializr 快速生成(选择 Web, JPA, MySQL Driver, Lombok):
# 创建订单服务
spring init --dependencies=web,data-jpa,mysql,lombok order-service
# 创建库存服务
spring init --dependencies=web,data-jpa,mysql,lombok inventory-service
第二步:启动 Seata Server(分布式事务协调者)
Seata 是阿里开源的分布式事务解决方案,GitHub 上 star 超过 24k,国内大厂广泛使用。
用 Docker 一行命令启动:
docker run -d --name seata-server \
-p 8091:8091 \
-e SEATA_PORT=8091 \
seataio/seata-server:1.7.0
✅ 验证:访问
http://localhost:8091,看到 Seata 控制台即成功。
三、核心概念:3种方案,一张表讲清楚
分布式事务有多种实现方式。作为初学者,你只需要掌握这三种主流方案:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| XA 模式 | 基于数据库 XA 协议(两阶段提交) | 强一致性,开发简单 | 性能差,锁粒度大 | 金融类强一致场景 |
| AT 模式(Seata 默认) | 自动代理 SQL,记录 undo log | 无侵入,性能较好 | 只支持关系型数据库 | 大多数互联网业务 |
| TCC 模式 | 业务层实现 Try/Confirm/Cancel | 灵活,性能高 | 代码侵入性强 | 高并发核心链路 |
📌 我建议新手从 AT 模式 入手!它对代码改动最小,最容易上手。
四、实战:用 Seata + AT 模式实现下单扣库存
我们用 order-service 调用 inventory-service,实现一个完整的分布式事务。
步骤1:配置数据库和 undo_log 表
两个服务都要连接自己的数据库,并额外建一张 undo_log 表(Seata 用它回滚):
-- 在 order_db 和 inventory_db 中都执行
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
);
步骤2:添加 Seata 依赖
在两个服务的 pom.xml 中加入:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
步骤3:配置 application.yml
以 order-service 为例:
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_db?useSSL=false
username: root
password: 123456
seata:
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: file
config:
type: file
⚠️ 注意:
inventory-service的端口设为 8082,数据库连inventory_db。
步骤4:编写业务代码
订单服务(发起方)
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private OrderService orderService;
// 关键:加上 @GlobalTransactional!
@GlobalTransactional
@PostMapping("/create")
public String createOrder(@RequestBody OrderRequest request) {
// 1. 创建订单(本地事务)
orderService.saveOrder(request.getUserId(), request.getProductId());
// 2. 调用库存服务扣减库存
String result = restTemplate.postForObject(
"http://localhost:8082/deduct",
request,
String.class
);
if ("success".equals(result)) {
return "订单创建成功";
} else {
throw new RuntimeException("库存不足");
}
}
}
库存服务(参与方)
@RestController
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@PostMapping("/deduct")
public String deductStock(@RequestBody OrderRequest request) {
boolean success = inventoryService.deduct(request.getProductId(), 1);
return success ? "success" : "fail";
}
}
🔑 核心点:只有发起方加
@GlobalTransactional,参与方保持普通@Transactional即可!
步骤5:测试事务回滚
- 启动 Seata Server
- 启动
inventory-service - 启动
order-service - 调用下单接口:
curl -X POST http://localhost:8081/create \
-H "Content-Type: application/json" \
-d '{"userId": 1, "productId": 1001}'
故意让库存服务抛异常(比如库存不足),你会发现:
- 订单没创建
- 库存没扣减
- Seata 自动回滚!
✅ 完美实现分布式事务!
五、新手常踩的5个坑(附解决方案)
我带过的实习生,90% 都在这几个地方栽过跟头:
❌ 坑1:忘记建 undo_log 表
现象:事务不生效,数据不一致
解决:两个数据库都必须有 undo_log 表!
❌ 坑2:@GlobalTransactional 加错位置
现象:事务没触发
解决:只加在发起方的方法上,且该方法必须是 public!
❌ 坑3:服务间调用没走 HTTP/RPC
现象:Seata 无法传播事务上下文
解决:必须通过网络调用(如 RestTemplate、Feign),不能直接 new 对象调用!
❌ 坑4:数据库连接池配置错误
现象:报错 “can not register RM”
解决:数据源必须被 Seata 代理。Spring Boot 下通常自动完成,但若自定义了 DataSource 需手动包装:
@Bean
public DataSource dataSource() {
return new DataSourceProxy(actualDataSource);
}
❌ 坑5:超时时间太短
现象:事务莫名其妙回滚
解决:在 @GlobalTransactional 中设置超时:
@GlobalTransactional(timeoutMills = 60000)
六、面试题高频考点(附答案思路)
分布式事务是高级 Java 岗必考题,这里给你划重点:
Q1:Seata 的 AT 模式原理是什么?
答:基于两阶段提交。第一阶段:执行 SQL 并记录 undo log;第二阶段:提交(删除 undo log)或回滚(用 undo log 恢复数据)。
Q2:TCC 和 AT 有什么区别?
答:AT 由框架自动处理回滚,TCC 需业务代码实现 Try/Confirm/Cancel 三个方法,更灵活但更复杂。
Q3:如何保证消息队列和数据库的一致性?
答:用“本地消息表”或 RocketMQ 的事务消息。核心思想:先写 DB,再发消息,失败则重试。
七、下一步学习建议
你已经掌握了分布式事务的入门实战!接下来可以:
- 深入 Seata 源码:看看
GlobalTransactionalInterceptor怎么工作的 - 尝试 TCC 模式:实现一个资金转账场景(冻结→确认/取消)
- 研究 Saga 模式:适用于长事务(如订单超时取消)
- 阅读官方文档:Seata GitHub Wiki
🌟 最后提醒:不要为了用分布式事务而用它!能用本地事务+补偿机制解决的,优先选简单方案。
结语
分布式事务听起来高大上,但拆开看,就是“跨服务的一致性协调”。用好 Seata 这样的工具,你完全可以在不影响业务逻辑的前提下,轻松搞定它。
我写这篇教程,就是希望你少走我当年踩过的坑。记住:技术的本质是解决问题,不是炫技。
代码已上传 GitHub,欢迎 star & fork:github.com/zhangsan/distributed-tx-demo
有问题?评论区留言,我会一一解答。下期我们聊聊「分布式 ID 生成方案」,记得关注!

评论 0