分布式事务没那么可怕:零基础也能掌握的最佳实践
大家好,我是掘金上经常写后端入门教程的老张。作为一名985毕业、如今在一线大厂做全栈开发的工程师,我深知分布式事务对初学者来说有多“劝退”——光是听到“CAP理论”、“两阶段提交”这些词就让人头大。但其实,只要方法对,零基础也能搞懂它。
我当初学的时候,也是一脸懵:明明单机数据库里一个 BEGIN...COMMIT 就能搞定的事,为什么拆成两个服务就乱套了?钱扣了,订单却没生成?库存减了,用户却没收到货?这些问题背后,都是分布式事务在作祟。
今天这篇教程,我就用最直白的语言、最贴近实战的例子,带你从零搭建一个能处理分布式事务的后端系统。不用怕,我们一步步来。
一、什么是分布式事务?为什么后端开发者必须懂?
简单说:分布式事务,就是在多个独立的资源(比如数据库、消息队列、第三方服务)之间,保证操作要么全部成功,要么全部失败。
想象一个电商下单场景:
- 扣减用户账户余额(操作数据库A)
- 创建订单记录(操作数据库B)
- 发送短信通知(调用短信服务)
这三个操作分布在三个不同的“资源”上。如果第1步成功了,第2步失败了,那用户钱没了,订单却没生成——这就是典型的数据不一致问题。
✅ 关键点:分布式事务的核心目标是 “一致性” ——不让系统处于“半成功”状态。
作为后端工程师,你迟早会遇到微服务架构。一旦服务拆分,本地事务(如MySQL的InnoDB事务)就失效了,你必须主动设计分布式事务方案。
二、环境准备:5分钟搭好实验环境
为了让你亲手体验,我们先准备好开发环境。本教程使用 Java + Spring Boot + MySQL + Seata(一个开源的分布式事务框架),但思路适用于任何语言。
1. 安装必要软件
- JDK 17(推荐)
- Maven
- Docker(用于快速启动Seata Server和MySQL)
2. 启动MySQL容器(两个数据库实例)
# 启动第一个MySQL(模拟用户服务)
docker run -d --name mysql-user -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 mysql:8.0
# 启动第二个MySQL(模拟订单服务)
docker run -d --name mysql-order -e MYSQL_ROOT_PASSWORD=123456 -p 3307:3307 mysql:8.0
3. 初始化数据库
分别连接两个MySQL,执行以下SQL:
-- 在 mysql-user 中
CREATE DATABASE user_db;
USE user_db;
CREATE TABLE account (
id BIGINT PRIMARY KEY,
user_id VARCHAR(50),
balance DECIMAL(10,2)
);
INSERT INTO account VALUES (1, 'U1001', 1000.00);
-- 在 mysql-order 中
CREATE DATABASE order_db;
USE order_db;
CREATE tABLE t_order (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(50),
user_id VARCHAR(50),
amount DECIMAL(10,2)
);
4. 启动Seata Server(分布式事务协调器)
Seata 是阿里巴巴开源的分布式事务解决方案,对新手非常友好。
# 拉取镜像并启动
docker run -d --name seata-server \
-p 8091:8091 \
-e SEATA_PORT=8091 \
-e STORE_MODE=db \
seataio/seata-server:1.5.2
💡 提示:Seata 内部会自动创建 undo_log 表(用于回滚),你无需手动建。
三、核心概念:用大白话讲清楚分布式事务
别被术语吓到,记住这3个核心角色就够了:
| 角色 | 作用 | 类比 |
|---|---|---|
| TC (Transaction Coordinator) | 事务协调器,全局调度者 | 婚礼司仪 |
| TM (Transaction Manager) | 事务发起方,决定全局提交或回滚 | 新郎(发起婚礼) |
| RM (Resource Manager) | 资源管理者,管理本地事务 | 各个伴郎伴娘(执行具体任务) |
三种主流方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 先问“能不能提交”,再真正提交 | 强一致性 | 性能差,阻塞 | 银行转账等强一致场景 |
| TCC(Try-Confirm-Cancel) | 业务层面实现预留/确认/取消 | 灵活,性能好 | 开发复杂 | 库存、积分等可逆操作 |
| Saga | 一长串本地事务+补偿操作 | 无锁,并发高 | 最终一致性 | 订单流程、审批流 |
📌 新手建议:先从 Seata 的 AT 模式(基于2PC改进)入手,它能自动处理回滚,代码侵入性最小。
四、实战:用Seata实现一个分布式下单功能
我们现在用两个Spring Boot服务模拟“用户扣款 + 创建订单”。
第一步:创建用户服务(user-service)
pom.xml 添加依赖:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
</dependency>
application.yml 配置:
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false
username: root
password: 123456
seata:
enabled: true
application-id: user-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: file # 简化,实际用Nacos
Service 层代码(关键!):
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 注意:这个方法会被Seata代理,自动加入全局事务
public void deduct(String userId, BigDecimal amount) {
String sql = "UPDATE account SET balance = balance - ? WHERE user_id = ?";
int rows = jdbcTemplate.update(sql, amount, userId);
if (rows == 0) {
throw new RuntimeException("余额不足");
}
// 模拟异常:让订单服务有机会失败
// if (true) throw new RuntimeException("故意失败");
}
}
第二步:创建订单服务(order-service)
配置类似,只是数据库连 3307/order_db。
Controller 接收下单请求:
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
// 全局事务入口!@GlobalTransactional 注解是关键
@GlobalTransactional
@PostMapping("/create")
public String createOrder(@RequestBody OrderRequest request) {
// 1. 远程调用用户服务扣款(用Feign或RestTemplate)
restTemplate.postForObject("http://localhost:8081/deduct",
Map.of("userId", request.getUserId(), "amount", request.getAmount()),
Void.class);
// 2. 本地创建订单
orderService.create(request.getUserId(), request.getAmount());
return "下单成功";
}
}
第三步:测试事务回滚
- 启动 Seata Server
- 启动 user-service 和 order-service
- 发送 POST 请求:
{
"userId": "U1001",
"amount": 200
}
✅ 正常情况:两个库数据都更新。
❌ 故意在 AccountService.deduct() 中抛异常:你会发现 user_db 的余额没变,order_db 也没新订单——Seata 自动回滚了!
🔍 原理揭秘:Seata 在执行
UPDATE时,会偷偷记录 before image(修改前的数据)到undo_log表。一旦全局事务失败,就用这个镜像反向执行 SQL 回滚。
五、新手常见问题 & 避坑指南
Q1:为什么我的事务没回滚?
- 检查点1:确保
@GlobalTransactional加在 入口方法 上(通常是 Controller 或 Service 的 public 方法)。 - 检查点2:所有参与的服务必须注册到同一个 Seata TC(看
tx-service-group是否一致)。 - 检查点3:数据库表必须有 主键!Seata 靠主键定位回滚记录。
Q2:Seata 性能很差怎么办?
- 默认 AT 模式会有全局锁,高并发场景可考虑:
- 改用 TCC 模式(自己写 Try/Confirm/Cancel)
- 或接受 最终一致性,用 本地消息表 + 定时补偿
Q3:一定要用 Seata 吗?
不一定!如果你的业务能接受短暂不一致(比如发短信失败可以重试),可以用更简单的方案:
// 伪代码:本地消息表模式
@Transactional
public void createOrder() {
// 1. 插入订单
orderDao.insert(order);
// 2. 插入“待发送短信”消息到本地表
messageDao.insert(new Message("SEND_SMS", orderId));
}
// 另一个定时任务扫描 message 表,发送短信并标记成功
💡 经验之谈:80% 的业务其实不需要强一致!先问自己:这个操作失败了,能不能靠人工或异步补偿解决?
六、下一步学习建议
恭喜你,已经迈出了分布式事务的第一步!接下来可以:
- 深入 Seata 源码:看看
undo_log是怎么生成的,AT 模式如何解析 SQL。 - 尝试 TCC 模式:实现一个“冻结库存 -> 扣减库存 -> 释放库存”的完整流程。
- 学习 Saga 模式:用 Netflix Conductor 或自研状态机处理长流程。
- 了解云原生方案:阿里云 GTS、腾讯云 DTF,它们封装得更易用。
🌟 最后提醒:分布式事务不是银弹。能不分库就不分库,能不用分布式事务就不用。很多问题通过合理设计(比如把相关数据放在同一库)就能避免。
结语
分布式事务听起来高大上,但拆解开来,无非是“协调多个资源,保证一起成功或一起失败”。作为后端开发者,你不需要一开始就精通所有方案,但必须知道:当系统出现跨资源操作时,一致性风险在哪里,有哪些工具可以兜底。
希望这篇教程能帮你撕掉“分布式事务很难”的标签。我在掘金还会持续更新更多零基础后端实战,欢迎关注!
记住:每一个复杂的系统,都是从一行 Hello World 开始的。你,也可以做到。

评论 0