分布式事务太难?手把手教你用Java搞定它

代码自留地
2026-01-13 16:16
阅读 209

大家好,我是一名从培训班出来的前端开发,但别误会——这篇教程讲的是后端的分布式事务。你可能会问:“一个前端为啥写 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 模式怎么工作?

  1. 全局事务开始:订单服务调用 GlobalTransaction.begin()
  2. 分支注册:每个本地数据库操作前,Seata 会记录“前镜像”(before image)
  3. 提交 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 模式(但代码复杂度高很多)。


七、学习建议:下一步怎么走?

  1. 先跑通本地 demo:把上面三个服务跑起来,亲手制造一次回滚。
  2. 理解 undo_log 表:Seata 会在每个业务库自动建 undo_log 表,看看里面存了什么。
  3. 尝试 TCC 模式:当你需要更精细控制(比如预扣款、确认、取消),再学 TCC。
  4. 不要过度设计:不是所有系统都需要分布式事务!如果业务允许“最终一致性”,用消息队列(如 RocketMQ 事务消息)更简单。

🌟 我的经验:我当年为了炫技,在一个内部工具里硬上 Seata,结果调试三天。后来发现——能不用分布式事务,就别用。先评估业务是否真的需要强一致性!


结语

分布式事务听起来高大上,但用 Seata 的 AT 模式,其实就三步:

  1. 启动 Seata Server
  2. 业务方法加 @GlobalTransactional
  3. 参与方保留 @Transactional

希望这篇 纯实践导向 的 Java 教程,能帮你跨过第一道坎。记住:所有复杂的概念,拆解到代码层面,都不过是几行注解和配置

如果你跟着做了一遍,欢迎在评论区留言你的踩坑经历!我们一起进步。

评论 0

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