分布式事务解决方案:最佳实践(零基础入门教程)

报警声中醒来
2025-06-18 06:48
阅读 609

开篇:什么是分布式事务?为什么要学?

开篇:什么是分布式事务?为什么要学?

在我们开发一个简单的网站时,比如用户下单购买商品,通常会涉及多个操作:

  • 扣除库存
  • 记录订单
  • 扣减用户账户余额

这些操作一般都发生在同一个数据库中。如果我们使用传统的事务机制(如 MySQL 的 BEGIN ... COMMIT/ROLLBACK),就可以保证这三个操作要么全部成功,要么都不执行。

但随着业务增长,我们可能会将系统拆分成多个服务,例如:

  • 用户服务
  • 商品服务
  • 订单服务
  • 支付服务

每个服务都有自己的数据库。此时,要完成一次“下单”操作,就涉及到不同数据库之间的数据修改,这种场景下的事务问题,就是我们常说的 分布式事务问题


环境准备:搭建开发环境

环境准备:搭建开发环境

为了完成后续示例,你需要准备以下工具和软件:

1. Java 运行环境(JDK 1.8 或更高)

下载地址:https://www.oracle.com/java/technologies/javase-downloads.html

安装完成后,在终端输入命令查看版本:

java -version

应该看到类似以下输出:

openjdk version "17.0.5" 2022-10-18

2. Maven(构建工具)

下载地址:https://maven.apache.org/download.cgi

安装后执行:

mvn -v

看到版本信息说明安装成功。

3. Spring Boot + Seata(分布式事务中间件)

我们将使用 Spring Boot + MyBatis + Seata 来演示分布式事务。

Seata 是阿里巴巴开源的一个分布式事务框架,对新手很友好。

你也可以使用其他方案如 XA、TCC、Saga 等,但在本教程中我们先从最易上手的 Seata 入手。

安装 Seata Server

访问 GitHub 下载最新版:
👉 https://github.com/seata/seata/releases

解压后,在 conf/start.sh 启动 Seata 服务端:

cd seata-server
sh start.sh

启动成功后会显示如下信息:

started @ 1234ms

表示 Seata 正在运行,默认监听端口为 8091


核心概念:用最简单的语言解释专业术语

核心概念:用最简单的语言解释专业术语

负载均衡配置-2

📌 什么是事务?

事务是一个操作单元,要么全部执行成功,要么全部失败回滚。比如银行转账,不能只扣钱不加钱。

📌 本地事务 vs 分布式事务

  • 本地事务:单个数据库内部的事务,由数据库本身支持,简单可靠。
  • 分布式事务:多个数据库之间的操作需要保持一致性,难度高,需要借助第三方框架处理。

📌 CAP 理论简述

CAP 是理解分布式系统的关键:

  • C:Consistency(一致性)
  • A:Availability(可用性)
  • P:Partition tolerance(分区容忍性)

这三者最多只能同时满足两个。所以我们在设计系统时,要权衡取舍。

对于分布式事务,我们通常更关注 一致性可用性


📌 常见的分布式事务方案

名称 简介 是否适合初学者
两阶段提交(2PC) 经典协议,协调器负责决策,存在单点故障
TCC Try - Confirm - Cancel 模式,手动控制补偿逻辑
Saga 长时间运行的事务,通过逆向操作补偿 ⚠️
最大努力通知 通过消息队列异步通知下游系统
Seata 国内主流开源框架,支持 AT 模式,自动管理事务日志 ✅✅

实战项目:一步步实现 Seata 分布式事务

实战项目:一步步实现 Seata 分布式事务

目标:模拟用户下单购买商品,涉及三个服务:

  • 库存服务(Inventory Service)
  • 订单服务(Order Service)
  • 账户服务(Account Service)

我们要确保下单过程中,所有操作一致成功或失败。

第一步:创建 Spring Boot 多模块项目结构

使用 IDEA 创建一个 Maven 多模块项目,包含三个子模块:

  • inventory-service
  • order-service
  • account-service

pom.xml 中引入公共依赖:

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>

第二步:配置 Seata 客户端

在每个服务的 application.yml 中添加:

seata:
  enabled: true
  application-id: inventory-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

application-id 表示当前服务名称
tx-service-group 是组名,用于集群分组
grouplist 指向刚刚启动的 Seata 服务地址


第三步:初始化数据库表结构(以库存为例)

每个服务都有自己独立的数据库,比如:

库存数据库(inventory_db):

CREATE TABLE t_inventory (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_code VARCHAR(50),
    stock INT
);

插入初始数据:

