分布式事务没那么可怕:零基础也能掌握的最佳实践

一行代码半杯茶
2026-01-15 14:44
阅读 2081

大家好,我是掘金上经常写后端入门教程的老张。作为一名985毕业、如今在一线大厂做全栈开发的工程师,我深知分布式事务对初学者来说有多“劝退”——光是听到“CAP理论”、“两阶段提交”这些词就让人头大。但其实,只要方法对,零基础也能搞懂它

我当初学的时候,也是一脸懵:明明单机数据库里一个 BEGIN...COMMIT 就能搞定的事,为什么拆成两个服务就乱套了?钱扣了,订单却没生成?库存减了,用户却没收到货?这些问题背后,都是分布式事务在作祟。

今天这篇教程,我就用最直白的语言、最贴近实战的例子,带你从零搭建一个能处理分布式事务的后端系统。不用怕,我们一步步来。


一、什么是分布式事务?为什么后端开发者必须懂?

简单说:分布式事务,就是在多个独立的资源(比如数据库、消息队列、第三方服务)之间,保证操作要么全部成功,要么全部失败

想象一个电商下单场景:

  1. 扣减用户账户余额(操作数据库A)
  2. 创建订单记录(操作数据库B)
  3. 发送短信通知(调用短信服务)

这三个操作分布在三个不同的“资源”上。如果第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 "下单成功";
    }
}

第三步:测试事务回滚

  1. 启动 Seata Server
  2. 启动 user-service 和 order-service
  3. 发送 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% 的业务其实不需要强一致!先问自己:这个操作失败了,能不能靠人工或异步补偿解决?


六、下一步学习建议

恭喜你,已经迈出了分布式事务的第一步!接下来可以:

  1. 深入 Seata 源码:看看 undo_log 是怎么生成的,AT 模式如何解析 SQL。
  2. 尝试 TCC 模式:实现一个“冻结库存 -> 扣减库存 -> 释放库存”的完整流程。
  3. 学习 Saga 模式:用 Netflix Conductor 或自研状态机处理长流程。
  4. 了解云原生方案:阿里云 GTS、腾讯云 DTF,它们封装得更易用。

🌟 最后提醒:分布式事务不是银弹。能不分库就不分库,能不用分布式事务就不用。很多问题通过合理设计(比如把相关数据放在同一库)就能避免。


结语

分布式事务听起来高大上,但拆解开来,无非是“协调多个资源,保证一起成功或一起失败”。作为后端开发者,你不需要一开始就精通所有方案,但必须知道:当系统出现跨资源操作时,一致性风险在哪里,有哪些工具可以兜底

希望这篇教程能帮你撕掉“分布式事务很难”的标签。我在掘金还会持续更新更多零基础后端实战,欢迎关注!

记住:每一个复杂的系统,都是从一行 Hello World 开始的。你,也可以做到。

评论 0

最热最新
暂无评论
一行代码半杯茶Lv.1
0
影响力
0
文章
0
粉丝