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

Redis看门狗
2025-06-24 05:49
阅读 766

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

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

你有没有遇到过这样的问题?比如你在淘宝买东西,钱已经扣了,但系统却告诉你“下单失败”;或者你在银行转账时,转出成功但到账失败。这些问题的背后其实都涉及到一个叫做分布式事务的问题。

那么,什么是分布式事务呢?

简单来说:

分布式事务 = 多个系统一起完成一件事 + 这件事要么全做,要么全不做

比如说,你在网上订机票加酒店,这两个服务是由不同的系统负责的。如果其中一个成功、另一个失败,就可能导致数据不一致(比如酒店定了但飞机票没订到)。这时候我们就需要一种机制来保证:这两个操作要么同时成功,要么都不发生。

分布式事务就是用来解决这个问题的关键技术。


环境准备:你需要哪些工具和环境

环境准备:你需要哪些工具和环境

为了动手练习分布式事务的基础知识,我们使用以下技术栈(都是目前比较主流的):

  • 操作系统:Windows / Linux / Mac OS(任意)
  • Java 8+
  • Spring Boot 2.x+
  • MySQL 数据库
  • RocketMQ(消息队列)
  • Lombok(简化代码)
  • Maven(项目构建)

步骤一:安装 Java 和 Maven

  1. 下载 JDK(Java 11 推荐)
    官网地址:https://www.oracle.com/java/technologies/javase-jdk11-downloads.html
  2. 安装后在终端输入命令:
    java -version
    mvn -v
    
    如果能看到版本号说明安装成功。

步骤二:安装 MySQL

下载并安装 MySQL 8.x
安装完成后创建两个数据库用于我们的测试:

CREATE DATABASE bank_a;
CREATE DATABASE bank_b;

步骤三:安装 RocketMQ(消息队列)

下载地址:https://rocketmq.apache.org/

启动 RocketMQ 基本命令(详细步骤参考官方文档):

cd rocketmq-all-4.9.4-bin-release
start mqnamesrv
start mqbroker -n localhost:9876

确保服务能正常运行即可继续下一步。

步骤四:IDE 工具推荐(任选其一)

  • IntelliJ IDEA(推荐)
  • Eclipse + Lombok 插件

建议新手使用 IDEA 社区版,它对 Spring Boot 支持很好,并且有图形界面方便调试。


核心概念:搞懂这些词你就入门了

核心概念:搞懂这些词你就入门了

要理解分布式事务,首先得了解几个关键术语:

1. 事务(Transaction)

事务是数据库中的一个经典概念,它指的是一组操作,这组操作要么都成功,要么都失败。事务具备四个特性,统称 ACID:

  • A(原子性):整个事务中的所有操作是一个整体,不能拆开。
  • C(一致性):事务执行前后,数据库状态保持一致。
  • I(隔离性):多个事务之间互不干扰。
  • D(持久性):事务一旦提交,结果就会永久保存。

2. 分布式事务

当你的业务操作跨越多个系统或数据库时,就涉及到了分布式事务。

例如:

  • A 系统从账号 a 转 50 元到账号 b(本地事务)
  • B 系统把这次转账记录同步到日志服务器(另一个系统)
  • 要求两个操作一起成功或一起失败 —— 这就是典型的分布式事务场景。

3. CAP 定理

CAP 是分布式系统的三大核心约束:

  • C(一致性)
  • A(可用性)
  • P(分区容忍性)

CAP 定理指出:一个系统只能同时满足其中两项,另一项必须牺牲。

我们在设计分布式系统的时候会根据实际需求进行权衡。

4. 最终一致性 vs 强一致性

  • 强一致性:所有节点在同一时间看到相同的数据。
  • 最终一致性:允许数据暂时不同步,但最后会统一。

大多数互联网系统用的是“最终一致性”。


实战项目:用 RocketMQ 实现一个转账系统

现在我们来做一个最简单的案例:模拟跨数据库的转账操作,使用最终一致性 + RocketMQ的方式来实现分布式事务。

项目目标

我们要实现如下功能:

  • 用户从 Bank A 的账户转 100 元到 Bank B 的账户。
  • 两个账户分别属于两个数据库。
  • 如果转账失败,两个账户的钱都不会变化。

步骤一:创建数据库表结构

进入 bank_a 数据库,创建用户表:

USE bank_a;

CREATE TABLE user_account (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

INSERT INTO user_account (name, balance) VALUES ('张三', 500);

进入 bank_b 数据库,创建同样的表:

USE bank_b;

CREATE TABLE user_account (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

INSERT INTO user_account (name, balance) VALUES ('李四', 300);

步骤二:Spring Boot 项目搭建

用 IDEA 创建 Spring Boot 项目,添加以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

步骤三:配置多数据源

application.yml 中添加两个数据库连接信息:

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/bank_a?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql://localhost:3306/bank_b?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

注意:这里用的是 dynamic-datasource-spring-boot-starter 这个第三方组件,记得加入对应的 starter。

步骤四:定义基本 Service 层

BankAService.java

@Service
@DS("master")
public class BankAService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deductMoney(String name, double amount) {
        String sql = "UPDATE user_account SET balance = balance - ? WHERE name = ?";
        jdbcTemplate.update(sql, amount, name);
    }
}

BankBService.java

@Service
@DS("slave")
public class BankBService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void addMoney(String name, double amount) {
        String sql = "UPDATE user_account SET balance = balance + ? WHERE name = ?";
        jdbcTemplate.update(sql, amount, name);
    }
}

