分布式事务太难?手把手教你用Java搞定它
大家好,我是一名从培训班出来的前端开发,但别误会——这篇教程讲的是后端的分布式事务。你可能会问:“一个前端为啥写 Java 后端的内容?”其实,我在转行初期也学过后端基础,深知很多零基础同学第一次听到“分布式事务”时那种懵圈感:啥是事务?分布又是什么意思?为什么本地能跑通,一上微服务就崩?
我当初学的时候,光看理论文档根本搞不懂,直到自己动手搭了个小项目才明白其中门道。所以今天,我就用最朴素的语言、最真实的代码,带大家一步步搞懂 分布式事务在 Java 中的最佳实践。无论你是刚学完 Spring Boot 的新手,还是正在做毕业设计的学生,这篇教程都能让你少走弯路。
一、什么是分布式事务?为什么要关心它?
先说个生活例子:
你在网上下单买书:
- 第一步:扣减库存(库存服务)
- 第二步:扣钱(支付服务)
- 第三步:生成订单(订单服务)
这三个操作必须全部成功,或者全部失败。如果库存扣了,钱没扣,那商家亏了;如果钱扣了,订单没生成,用户就炸了。
在单体应用里,这叫“本地事务”,用 @Transactional 注解就能搞定。但一旦拆成多个微服务(比如 Spring Cloud 架构),每个服务有自己的数据库,跨服务的操作就无法用一个数据库事务包住——这就是分布式事务要解决的问题。
二、开发环境准备(手把手配置)
我们用最主流的 Java 技术栈来演示:
- JDK 17(推荐 LTS 版本)
- Maven
- Spring Boot 3.2+
- MySQL 8.0(每个服务独立数据库)
- Seata(开源分布式事务框架)
步骤 1:安装 MySQL 并创建三个数据库
CREATE DATABASE order_db;
CREATE DATABASE inventory_db;
CREATE DATABASE account_db;
每个库建一张表:
-- order_db.orders
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL
);
-- inventory_db.inventory
CREATE CREATE TABLE inventory (
product_id BIGINT PRIMARY KEY,
stock INT NOT NULL
);
-- account_db.accounts
CREATE TABLE accounts (
user_id BIGINT PRIMARY KEY,
balance DECIMAL(10,2) NOT NULL
);
步骤 2:引入 Seata 依赖(关键!)
在每个微服务的 pom.xml 中添加:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
💡 避坑提示:Seata 版本必须和 Spring Boot 兼容!1.7.0 支持 Spring Boot 3.x,别用旧版。
步骤 3:配置 Seata(统一事务协调器)
你需要一个 Seata Server(事务协调器 TC)。可以下载 Seata 官方 release,解压后修改 conf/application.yml:
server:
port: 7091
store:
mode: db
db:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?useSSL=false
user: root
password: your_password
然后执行 nacos-config.sh 或直接启动 seata-server.bat(Windows)。
⚠️ 注意:生产环境建议用 Nacos + Seata 高可用部署,但新手本地调试用 standalone 模式即可。
三、核心概念:用大白话解释 Seata 的 AT 模式
Seata 提供多种模式,AT 模式(Auto Transaction)最适合新手,因为它对业务代码侵入最小。
AT 模式怎么工作?
- 全局事务开始:订单服务调用
GlobalTransaction.begin() - 分支注册:每个本地数据库操作前,Seata 会记录“前镜像”(before image)
- 提交 or 回滚:
- 如果所有服务都成功 → 全局提交
- 任一服务失败 → Seata 用“前镜像”自动回滚其他服务的数据
整个过程对开发者几乎是透明的!
关键注解说明
| 注解 | 作用 |
|---|---|
@GlobalTransactional |
标记在发起方方法上,开启全局事务 |
@Transactional |
本地事务(仍需保留!) |
四、实战:用 Java 写一个下单流程(完整代码)
我们搭建三个 Spring Boot 微服务:
order-service(订单)inventory-service(库存)account-service(账户)
所有服务都注册到同一个注册中心(如 Nacos),这里为简化,用 Feign 直连。
1. 订单服务(事务发起方)
@RestController
public class OrderController {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private AccountClient accountClient;
@GlobalTransactional // ← 全局事务入口!
@PostMapping("/createOrder")
public String createOrder(@RequestParam Long userId,
@RequestParam Long productId) {
// 1. 创建订单(本地事务)
orderMapper.insert(userId, productId, "PENDING");
// 2. 扣库存(远程调用)
inventoryClient.decreaseStock(productId);
// 3. 扣余额(远程调用)
accountClient.decreaseBalance(userId, 100);
// 如果到这里没抛异常,全局提交
return "Order created!";
}
}
2. 库存服务(参与方)
@Service
public class InventoryService {
@Transactional // ← 本地事务必须加!
public void decreaseStock(Long productId) {
Inventory inventory = inventoryMapper.selectById(productId);
if (inventory.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
inventoryMapper.updateStock(productId, inventory.getStock() - 1);
}
}
3. 账户服务(参与方)
@Service
public class AccountService {
@Transactional
public void decreaseBalance(Long userId, BigDecimal amount) {
Account account = accountMapper.selectById(userId);
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
accountMapper.updateBalance(userId, account.getBalance().subtract(amount));
}
}
4. 配置文件(每个服务都要加)
application.yml 示例:
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: file # 本地调试用 file,生产用 nacos
config:
type: file
✅ 重要:
tx-service-group的值必须和 Seata Server 配置一致!
五、测试:模拟失败场景
现在故意让账户服务抛异常:
// 在 AccountService 中加一行
if (userId == 999) throw new RuntimeException("模拟支付失败");
调用 /createOrder?userId=999&productId=1,你会发现:
- 订单插入了(但状态是 PENDING)
- 库存被扣了
- 但几秒后,订单和库存自动回滚!
这就是 Seata 的神奇之处——自动补偿。
六、新手常见问题 & 解决方案
Q1:为什么加了 @GlobalTransactional 还是不回滚?
可能原因:
- 忘记在参与方加
@Transactional - Seata Server 没启动或配置不匹配
- 数据库表没加 主键(Seata 要求表必须有主键!)
✅ 检查清单:
- 所有涉及的表都有主键
- 每个服务的
seata.enabled=true - Seata Server 日志显示“TM register success”
Q2:Seata 支持哪些数据库?
| 数据库 | 支持情况 |
|---|---|
| MySQL | ✅ 完全支持 |
| PostgreSQL | ✅ |
| Oracle | ✅ |
| MongoDB | ❌ 不支持(非关系型) |
Q3:性能会不会很差?
AT 模式会有额外的 undo_log 表写入,但对大多数业务(QPS < 1000)影响不大。如果追求极致性能,可考虑 TCC 模式(但代码复杂度高很多)。
七、学习建议:下一步怎么走?
- 先跑通本地 demo:把上面三个服务跑起来,亲手制造一次回滚。
- 理解 undo_log 表:Seata 会在每个业务库自动建
undo_log表,看看里面存了什么。 - 尝试 TCC 模式:当你需要更精细控制(比如预扣款、确认、取消),再学 TCC。
- 不要过度设计:不是所有系统都需要分布式事务!如果业务允许“最终一致性”,用消息队列(如 RocketMQ 事务消息)更简单。
🌟 我的经验:我当年为了炫技,在一个内部工具里硬上 Seata,结果调试三天。后来发现——能不用分布式事务,就别用。先评估业务是否真的需要强一致性!
结语
分布式事务听起来高大上,但用 Seata 的 AT 模式,其实就三步:
- 启动 Seata Server
- 业务方法加
@GlobalTransactional - 参与方保留
@Transactional
希望这篇 纯实践导向 的 Java 教程,能帮你跨过第一道坎。记住:所有复杂的概念,拆解到代码层面,都不过是几行注解和配置。
如果你跟着做了一遍,欢迎在评论区留言你的踩坑经历!我们一起进步。

评论 0