分布式事务解决方案:最佳实践(新手友好版)

萧勇_移动端
2025-06-15 15:37
阅读 310

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

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

在我们学习写程序的时候,最开始都是写一个简单的“小卖部系统”,比如下单、减库存这些操作都在同一个数据库里。这个时候,只要使用一个BEGIN TRANSACTION就能保证数据不出错。

但现实中的项目往往没那么简单。很多大型系统是由多个服务组成的,比如下单是一个服务,支付是另一个服务,库存管理又是另一个服务。它们可能运行在不同的服务器上,连接着不同的数据库。
这时候问题就来了:

如果用户下单成功了,但支付失败了,那库存是不是要回退呢?

这就引出了我们要讲的主题:分布式事务 —— 一种让我们能在多个服务、多个数据库之间保持数据一致性的机制。


环境准备:搭建你的第一个微服务环境

环境准备:搭建你的第一个微服务环境

数据流转过程-2

本教程将使用Java + Spring Boot作为开发语言,并配合Seata来处理分布式事务。

第一步:安装开发工具

你需要安装以下工具:

  • JDK 1.8 或以上
  • Maven 3.x
  • IntelliJ IDEA(推荐)
  • MySQL 5.7+ (也可以用Docker快速启动)

✅ 新手建议:如果你对命令行不熟悉,可以下载MySQL WorkbenchIntelliJ IDEA Community Edition 来辅助开发。

第二步:安装 Seata Server(这是我们的分布式事务协调器)

Seata 是阿里巴巴开源的一个分布式事务中间件,非常适合作为入门工具。

安装步骤如下:

  1. 前往官网下载:https://seata.io 下载最新版本(例如 seata-server-1.6.1.zip)。
  2. 解压文件到本地目录,进入 conf 目录,修改配置文件:
    • 修改 registry.conf 文件,设置注册中心为 file(即本地文件方式),方便初学者调试。
registry {
  type = "file"
}
  1. 启动 Seata Server:
# Windows下双击 startup.bat 即可
# Linux/Mac 下执行:
sh seata-server.sh -p 8091 -m file
  1. 成功后你会看到日志输出中有 Server started 字样,说明Seata已经准备好工作了。

核心概念:轻松理解分布式事务的几个关键词

核心概念:轻松理解分布式事务的几个关键词

1. 什么是本地事务?

在单个数据库中,我们熟悉的事务就是本地事务。它满足 ACID 特性(原子性、一致性、隔离性、持久性)。例如:

@Transactional
public void transferMoney() {
    jdbcTemplate.update("UPDATE account SET balance = balance - 100 WHERE name = '张三'");
    jdbcTemplate.update("UPDATE account SET balance = balance + 100 WHERE name = '李四'");
}

数据库设计模型-1

这段代码要么全部执行成功,要么都不执行,不会出现只转账了一个人的情况。

2. 什么是全局事务?

当我们在多个服务之间进行操作时(如下单、付款、减库存),我们就需要一个“统一指挥”的角色,告诉各个服务要不要提交或回滚,这个“指挥官”就是 Seata 所提供的 全局事务协调者

你可以想象它是这样工作的:

  • 下单服务先登记自己参与了事务
  • 支付服务也登记自己参与了事务
  • 如果一切顺利,协调者说:“都提交”
  • 如果某一步出错,协调者说:“大家都回滚”

3. 几种常见的分布式事务模式(简单对比)

名称 是否自动回滚 使用场景 是否适合新手
XA 数据库支持严格一致性 ❌复杂
TCC 可控的服务逻辑 ✅较适合
Saga 长流程业务 ⚠️有一定难度
AT (Auto Transaction) 对数据库透明操作 ✅推荐

今天我们主要讲解 AT 模式,因为它对开发者最友好,只需加注解即可。


实战项目:写一个下单+扣库存的例子

我们将创建两个服务:

  • order-service(下单服务)
  • inventory-service(库存服务)

这两个服务通过 Restful 接口调用彼此,并且使用 Seata 保证一致性。

第一步:创建数据库和表

在MySQL中分别创建两个库:

CREATE DATABASE order_db;
CREATE DATABASE inventory_db;

USE order_db;
CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  product_id BIGINT NOT NULL,
  user_id BIGINT NOT NULL
);

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