TransferService.java

@Service
public class TransferService {

    @Autowired
    private BankAService bankAService;

    @Autowired
    private BankBService bankBService;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void transfer(String from, String to, double amount) {
        try {
            // 第一步:A 减钱
            bankAService.deductMoney(from, amount);

            // 第二步:发送 RocketMQ 消息通知 B 加钱
            Message<String> message = MessageBuilder.withPayload("ADD:" + to + ":" + amount).build();
            rocketMQTemplate.send(message, new CorrelationIdCallback() {
                @Override
                public String getCorrelationId(Message<?> message) {
                    return UUID.randomUUID().toString();
                }
            });


![微服务架构示意图-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062405/ca27c70a-3973-421e-9872-348d7c941e70.jpg)


        } catch (Exception e) {
            System.out.println("转账失败,回滚未处理");
        }
    }
}

这只是一个简单示例,真正的补偿逻辑、消费端处理等内容我们下面补充。

步骤五:RocketMQ 消费端逻辑(BankB 模块)

在消费者一方编写监听器:

@Component
@RocketMQMessageListener(topic = "TRANSFER_TOPIC", consumerGroup = "transfer-group")
public class BankBConsumer implements RocketMQListener<String> {

    @Autowired
    private BankBService bankBService;

    @Override
    public void onMessage(String msg) {
        if (msg.startsWith("ADD:")) {
            String[] parts = msg.split(":");
            String name = parts[1];
            double amount = Double.parseDouble(parts[2]);

            bankBService.addMoney(name, amount);
        }
    }
}

测试:运行整个流程

你可以通过 Controller 或者 Postman 发起一个转账请求:

@RestController
public class TransferController {

    @Autowired
    private TransferService transferService;

    @GetMapping("/transfer")
    public String doTransfer() {
        transferService.transfer("张三", "李四", 100);
        return "转账开始";
    }
}

访问:http://localhost:8080/transfer

可以看到:

  • 张三的余额减少了 100 元
  • 李四的余额增加了 100 元

这样,我们就完成了基于 RocketMQ 的“半消息+本地事务”的一个典型应用场景!


常见问题:为什么我的事务有时候失败了数据还变了?

以下是新手最容易遇到的几个问题,以及它们的解答:

✅ 问题 1:两个操作不是同时成功的怎么办?

回答:我们这里是“异步补偿”的方式,即先减钱再发消息,然后由消息队列去触发加钱。如果消息丢失或失败,我们需要额外加“事务日志”和“定时重试”机制才能保障最终一致性。

✅ 问题 2:事务中间失败,如何回滚?

回答:传统数据库事务可以自动回滚,但分布式事务中没有单一事务管理器。你需要自己手动写“补偿”逻辑,比如调用另一个接口来回退操作(如退款、取消订单等)。

✅ 问题 3:RocketMQ 没收到消息怎么办?

回答:可以开启 RocketMQ 的事务消息机制,或者采用延迟重试策略,比如每隔一段时间尝试重新投递。


学习建议:进阶路线图

恭喜你完成了这个项目的演练!接下来,我建议你按以下路径继续学习:

  1. 掌握 Seata 的使用
    阿里开源的分布式事务框架 Seata 支持 AT 模式、TCC 模式等,适合企业级项目。

  2. 了解 SAGA 模式与 TCC 模式对比
    不同业务场景选择不同模式,比如金融交易更适合 TCC,而订单处理更常用 SAGA。

  3. 深入 RocketMQ 事务消息机制
    了解 RocketMQ 提供的“本地事务 + 回查”机制,实现更安全的异步通知。

  4. 实战微服务项目整合
    尝试用 Spring Cloud Alibaba + Seata 构建一个完整的微服务架构下的分布式事务系统。

  5. 性能优化与故障排查技巧
    包括幂等性设计、重复消费、死信队列处理等内容。


总结

在这篇教程中,我们从零基础出发,一步步讲解了:

  • 分布式事务的基本概念
  • 所需的开发环境搭建
  • 使用 RocketMQ 实现一个简单的转账系统
  • 初学者常见的疑问及解答
  • 后续学习的方向建议

虽然分布式事务听起来很复杂,但它本质上就是在多个服务间协调数据的一致性。只要掌握了原理和常用方案,就可以应对大部分业务场景的需求。

如果你喜欢这篇教程,欢迎收藏并分享给其他初学的朋友!也欢迎在评论区留言,提出你的问题或想法 😊

评论 0

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