分布式事务解决方案:最佳实践(零基础入门)
大家好,我是一名工作5年的后端开发工程师。过去几年里,我参与过多个高并发、多服务的系统架构设计,也踩过不少“分布式事务”的坑。今天写这篇教程,是因为我当初学的时候,看了很多理论文章,但一到实战就懵了——不知道怎么选方案、怎么写代码、怎么排查问题。
所以,我想用最简单的话、最真实的代码,带完全零基础的朋友搞懂:什么是分布式事务?为什么需要它?以及在真实项目中,我们到底该怎么用?
一、先说人话:分布式事务到底是啥?
想象一个电商场景:
用户下单 → 扣库存 → 扣余额 → 生成订单
如果这三个操作都在同一个数据库里,用 BEGIN TRANSACTION 和 COMMIT/ROLLBACK 就能保证“要么全成功,要么全失败”——这叫本地事务。
但现在,系统拆成了微服务:
- 订单服务(独立数据库)
- 库存服务(独立数据库)
- 账户服务(独立数据库)
这时候,三个操作跨了三个数据库,本地事务不管用了!
分布式事务就是解决这种“跨服务、跨数据库”的一致性问题。
💡 简单说:分布式事务 = 让多个独立系统像一个整体一样,要么一起成功,要么一起失败。
二、环境准备:5分钟搭好实验环境
我们用最轻量的方式搭建一个可运行的 demo。你只需要:
前置条件
- JDK 8+
- Maven
- Docker(用于启动 MySQL 实例)
- IDE(IntelliJ IDEA 或 VS Code)
步骤 1:启动两个 MySQL 实例(模拟两个服务的数据库)
# 启动订单数据库(端口3307)
docker run -d --name order-db -p 3307:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=order_db mysql:8.0
# 启动账户数据库(端口3308)
docker run -d --name account-db -p 3308:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-e MYSQL_DATABASE=account_db mysql:8.0
步骤 2:创建 Spring Boot 项目(Maven)
pom.xml 关键依赖:
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Seata(分布式事务框架)-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
</dependencies>
🛠️ 工具提示:Seata 是阿里开源的分布式事务中间件,支持 AT、TCC、Saga 等模式,对新手最友好。
三、核心概念:3 种主流方案怎么选?
不是所有场景都用同一种方案。以下是我在运营真实系统时总结的选型指南:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Seata AT 模式 | 大多数 CRUD 场景(如订单+库存) | 自动回滚,代码侵入小 | 需要全局锁,性能一般 |
| TCC 模式 | 金融、支付等强一致性场景 | 性能高,控制精细 | 代码复杂,需实现 Confirm/Cancel |
| 消息队列最终一致性 | 异步解耦场景(如发通知、积分) | 高可用、高性能 | 只能保证最终一致,不能回滚 |
✅ 对初学者:从 Seata AT 模式开始!它最接近“本地事务”的体验。
关键术语解释(用大白话):
- TC(Transaction Coordinator):事务协调器,相当于“裁判”,决定要不要回滚。
- TM(Transaction Manager):发起全局事务的服务(比如订单服务)。
- RM(Resource Manager):参与事务的服务(比如库存、账户服务)。
流程文字版:
1. TM 开启全局事务(@GlobalTransactional)
2. 调用 RM1(库存)→ RM2(账户)
3. 所有 RM 注册分支事务到 TC
4. 如果都成功 → TC 通知所有 RM 提交
5. 如果任一失败 → TC 通知所有 RM 回滚
四、实战项目:用 Seata 实现“下单扣款”
我们来做一个最简 demo:用户下单时,同时扣减账户余额和生成订单。
第一步:初始化数据库表
订单库(order_db)
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) DEFAULT 'created',
PRIMARY KEY (id)
);
账户库(account_db)
CREATE TABLE accounts (
user_id BIGINT PRIMARY KEY,
balance DECIMAL(10,2) NOT NULL DEFAULT 0.00
);
INSERT INTO accounts (user_id, balance) VALUES (1001, 100.00);
第二步:配置双数据源 + Seata
application.yml:
server:
port: 8080
spring:
datasource:
order:
url: jdbc:mysql://localhost:3307/order_db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
account:
url: jdbc:mysql://localhost:3308/account_db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: file # 生产建议用 nacos
config:
type: file
⚠️ 注意:Seata 还需要单独启动 TC 服务(协调器)。为简化,我们用内嵌模式(开发用),生产必须独立部署。
第三步:写代码(关键!)
1. 定义 OrderService(TM 角色)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountClient accountClient; // Feign 调用账户服务
@GlobalTransactional // ← 关键注解!开启全局事务
public void createOrder(Long userId, BigDecimal amount) {
// 1. 创建订单(本地事务)
Orders order = new Orders();
order.setUserId(userId);
order.setAmount(amount);
order.setStatus("created");
orderMapper.insert(order);
// 2. 调用账户服务扣款(远程调用)
boolean success = accountClient.deduct(userId, amount);
if (!success) {
throw new RuntimeException("扣款失败,触发回滚");
}
// 3. 更新订单状态
orderMapper.updateStatus(order.getId(), "paid");
}
}
2. 账户服务接口(AccountClient)
@FeignClient(name = "account-service")
public interface AccountClient {
@PostMapping("/account/deduct")
boolean deduct(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount);
}
3. 账户服务实现(RM 角色)
@RestController
public class AccountController {
@Autowired
private AccountMapper accountMapper;
@PostMapping("/account/deduct")
public boolean deduct(@RequestParam Long userId, @RequestParam BigDecimal amount) {
// Seata 会自动代理此方法,注册为分支事务
Account account = accountMapper.selectById(userId);
if (account.getBalance().compareTo(amount) < 0) {
return false; // 余额不足
}
accountMapper.deduct(userId, amount); // 扣款
return true;
}
}
🔍 重点:只要在
application.yml中配置了 Seata,并且方法被@GlobalTransactional标记,Seata 就会自动拦截数据库操作,生成 undo log(用于回滚)。
五、常见问题 & 避坑指南
❓ 问题1:为什么加了 @GlobalTransactional 还是没回滚?
✅ 检查清单:
- 是否启动了 Seata Server(TC)?
- 两个服务的
tx-service-group名称是否一致? - 数据库表是否加了 主键?(Seata AT 模式要求)
- 是否使用了 MyBatis Plus 的自动填充?可能导致 undo log 异常。
❓ 问题2:Seata 会影响性能吗?
✅ 答:会,但可控。
- AT 模式会在业务表旁生成
undo_log表 - 每次更新会加全局锁(行锁),高并发时可能成为瓶颈
- 建议:非核心链路(如日志、通知)不要纳入分布式事务
❓ 问题3:能用 RabbitMQ 替代 Seata 吗?
✅ 可以,但场景不同:
- Seata:强一致,适合“钱、库存”等不能错的场景
- MQ 最终一致:适合“发短信、更新积分”等允许短暂不一致的场景
示例(MQ 方案伪代码):
// 订单服务
@Transactional
public void createOrder() {
saveOrder();
rabbitTemplate.convertAndSend("order.created", orderId); // 发消息
}
// 账户服务监听
@RabbitListener(queues = "order.created")
public void handleOrderCreated(String orderId) {
deductAccount(); // 失败可重试,最终一致
}
📌 运营经验:核心交易用 Seata,边缘功能用 MQ,这是我们在实际项目中的黄金组合。
六、学习建议:下一步怎么走?
动手跑通 demo
先把本文代码跑起来,故意制造异常(如余额不足),看是否自动回滚。理解 undo_log 表
查看 Seata 自动生成的undo_log表,理解它是如何记录“前镜像”和“后镜像”的。尝试 TCC 模式
当你需要更高性能时(如秒杀),学习 TCC:自己实现try、confirm、cancel。了解 Saga 模式
适用于长事务(如旅行预订:订机票 → 订酒店 → 租车),每个步骤可补偿。关注工具链
- Seata Dashboard:监控事务状态
- SkyWalking / Zipkin:追踪跨服务调用链
- Arthas:线上排查事务卡住问题
结语
分布式事务听起来高大上,但本质就是“让多个服务协作时不掉链子”。我当初学的时候,也是从一个简单的 Seata demo 开始,慢慢理解了背后的原理。
记住:没有银弹。根据业务场景选择方案,比追求技术先进更重要。在真实运营中,稳定性和可维护性永远排在第一位。
希望这篇“手把手”教程能帮你迈出第一步。有问题欢迎留言,我们一起讨论!
✨ 最后送你一句心得:复杂系统,始于简单;高手之道,在于拆解。

评论 0