分布式事务解决方案:最佳实践

独立开发练习生
2025-06-13 01:25
阅读 265

开篇:分布式事务是做什么的?

开篇:分布式事务是做什么的?

在传统的单体架构中,数据库事务非常好处理。比如一个银行转账操作:“从A账户减钱”和“给B账户加钱”可以放在同一个事务里,要么一起成功,要么一起失败。

但是在现代系统中,尤其是微服务架构中,业务被拆分到多个服务里,例如:

  • 用户服务负责用户信息
  • 订单服务负责下单
  • 库存服务负责扣库存
  • 支付服务负责支付

这些服务各自使用不同的数据库,那么如果出现一个操作需要跨服务执行(比如下单时同时扣库存、生成订单),就无法再通过单个事务来保证数据一致性了。

这就是我们今天要讲的主题——分布式事务

一句话总结:当多个服务或数据库参与一个操作的时候,如何让它们像一个整体一样工作?这就要用到分布式事务解决方案


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

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

我们要写的是一个最简单的电商项目示例:用户下单 + 扣库存,涉及到两个服务:

  • order-service 负责创建订单
  • inventory-service 负责减少库存

技术栈(简单易上手):

  • Java 17
  • Spring Boot 3.x
  • MySQL 8
  • Lombok
  • Seata(开源的分布式事务框架)

步骤一:安装JDK & Maven

请自行下载安装好 JDK 17 和 Maven,并配置好环境变量。

步骤二:安装MySQL并建库

创建两个数据库分别模拟两个服务的数据存储:

-- 创建 order 数据库
CREATE DATABASE order_db;

-- 创建 inventory 数据库
CREATE DATABASE inventory_db;

然后在这两个数据库下建立对应的表:

order_db.order_table:

CREATE TABLE order_table (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    count INT NOT NULL
);

inventory_db.inventory_table:

CREATE TABLE inventory_table (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT UNIQUE NOT NULL,
    stock INT NOT NULL DEFAULT 0
);

插入一条测试商品:

INSERT INTO inventory_db.inventory_table (product_id, stock) VALUES (1001, 10);

步骤三:启动 Seata Server

Seata 是实现分布式事务的一个开源中间件,它作为协调者帮助我们管理全局事务。

安装步骤简述:

  1. 前往 Seata GitHub 下载最新 release 包。
  2. 解压后进入目录:
    cd seata-server-xx.x.x
    
  3. 启动 TC(Transaction Coordinator):
    sh seata-server.sh -p 8091 -m db
    

默认使用的是 file 模式,我们也可以改成数据库模式以便持久化保存事务日志。


核心概念讲解

下面是一些你需要了解的关键术语:

概念名称 中文解释 简单理解
XA协议 一种经典分布式事务协议 多个数据库支持它,但性能差
TCC Try-Confirm-Cancel 先预占资源,再确认或回滚
Saga 补偿型事务 一步一步执行,失败则反向补偿
SAGA与TCC的区别 实现方式不同 TCC强调预占,Saga强调补偿
Seata 阿里开源的分布式事务框架 提供一站式解决方案

我们选用Seata + AT模式进行实战演示:

AT模式优势:对业务无侵入,兼容性好,适合刚接触分布式事务的新手。


实战项目:写一个分布式事务案例

我们现在要做一个功能:用户下单的时候自动扣除对应商品库存,整个过程必须具备事务能力。

项目结构概览

我们将构建两个 Spring Boot 微服务:

  • order-service
  • inventory-service

每个服务都有独立的数据库。

第一步:添加 Seata 的依赖(以 Maven 为例)

在两个项目的 pom.xml 中加入:

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

第二步:配置 Seata 客户端

在每个微服务的 application.yml 文件中添加如下配置:

seata:
  enabled: true
  application-id: order-service # 或 inventory-service
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: file
  registry:
    type: file

确保两个服务都能找到 Seata Server。

第三步:编写下单接口

在 OrderService 中调用 InventoryFeignClient 来扣库存:

// OrderController.java
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/place-order")
    public String placeOrder(@RequestBody OrderDTO dto) {
        try {
            orderService.createOrder(dto);
            return "下单成功!";
        } catch (Exception e) {
            return "下单失败:" + e.getMessage();
        }
    }
}

实现核心逻辑(伪代码):

@Transactional
public void createOrder(OrderDTO dto) throws Exception {
    // 1. 创建订单记录
    jdbcTemplate.update("INSERT INTO order_table(user_id, product_id, count) VALUES(?,?,?)",
        dto.getUserId(), dto.getProductId(), dto.getCount());

    // 2. 调用库存服务,扣库存
    inventoryFeignClient.decreaseStock(dto.getProductId(), dto.getCount());
}

第四步:在库存服务中定义扣库存方法

@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {
    @PostMapping("/decrease-stock")
    void decreaseStock(@RequestParam Long productId, @RequestParam Integer count);
}

@RestController
public class InventoryController {

    @Transactional
    @PostMapping("/decrease-stock")
    public void decreaseStock(@RequestParam Long productId, @RequestParam Integer count) {
        int currentStock = getCurrentStock(productId);
        if (currentStock < count) {
            throw new RuntimeException("库存不足!");
        }
        // 更新库存
        jdbcTemplate.update("UPDATE inventory_table SET stock = stock - ? WHERE product_id = ?",
            count, productId);
    }
}

第五步:测试异常场景

你可以故意在某个服务中抛出异常,观察是否能回滚。

比如修改 InventoryController 中的代码:

throw new RuntimeException("人为制造错误");

再次调用 /place-order 接口,观察订单有没有被创建、库存有没有被减少。

如果没有变化,说明 Seata 正确地进行了事务回滚!


新手常见问题解答

Q1:为什么Seata启动不起来?

  • 可能没有开放端口;
  • 可能配置文件路径不对;
  • 使用数据库模式的话,需要额外配置数据库连接信息。

Q2:我在一个服务中开启@Transactional,另一个服务报错会回滚吗?

✅ 如果你使用了 Seata 并且正确配置,是可以的。

❌ 如果你没有集成任何分布式事务框架,这是做不到的。

Q3:除了Seata还有其他替代方案吗?

当然有:

  • Atomikos(两阶段提交实现)
  • Nacos + RocketMQ 实现异步事务消息
  • 自研 TCC 框架等

但对于新手来说,Seata是最好的入门选择之一


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

✅ 初学完成推荐路线如下:

  1. 理解 Seata 支持的其他模式(如TCC、SAGA)及其适用场景
  2. 动手实现一个基于事件驱动+本地事务表的“最终一致性”方案
  3. 学习 Kafka 或 RocketMQ 的事务消息机制
  4. 掌握 CAP 原则,学会根据业务场景选型

📚 推荐资料:


🎉 恭喜你完成了你的第一个分布式事务实战项目!

如果你已经顺利跑通这个 Demo,并能理解背后的设计思想,那你就已经站在了一个很高的起点上了!

继续加油,后端开发的世界非常精彩!🚀

评论 0

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