插入一条测试数据:

INSERT INTO inventory (product_id, stock) VALUES (1, 100);

第二步:编写项目结构(Spring Boot)

创建两个Maven模块:

project-root/
├── order-service/
│   └── pom.xml
│   └── src/main/java/...
└── inventory-service/
    └── pom.xml
    └── src/main/java/...

公共依赖(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: my_tx_group

Seata会根据 tx-service-group 自动去连接你刚才启动的 Server。


第四步:编写 OrderService

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

    @Autowired
    private OrderService orderService;

    @GetMapping("/create")
    public String createOrder(@RequestParam Long productId) {
        orderService.createOrder(productId);
        return "订单创建完成!";
    }
}

实现类:

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @GlobalTransactional // 关键:开启全局事务
    @Override
    public void createOrder(Long productId) {
        // 1. 插入订单
        orderMapper.insertOrder(productId, 1L); // 假设用户id是1

        // 2. 调用库存服务
        String url = "http://localhost:8081/inventory/deduct?productId=" + productId;
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

        if (!"SUCCESS".equals(response.getBody())) {
            throw new RuntimeException("库存不足或系统异常");
        }
    }
}

📌 @GlobalTransactional 就是我们引入 Seata 的关键注解!


第五步:编写 InventoryService

接口:

@GetMapping("/deduct")
public String deductStock(@RequestParam Long productId) {
    boolean success = inventoryService.deduct(productId);
    return success ? "SUCCESS" : "FAIL";
}

实现类:

@Service
public class InventoryServiceImpl implements InventoryService {

    @Autowired
    private InventoryMapper inventoryMapper;

    @Transactional
    @Override
    public boolean deduct(Long productId) {
        Integer affected = inventoryMapper.deductStock(productId);
        return affected > 0;
    }
}

SQL(mapper.xml):

<update id="deductStock">
    UPDATE inventory SET stock = stock - 1 WHERE product_id = #{productId} AND stock > 0
</update>

第六步:运行项目测试

  1. 启动两个服务,端口号分别是8080(order)、8081(inventory)。
  2. 启动Seata Server。
  3. 浏览器访问:http://localhost:8080/order/create?productId=1
  4. 成功则看到“订单创建完成!”
  5. 故意改错代码模拟异常,比如让库存服务抛出异常,检查是否回滚。

常见问题解答

Q1:Seata无法连接,怎么办?

确保你本地有启动Seata Server,并检查 application.yml 中的配置是否正确。


Q2:@GlobalTransactional 注解没有生效?

确保你已添加 Seata Starter 依赖,并且启动类加上了 @EnableTransactionManagement


Q3:事务跨服务为什么还能回滚?

因为 Seata 的 AT 模式会在你每个数据库操作前后记录“快照”,一旦失败,Seata 会帮你把这些快照“还原”。


Q4:TCC 和 AT 有什么区别?

  • TCC 需要你自己定义 try-confirm-cancel 三个阶段,控制更精细,但也更复杂。
  • AT 则由框架自动记录操作前后的状态,适合大多数 CRUD 场景,更适合刚入门的同学。

学习建议:下一步该学什么?

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

接下来你可以尝试以下几个方向:

方向一:深入理解 Seata 的底层原理

  • 了解 Seata 的 TC、TM、RM 架构
  • 理解 AT 模式的 undo log 工作原理

方向二:尝试其他事务模式(如 TCC)

  • 学习如何编写 Try 方法、Confirm 方法、Cancel 方法
  • 更好地控制复杂的业务流程

方向三:集成 Nacos 或 Eureka 作为服务发现

这会更加贴近企业级架构,让你更好地理解服务间通信与协调。


总结

这篇文章从零开始带你认识了什么是分布式事务,学会了如何搭建 Seata 环境,并动手完成了第一个“下单+扣库存”项目。整个过程强调“看得懂、做得出来、能跑通”。

分布式事务并不是遥不可及的技术难点,只要掌握了核心思想和基础工具(如 Seata),就可以一步步构建稳定可靠的企业级应用系统。

继续加油,未来的架构师!🚀


如果你觉得这篇教程对你有帮助,欢迎点赞/收藏/分享让更多人看到!

评论 0

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