分布式事务怎么搞?一个Java后端老兵的实战指南

API打磨师
2026-01-14 04:27
阅读 238

大家好,我是老张,一个写了五年 Java 后端的老兵。这几年带过不少实习生,也面试过上百位候选人。每次聊到分布式系统,“分布式事务” 几乎是必问的面试题。很多同学一听到这个词就懵了:“事务不是数据库的事吗?怎么还分布式了?”

我当初学的时候也一样——看着各种术语(两阶段提交、TCC、Saga……)头都大了。但其实,只要用对方法,分布式事务没那么可怕。今天我就用一个真实的小项目,手把手带你从零搞懂它,还会告诉你生产环境里真正管用的最佳实践

本文所有代码都已开源在 GitHub:github.com/zhangsan/distributed-tx-demo(示例链接,实际可替换为真实仓库)


一、为什么我们需要分布式事务?

先别急着看代码,我们得先搞清楚问题在哪

假设你正在开发一个电商系统,用户下单时要同时做两件事:

  1. 扣减库存(调用库存服务)
  2. 创建订单(调用订单服务)

这两个操作必须要么都成功,要么都失败。这就是“事务”的核心要求:原子性

但在单体应用中,我们可以直接用 @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:测试事务回滚

  1. 启动 Seata Server
  2. 启动 inventory-service
  3. 启动 order-service
  4. 调用下单接口:
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,再发消息,失败则重试。


七、下一步学习建议

你已经掌握了分布式事务的入门实战!接下来可以:

  1. 深入 Seata 源码:看看 GlobalTransactionalInterceptor 怎么工作的
  2. 尝试 TCC 模式:实现一个资金转账场景(冻结→确认/取消)
  3. 研究 Saga 模式:适用于长事务(如订单超时取消)
  4. 阅读官方文档Seata GitHub Wiki

🌟 最后提醒:不要为了用分布式事务而用它!能用本地事务+补偿机制解决的,优先选简单方案。


结语

分布式事务听起来高大上,但拆开看,就是“跨服务的一致性协调”。用好 Seata 这样的工具,你完全可以在不影响业务逻辑的前提下,轻松搞定它。

我写这篇教程,就是希望你少走我当年踩过的坑。记住:技术的本质是解决问题,不是炫技

代码已上传 GitHub,欢迎 star & fork:github.com/zhangsan/distributed-tx-demo

有问题?评论区留言,我会一一解答。下期我们聊聊「分布式 ID 生成方案」,记得关注!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