分布式事务解决方案:最佳实践(面向初学者的教程)

索引没建好
2025-06-20 02:43
阅读 462

开篇:什么是分布式事务?它用来做什么?

开篇:什么是分布式事务?它用来做什么?

在现代互联网开发中,我们的系统往往不是单一的服务,而是由多个“微服务”组成的。比如,一个电商系统的订单、支付、库存功能可能分别运行在不同的服务器上。

问题来了: 如果用户下单时,需要同时完成“减库存”和“扣款”,但这两个操作在两个服务里,其中一个失败了怎么办?你肯定不希望钱被扣了但没发货,或者发货了却没收钱!

这个时候,我们就需要用到 分布式事务 —— 它的作用就是让多个服务的多个数据库操作“要么一起成功,要么一起失败”。


环境准备:搭建你的第一个分布式项目环境

服务器部署方案-1

环境准备:搭建你的第一个分布式项目环境

所需工具:

  1. JDK 8 或以上
  2. Maven
  3. IntelliJ IDEA 或 Eclipse(推荐 IDEA)
  4. MySQL(至少两个数据库实例)
  5. Spring Boot + Dubbo + Seata(我们会用到这个开源中间件)

搭建步骤:

Step 1:安装 Java 和 Maven

下载安装好 JDK 并配置好环境变量(JAVA_HOME),然后验证命令:

java -version
mvn -v

Step 2:创建两个数据库

我们模拟两个微服务使用不同的数据库。

执行以下 SQL 创建两个数据库:

CREATE DATABASE order_db;
CREATE DATABASE stock_db;

再分别为它们创建一个表:

-- 订单表
USE order_db;
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_id INT,
    status VARCHAR(20)
);

-- 库存表
USE stock_db;
CREATE TABLE stocks (
    id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    count INT
);

Step 3:Spring Boot 初始化

访问 https://start.spring.io,选择如下依赖:

  • Spring Web
  • MyBatis Framework
  • MySQL Driver
  • Dubbo(可以搜索 Apache Dubbo
  • Lombok(可选)

下载并解压后导入 IDEA。

Step 4:集成 Seata

Seata 是一个开源的分布式事务中间件。我们需要添加它的依赖到 pom.xml 中:

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

下载 Seata Server(最新版):
https://github.com/seata/seata/releases

启动 Seata Server:

cd seata-server-1.6.1
sh bin/seata-server.sh -p 8091 -m db

确保配置文件里的数据源已指向自己的数据库。


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

我们在讲解前先搞清楚几个关键术语:

✅ 微服务架构(Microservice Architecture)

多个服务各自负责一件事,例如:

  • 用户服务:处理用户相关逻辑
  • 支付服务:处理付款逻辑
  • 库存服务:处理商品库存减少

这些服务相互独立部署,通过网络通信(如 REST API / Dubbo)互相协作。

❗ 分布式事务(Distributed Transaction)

当一个任务要跨多个微服务完成,并且所有服务的操作必须都成功或者都失败,就叫分布式事务。

例如:用户下了一个订单,系统要做两件事:

  1. 扣款 → 支付服务
  2. 减库存 → 商品服务

这两个操作要么全部执行成功,要么都不执行,否则会出乱子。

💡 本地事务 vs 分布式事务

类型 特点
本地事务 同一数据库内部,ACID 完全支持
分布式事务 跨库或服务,需要特殊机制保障一致性

📦 Seata 是什么?

Seata 是阿里巴巴开源的一个分布式事务中间件,用于解决跨服务数据的一致性问题。它提供了三种模式(稍后我们会讲到),其中最常用的是 AT 模式(Automatic Transaction)。


实战项目:实现一个简单的分布式下单流程

现在,我们来做一个项目:用户下单时自动扣款和减库存。我们将分三个服务:

  • 下单服务(order-service)—— 生成订单
  • 库存服务(stock-service)—— 减库存
  • 支付服务(payment-service)—— 扣款

但我们这里为了简化演示,只保留两个服务 + Seata 的 AT 模式进行事务控制。


第一步:创建模块

我们用一个 Spring Boot 项目作为主工程,分成以下几个模块:

springboot-dtx-seata/
├── order-service/    ← 下单服务
├── stock-service/    ← 库存服务
└── common-api/       ← 通用接口定义

每个模块都是一个独立的 Spring Boot 项目,并使用 Dubbo 进行远程调用。


第二步:编写 StockService

stock-service 中,我们写一个减库存的逻辑。

代码示例:

@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    public void decreaseStock(int productId, int quantity) {
        // 查看当前库存
        Stocks stock = stockMapper.selectById(productId);
        if (stock.getCount() < quantity) {
            throw new RuntimeException("库存不足");
        }
        // 减库存
        stock.setCount(stock.getCount() - quantity);
        stockMapper.updateById(stock);
    }
}