INSERT INTO t_inventory(product_code, stock) VALUES ('P1001', 100);

第四步:编写业务代码(库存服务)

inventory-service 模块中编写一个接口来扣除库存:

@Service
public class InventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    @Transactional
    public void deductStock(String productCode, int quantity) {
        Inventory inventory = inventoryMapper.selectByProductCode(productCode);

        if (inventory.getStock() < quantity) {
            throw new RuntimeException("库存不足");
        }

        inventory.setStock(inventory.getStock() - quantity);
        inventoryMapper.updateById(inventory);
    }
}

注意:这个方法是被事务注解修饰的方法,会被 Seata 自动增强,参与全局事务。


第五步:调用链开始 —— 订单服务发起请求

在订单服务中发起调用:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private AccountClient accountClient; // Feign 远程调用

    @Autowired
    private InventoryService inventoryService;

    @GetMapping("/place")
    @GlobalTransactional // Seata 注解开启全局事务
    public String placeOrder() {
        try {
            // 1. 扣库存
            inventoryService.deductStock("P1001", 1);

            // 2. 扣余额
            accountClient.deductBalance(50L); // 假设商品价格是50元
        } catch (Exception e) {
            return "下单失败:" + e.getMessage();
        }
        return "下单成功!";
    }
}

注意:这里的 @GlobalTransactional 是关键注解,它标记这是一个需要跨服务协调的事务。


第六步:测试异常情况

我们故意在账户服务制造一个错误:

@GetMapping("/deduct-balance")
public void deductBalance(@RequestParam Long amount) {
    if (amount > 50) {
        throw new RuntimeException("金额过大");
    }
    // 正常逻辑:减余额
}

现在访问 /order/place?amount=100,你会看到所有前面的操作都被自动回滚了!

这就是 Seata 在背后做的工作:记录每一步变更,发生异常则撤销所有操作。


常见问题答疑(FAQ)

数据流转过程-1

🔍 问:分布式事务会不会影响性能?

答:会的。由于增加了日志记录和网络通信,分布式事务相比本地事务确实慢一些。但 Seata 的 AT 模式已经尽量减少影响,适合大部分中小规模场景。


🔍 问:我的事务没有生效怎么办?

常见原因:

  • 没有加上 @GlobalTransactional 注解
  • Seata 服务未启动
  • 数据库没有配置 undo_log 日志表(这是 Seata 自动管理事务的关键表)

解决方法:

  • 查看日志:Seata Client 会打印事务 ID 和状态
  • 使用 MySQL 工具查询 undo_log 表是否有记录

🔍 问:可以不用 Seata 吗?还有别的替代吗?

当然可以,比如:

  • 使用 RocketMQ + 补偿机制(最大努力通知)
  • 使用 TCC 模式自己写补偿逻辑(较复杂)
  • 使用数据库 XA 协议(强一致性但性能差)

如果你是初学者,建议优先掌握 Seata,因为它的社区活跃、文档完善、案例丰富。


学习建议:下一步怎么学?

恭喜你完成了第一个分布式事务实战!接下来你可以继续深入学习以下几个方向:

✅ 进阶方向一:理解 TCC 模式与补偿机制

  • 学习 TCC 的 Try、Confirm、Cancel 流程
  • 自己动手写一个 TCC 示例
  • 掌握幂等性和重试机制

✅ 进阶方向二:深入 Seata 的底层原理

  • 理解事务协调器的工作机制
  • 学习 AT 模式的 SQL 解析与反向 SQL 生成
  • 探索 Seata 如何实现事务隔离级别

✅ 进阶方向三:结合消息队列优化最终一致性

  • 使用 RabbitMQ 或 Kafka 发送事件通知
  • 实现基于消息的最大努力通知模式
  • 构建更加健壮的服务间通信机制

总结

本文带你了解了什么是分布式事务,为什么它重要,并且通过 Spring Boot + Seata 实现了一个完整的实战案例。

重点知识回顾:

概念 内容
什么是事务 多个操作要么全成功,要么全失败
什么是 Seata 国内流行的开源分布式事务中间件,支持 AT 模式
核心组件 TC(事务协调器)、TM(事务管理器)、RM(资源管理者)
实际应用 订单系统中库存、账户、订单三个服务的数据一致性保障
注意事项 日志表 undo_log 必须存在、Seata Server 必须启动、事务必须被正确注解

希望你能在这个基础上继续探索分布式系统的更多内容,比如服务注册发现、链路追踪、熔断降级等。

如有疑问,欢迎留言交流,我们一起成长进步 💪

评论 0

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