对应的 Mapper:

@Mapper
public interface StockMapper extends BaseMapper<Stocks> {
}

实体类:

@Data
public class Stocks {
    private Integer id;
    private Integer productId;
    private Integer count;
}

第三步:编写 OrderService

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

    @Reference
    private StockService stockService;  // Dubbo远程调用

    @Autowired
    private OrderMapper orderMapper;

    @PostMapping
    @GlobalTransactional // Seata注解
    public String createOrder(@RequestParam int userId, @RequestParam int productId) {
        try {
            // 1. 减库存
            stockService.decreaseStock(productId, 1);


![系统架构设计图-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062002/f1af87c8-e78f-4dae-9042-c2e35bcef451.jpg)


            // 2. 创建订单
            Orders order = new Orders();
            order.setUserId(userId);
            order.setProductId(productId);
            order.setStatus("created");
            orderMapper.insert(order);

            return "下单成功!";
        } catch (Exception e) {
            return "下单失败: " + e.getMessage();
        }
    }
}

注意:

  • @GlobalTransactional 是 Seata 提供的事务注解。
  • 当这个方法中抛异常时,Seata 会自动回滚整个事务。

第四步:测试一下是否能回滚

我们可以故意制造一次错误,比如把库存数量改为 0,再次调用下单接口。

POST /orders?userId=1&productId=1

如果返回“库存不足”的错误,你会发现:

  • 库存没有变化(因为已经回滚)
  • 订单也没插入数据库

✅ 成功!


常见问题:新手最容易遇到的问题及解决方案

❓Q1:调用服务时报错 “Dubbo service not found”

原因:

  • Dubbo 服务提供方未启动
  • 注册中心(Zookeeper/Nacos)未正常运行

解决办法:

  • 检查服务端是否已启动
  • 使用命令 telnet localhost 2181 看 Nacos/ZK 是否正常运行
  • 在配置中检查 dubbo.registry.address=zookeeper://... 是否正确

❓Q2:Seata 注解不起作用,没有回滚?

常见情况:

  • @GlobalTransactional 写在普通私有方法或非 controller 上
  • 数据库表没有 undo_log 表(这是 Seata 必须的)

解决办法:

  1. 保证 @GlobalTransactional 加在外部调用的方法上,比如 Controller 的入口函数。
  2. 在每个业务数据库中执行如下语句:
-- 创建undo_log表
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,
  `ext` VARCHAR(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

❓Q3:事务不生效,数据被修改了一部分?

可能性:

  • 没有使用 Seata 的 AT 模式,或未正确拦截 SQL
  • 异常被捕获并吞掉,导致事务没有回滚

建议:

  • 方法内尽量不要捕获异常,交由全局事务管理器处理
  • 使用 try-catch 包裹时重新抛出异常
try {
    someMethod();
} catch (Exception e) {
    throw new RuntimeException(e);
}

学习建议:下一步你可以学什么?

恭喜你完成了本节内容!接下来可以深入学习以下内容:

🔍 继续了解 Seata 其他模式:

模式 特点说明
AT模式 自动拦截SQL,开发者无感知(推荐)
TCC模式 需要手动定义 Confirm / Cancel 方法(更灵活)
Saga模式 适合长流程任务(如物流+支付+通知)

🧠 深入理解 CAP 理论 和 BASE 思想

  • CAP 理论解释了分布式系统中一致性和可用性的矛盾。
  • BASE 是一种弱一致性指导原则(基本可用、柔性状态、最终一致)

🚀 学习其他分布式事务框架:

  • Atomikos:本地 JTA 实现
  • LCN:国内老框架,已被淘汰(仅供参考)
  • Nacos + RocketMQ Dledger + Seata 构建完整生态链

结语:坚持练习,你就离大厂不远啦!

分布式事务是微服务开发中最核心的难点之一。这篇文章带你一步步从零开始搭建一个支持分布式事务的小项目。虽然过程有点复杂,但只要跟着步骤一步步走,你一定能掌握!

🎯 小提醒:学习过程中一定要动手写代码,不要光看文档。有问题随时查看官方文档或搜索社区文章,也可以留言提问哦!


如果你觉得这篇文章对你有帮助,请点赞转发让更多小伙伴看到,我们一起进步!🚀

评论 0

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