你知道什么必须用到分布式事务吗?【从理论到实战】

事务需实现ACID,常采用分阶段提交方式。一阶段提交协议简单,性能好,但处理多数据源有局限。二阶段提交协议增加管理者角色,可协调多数据源,分准备和执行两阶段,能降低操作失败可能性,但存在阻塞弊端。

目录

第一章 分布式事务概述

1.1 什么是分布式事务

1.2 为什么需要分布式事务

1.3 分布式事务的挑战

1.4 基本概念和术语

第二章 理论基础

2.1 ACID特性回顾

2.2 CAP定理

2.3 BASE理论

2.4 柔性事务与刚性事务

第三章 分布式事务解决方案

3.1 两阶段提交2PC

3.2 三阶段提交3PC

3.3 TCC补偿型事务

3.4 本地消息表

3.5 可靠消息最终一致性

3.6 最大努力通知

3.7 Seata框架介绍

第四章 实战项目准备

4.1 项目场景描述

4.2 技术栈选型

4.3 环境搭建

4.4 数据库设计

第五章 代码实战

5.1 问题场景演示

5.2 Seata AT模式实现

5.3 TCC模式实现

5.4 消息队列实现可靠消息

第六章 最佳实践与总结

6.1 方案选型建议

6.2 常见问题与解决方案

6.3 性能优化技巧

6.4 总结与展望


第一章 分布式事务概述

1.1 什么是分布式事务

在正式进入分布式事务的世界之前,我们先从一个日常生活中的例子开始。

想象一下你在网上购买了一部手机。下单的过程是这样的:点击“立即购买”按钮后,系统需要创建订单、扣减商品库存、处理支付、通知物流发货。在传统的单体应用中,这些操作都在同一个数据库中进行,使用数据库自带的本地事务就能轻松保证所有操作要么全部成功,要么全部失败。

然而,在现代的微服务架构下,情况变得复杂了。订单、库存、支付、物流可能分属不同的微服务,每个服务都有自己的数据库。当你点击“购买”时,实际上是在协调多个独立的系统共同完成一项任务。这种在分布式系统环境下,由多个服务通过网络通信协作完成的事务,就称为分布式事务

简单来说,分布式事务可以理解为一组操作的集合,这些操作可能分布在不同的服务、不同的数据库、甚至不同的物理服务器上,但它们需要作为一个不可分割的逻辑单元来执行。分布式事务的目的就是保证这些分散的操作能够保持一致性和完整性。

1.2 为什么需要分布式事务

随着业务的发展和技术的演进,系统架构经历了从单体到分布式的转变,这也带来了对分布式事务的需求。

业务复杂化的驱动

以电商平台为例,一个订单的完成涉及多个业务模块的协作。每个模块都可能需要更新自己的数据:订单服务要生成订单记录,库存服务要扣减库存,积分服务要增加用户积分,优惠券服务要标记优惠券已使用。如果这些操作没有统一的事务管理,就可能出现订单生成但库存未扣减的超卖问题,或者积分已加但订单未生成的脏数据问题。

数据一致性的刚性需求

在某些业务场景中,数据的一致性要求极为严格。比如银行转账,A账户扣款成功但B账户未到账,这种情况是绝对不能接受的。金融系统要求强一致性,分布式事务就是保障这种一致性的技术手段。

微服务架构的自然产物

微服务架构强调“每个服务拥有自己的数据”,这意味着原本在单一数据库中通过本地事务就能解决的跨表操作,现在变成了跨服务的操作。分布式事务成为了微服务架构中不可或缺的基础设施。

1.3 分布式事务的挑战

分布式事务之所以复杂,是因为它面临着传统单机事务不曾遇到的困境。

网络的不可靠性

网络是分布式系统的基石,也是最不稳定的因素。网络延迟、消息丢失、消息乱序、网络分区等问题时有发生。某物流平台的实测数据显示,高峰期消息延迟率高达3%,重复消费率达到0.5%。在不可靠的网络环境中保证事务的一致性,是分布式事务面临的首要挑战。

节点异构性

分布式系统中的节点往往采用不同的技术栈。支付系统可能使用Oracle数据库,订单系统使用MySQL,积分系统使用Redis。不同技术栈的事务机制各不相同,如何协调这些异构节点成为一个难题。

性能与一致性的权衡

根据CAP定理,分布式系统无法同时满足一致性、可用性和分区容错性。这意味着在分布式事务设计中,往往需要在一致性和性能之间做出权衡。追求强一致性通常会带来性能损耗,而追求高性能则可能需要接受最终一致性。

故障处理的复杂性

分布式环境下,任何节点都可能随时出现故障。协调者宕机怎么办?参与者宕机怎么办?网络中断怎么办?这些故障场景都需要有完善的应对机制,保证系统能够在故障发生时保持数据一致。

1.4 基本概念和术语

在深入学习之前,我们先了解一些关键术语:

  • 事务参与者:参与分布式事务的各个服务或资源,比如订单服务、库存服务。

  • 事务协调者:负责协调和管理分布式事务的中心节点,决定事务的提交或回滚。

  • 全局事务:由多个分支事务组成的完整事务单元。

  • 分支事务:参与者内部执行的本地事务,是全局事务的一部分。

  • 提交:确认事务中的所有操作生效。

  • 回滚:撤销事务中已执行的操作。

  • 补偿:对已提交的操作执行逆向操作,恢复到之前的状态。

  • 幂等性:同一个操作执行多次和执行一次的效果相同,这是分布式事务设计中的重要原则。

第二章 理论基础

2.1 ACID特性回顾

在理解分布式事务之前,有必要回顾一下传统数据库的ACID特性,这是事务的黄金标准。

原子性:事务中的所有操作被视为一个不可分割的单元,要么全部成功,要么全部失败。如果事务执行过程中发生错误,所有已执行的操作都会被回滚。

一致性:事务执行前后,数据库从一个一致的状态转换到另一个一致的状态。事务不能破坏数据的完整性约束,比如外键约束、唯一性约束等。

隔离性:多个事务并发执行时,彼此之间互不干扰。一个事务的中间状态对其他事务是不可见的。隔离性通过锁机制或多版本并发控制来实现。

持久性:一旦事务提交,其对数据库的修改就是永久性的,即使系统发生故障也不会丢失。

在单机数据库中,ACID特性通过数据库引擎自身的事务机制来保证。但在分布式环境中,由于数据分布在多个节点上,维持ACID特性变得异常困难。

2.2 CAP定理

CAP定理是分布式系统的基石理论,由计算机科学家Eric Brewer提出。该定理指出,分布式系统最多只能同时满足以下三个特性中的两个:

一致性:所有节点在同一时刻看到相同的数据。也就是说,对系统的一个写操作完成后,后续的所有读操作都应该读到这个新值。

可用性:每个请求都能得到响应,但不保证响应中包含最新的数据。系统始终对外提供服务,不会因为部分节点故障而导致整个系统不可用。

分区容错性:系统能够容忍网络分区(节点之间无法通信)的情况,在网络故障时仍能继续工作。

CAP定理的核心结论是:在分布式系统中,P(分区容错性)是必须满足的,因为网络分区是客观存在的现实。因此,系统实际上需要在C(一致性)和A(可用性)之间做出选择:

  • CP系统:放弃可用性,保证一致性。当网络分区发生时,系统会暂停服务,直到数据一致。Zookeeper是典型的CP系统,在Leader选举期间,集群不可用。

  • AP系统:放弃强一致性,保证可用性。系统始终对外服务,但允许数据出现短暂的不一致,最终达到一致。Eureka、Nacos(默认配置)是典型的AP系统。

不同业务场景对CAP的权衡不同。金融系统通常选择CP,因为数据一致性比可用性更重要;而社交媒体可能选择AP,因为短暂的点赞数不一致可以接受,但服务不能停。

2.3 BASE理论

BASE理论是对CAP定理中一致性和可用性权衡的延伸,它描述了对最终一致性系统的期望。

基本可用:系统出现故障时,允许损失部分可用性。比如响应时间变长,或者部分功能不可用,但核心功能保持正常。

软状态:允许系统存在中间状态,且这种中间状态不影响系统的整体可用性。比如订单状态为“处理中”,这种状态是临时的,最终会变成“成功”或“失败”。

最终一致性:系统不保证数据时刻一致,但经过一段时间后,数据最终会达到一致状态。这个“一段时间”可以是毫秒级,也可以是小时级,取决于业务要求。

BASE理论大大降低了分布式系统设计的复杂度,它告诉我们:不一定要追求强一致性,最终一致性在很多场景下已经足够。

2.4 柔性事务与刚性事务

基于CAP定理和BASE理论,分布式事务可以分为两类:

刚性事务:追求强一致性,遵循ACID原则。这类事务通常使用两阶段提交等协议实现,保证数据在任何时刻都是一致的。刚性事务的代价是性能较低,适合金融、支付等对一致性要求严苛的场景。

柔性事务:追求最终一致性,遵循BASE原则。这类事务允许数据在短时间内不一致,但保证最终会一致。柔性事务的性能较好,适合电商、社交等对并发要求高、可以容忍短暂不一致的场景。

在实际应用中,柔性事务因其更好的性能和可扩展性,得到了更广泛的应用。

第三章 分布式事务解决方案

3.1 两阶段提交2PC

两阶段提交是最经典的分布式事务协议,它将事务的提交过程分为准备阶段和提交阶段。

第一阶段:准备阶段

事务协调者向所有参与者发送Prepare请求,询问是否可以执行事务。每个参与者收到请求后,执行本地事务操作,但不提交,并将执行结果(成功或失败)返回给协调者。参与者会在本地记录事务日志,锁定相关资源。

第二阶段:提交/回滚阶段

协调者收集所有参与者的响应:

  • 如果所有参与者都返回成功,协调者向所有参与者发送Commit请求,参与者收到后正式提交本地事务。

  • 如果任一参与者返回失败,或者超时未响应,协调者向所有参与者发送Abort请求,参与者收到后回滚本地事务。

2PC的优点是实现相对简单,能够提供强一致性保证。但其缺点也很明显:

  • 同步阻塞:参与者从准备阶段到提交完成期间,一直持有资源锁,这会严重影响系统并发能力。

  • 单点故障:协调者是单点,如果协调者在发送Commit之前宕机,所有参与者会一直阻塞。

  • 脑裂风险:在协调者发送Commit后、部分参与者收到前发生网络故障,会导致部分参与者提交而部分未提交的数据不一致。

2PC适用于并发量不高、对一致性要求极高的金融类场景。

3.2 三阶段提交3PC

三阶段提交是对2PC的改进,在准备阶段之前增加了一个询问阶段,并引入了超时机制。

第一阶段:CanCommit

协调者询问所有参与者是否可以执行事务。参与者根据自身状态返回Yes或No。这个阶段不执行任何实际操作。

第二阶段:PreCommit

如果所有参与者都返回Yes,协调者发送PreCommit请求。参与者收到后执行事务操作但不提交,返回响应。如果有参与者返回No或超时,协调者发送Abort请求。

第三阶段:DoCommit

协调者根据PreCommit阶段的响应决定提交或回滚。与2PC不同的是,3PC中参与者在超时后会自动提交(假设协调者大概率会发送Commit),这解决了协调者单点故障导致的阻塞问题。

3PC虽然减少了阻塞时间,但仍无法完全保证一致性,且实现复杂度更高。实际应用中,3PC的使用远不如2PC普遍。

3.3 TCC补偿型事务

TCC是一种补偿型事务模式,它将业务操作拆分为三个步骤:Try、Confirm和Cancel。相比2PC,TCC通过业务层面的控制代替了数据库层面的锁,性能更好。

Try阶段

尝试执行业务,完成所有业务检查,并预留必要的业务资源。例如,在扣款场景中,Try阶段会检查账户余额是否充足,并冻结相应金额。

Confirm阶段

确认执行业务,真正提交变更。Confirm操作必须幂等,因为可能会被重复调用。在扣款场景中,Confirm阶段会实际扣除已冻结的金额。

Cancel阶段

取消执行业务,释放Try阶段预留的资源。Cancel操作也必须幂等。在扣款场景中,Cancel阶段会解冻之前冻结的金额。

TCC模式需要解决几个关键问题:

  • 空回滚:Try阶段未执行(比如网络原因导致请求未到达),但Cancel却被调用了。解决方案是维护事务状态,只回滚实际执行过的操作。

  • 悬挂:Cancel先于Try执行。解决方案是记录操作顺序,拒绝在Cancel之后到达的Try请求。

  • 幂等性:Confirm和Cancel可能被多次调用,必须保证重复调用不会产生副作用。

TCC的优点是对业务资源控制精细,性能较好;缺点是业务侵入性强,每个参与的服务都需要实现Try、Confirm、Cancel三个接口。

3.4 本地消息表

本地消息表是eBay提出的经典方案,核心思想是将分布式事务拆分为本地事务和异步消息通知。

核心流程

  1. 在业务数据库中创建一张本地消息表,用于记录待发送的消息

  2. 业务操作和消息写入放在同一个本地事务中,保证原子性

  3. 定时任务扫描本地消息表,将未发送的消息投递到消息队列

  4. 下游服务消费消息,执行自己的业务逻辑

  5. 下游处理成功后,回调更新消息状态

实现示例

-- 本地消息表结构
CREATE TABLE `local_message` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `message_id` varchar(64) NOT NULL COMMENT '消息唯一ID',
    `message_content` text NOT NULL COMMENT '消息内容JSON',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0-待发送 1-已发送 2-已完成',
    `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
    `next_retry_time` datetime NOT NULL COMMENT '下次重试时间',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_id` (`message_id`),
    INDEX `idx_status_time` (`status`, `next_retry_time`)
);
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    @Transactional
    public void createOrder(Order order) {
        // 1. 保存订单
        orderMapper.insert(order);
        
        // 2. 写入本地消息表
        LocalMessage message = new LocalMessage();
        message.setMessageId(UUID.randomUUID().toString());
        message.setMessageContent(JSON.toJSONString(order));
        message.setStatus(0);
        message.setNextRetryTime(new Date());
        messageMapper.insert(message);
        
        // 事务提交后,消息和订单同时生效
    }
}

// 定时任务投递消息
@Component
public class MessageDeliveryTask {
    
    @Scheduled(fixedDelay = 10000) // 每10秒执行一次
    public void deliverMessages() {
        List<LocalMessage> messages = messageMapper.findPendingMessages();
        for (LocalMessage message : messages) {
            try {
                rabbitTemplate.convertAndSend("order.exchange", 
                    "order.routingkey", message.getMessageContent());
                message.setStatus(1); // 已发送
                messageMapper.update(message);
            } catch (Exception e) {
                // 投递失败,增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                // 指数退避:下次重试时间 = 当前时间 + 2^n 秒
                long delay = (long) Math.pow(2, message.getRetryCount());
                message.setNextRetryTime(new Date(System.currentTimeMillis() + delay * 1000));
                messageMapper.update(message);
            }
        }
    }
}

本地消息表的优点是实现简单、不依赖外部事务协调器;缺点是消息表与业务表耦合,需要维护定时任务。

3.5 可靠消息最终一致性

可靠消息方案是对本地消息表的优化,将消息存储和管理的职责从业务服务中分离出来,形成独立的消息服务。

核心思路

可靠消息方案的核心是保证本地事务执行与消息发送的原子性。具体有两种实现方式:

方式一:先发半消息,后执行本地事务

  1. 消息发送者向消息队列发送一条“半消息”(消费者不可见)

  2. 消息队列返回确认后,发送者执行本地事务

  3. 根据本地事务结果,消息队列决定将消息变为“可消费”或删除消息

这种方式依赖消息队列对事务消息的支持,RocketMQ就提供了完整的事务消息实现。

方式二:本地事务中写消息表,异步发送

与本地消息表类似,但通过独立的消息服务来管理消息的发送和状态同步,解耦业务系统。

可靠消息方案适用于对实时性要求不高的异步场景,如积分同步、日志记录等。

3.6 最大努力通知

最大努力通知是最简单的最终一致性方案,适用于对一致性要求不高的场景。

核心思想

业务操作完成后,发起方尽最大努力通知接收方,如果通知失败则按照预设策略进行重试。重试达到上限后仍失败,则记录失败信息,等待人工介入。

典型应用场景

  • 短信、邮件发送通知

  • 第三方接口回调

  • 非关键业务的异步通知

实现要点

  • 定义清晰的重试策略(重试次数、间隔时间、退避算法)

  • 记录通知日志,便于问题追溯

  • 提供人工补偿机制

最大努力通知的优点是实现极其简单;缺点是不保证消息一定送达,适合业务容忍度高的场景。

3.7 Seata框架介绍

Seata是阿里巴巴开源的分布式事务解决方案,它是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

核心组件

Seata包含三个核心组件:

  • TC:事务协调者,维护全局事务和分支事务的状态,驱动事务的提交或回滚。

  • TM:事务管理器,发起全局事务,定义事务边界。

  • RM:资源管理器,管理分支事务的资源,向TC注册分支事务并汇报状态。

支持的四种模式

  • AT模式:自动代理数据源,通过解析SQL自动生成回滚日志,对业务代码几乎零侵入。

  • TCC模式:需要业务实现Try-Confirm-Cancel接口,适合高性能场景。

  • Saga模式:通过状态机编排长事务,适合业务流程较长的场景。

  • XA模式:基于数据库XA协议实现强一致性。

Seata是目前最流行的分布式事务框架之一,接下来的代码实战将以Seata为主要工具。

第四章 实战项目准备

4.1 项目场景描述

为了直观地展示分布式事务的各种解决方案,我们设计一个经典的电商下单场景。

业务描述

用户在电商平台购买商品,下单操作需要协调三个服务:

  • 订单服务:创建订单记录

  • 库存服务:扣减商品库存

  • 账户服务:扣减用户余额

业务规则

  • 订单金额必须等于商品单价×数量

  • 库存不足时不能下单

  • 余额不足时不能下单

  • 任何一步失败,整个下单操作需要回滚

问题场景

在分布式架构下,订单服务、库存服务、账户服务是三个独立的微服务,各自拥有独立的数据库。一次下单操作需要调用三个服务的接口,如果没有分布式事务管理,可能出现以下问题:

  • 订单创建成功,但库存扣减失败 → 订单数据不完整

  • 库存扣减成功,但订单创建失败 → 超卖问题

  • 余额扣减成功,但订单创建失败 → 用户资金损失

这个场景涵盖了分布式事务的典型要素,非常适合用来演示各种解决方案。

4.2 技术栈选型

本项目将使用以下技术栈:

技术版本用途
Spring Boot2.7.x微服务基础框架
Spring Cloud2021.x微服务治理
MyBatis-Plus3.5.xORM框架
MySQL8.0数据存储
Seata1.6.x分布式事务框架
RocketMQ4.9.x消息队列
Nacos2.2.x服务注册与配置中心

4.3 环境搭建

安装Nacos

# 下载Nacos
wget https://github.com/alibaba/nacos/releases/download/2.2.0/nacos-server-2.2.0.zip
unzip nacos-server-2.2.0.zip
cd nacos/bin

# 启动Nacos(单机模式)
sh startup.sh -m standalone

安装Seata

# 下载Seata
wget https://github.com/seata/seata/releases/download/v1.6.1/seata-server-1.6.1.zip
unzip seata-server-1.6.1.zip
cd seata/conf

# 配置Seata注册中心为Nacos
# 编辑registry.conf文件

Seata需要初始化数据库表,在业务数据库中执行以下脚本:

-- 全局事务表
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
);

-- 分支事务表
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`     BIGINT       NOT NULL,
    `xid`           VARCHAR(128) NOT NULL,
    `transaction_id` BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`   VARCHAR(256),
    `branch_type`   VARCHAR(8),
    `status`        TINYINT,
    `client_id`     VARCHAR(64),
    `application_data` VARCHAR(2000),
    `gmt_create`    DATETIME(6),
    `gmt_modified`  DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
);

-- 锁表
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
);

项目结构

seata-demo/
├── order-service/          # 订单服务(端口8081)
├── inventory-service/      # 库存服务(端口8082)
├── account-service/        # 账户服务(端口8083)
└── api-common/            # 公共API模块

4.4 数据库设计

订单服务数据库

CREATE DATABASE `db_order`;

CREATE TABLE `t_order` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `order_no` varchar(32) NOT NULL COMMENT '订单号',
    `user_id` bigint NOT NULL COMMENT '用户ID',
    `product_id` bigint NOT NULL COMMENT '商品ID',
    `quantity` int NOT NULL COMMENT '数量',
    `amount` decimal(10,2) NOT NULL COMMENT '金额',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_no` (`order_no`)
);

-- Seata AT模式需要undo_log表
CREATE TABLE `undo_log` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `branch_id` bigint NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
);

库存服务数据库

CREATE DATABASE `db_inventory`;

CREATE TABLE `t_inventory` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `product_id` bigint NOT NULL COMMENT '商品ID',
    `total_count` int NOT NULL COMMENT '总库存',
    `frozen_count` int NOT NULL DEFAULT 0 COMMENT '冻结库存(TCC用)',
    `version` int NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_product_id` (`product_id`)
);

INSERT INTO `t_inventory` (`product_id`, `total_count`) VALUES (1, 100);

账户服务数据库

CREATE DATABASE `db_account`;

CREATE TABLE `t_account` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `user_id` bigint NOT NULL COMMENT '用户ID',
    `balance` decimal(10,2) NOT NULL COMMENT '余额',
    `frozen_balance` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额(TCC用)',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_user_id` (`user_id`)
);

INSERT INTO `t_account` (`user_id`, `balance`) VALUES (1, 10000.00);

第五章 代码实战

5.1 问题场景演示

首先,我们演示一下没有分布式事务时可能出现的问题。

订单服务创建订单方法

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryClient inventoryClient;
    
    @Autowired
    private AccountClient accountClient;
    
    /**
     * 创建订单 - 无分布式事务版本
     * 问题:库存扣减成功但订单创建失败,导致超卖
     */
    @Transactional  // 这只是本地事务,无法控制远程调用
    public Order createOrder(Long userId, Long productId, Integer quantity) {
        // 计算金额
        BigDecimal amount = getProductPrice(productId).multiply(BigDecimal.valueOf(quantity));
        
        // 1. 创建订单(本地操作)
        Order order = new Order();
        order.setOrderNo(OrderNoGenerator.generate());
        order.setUserId(userId);
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setAmount(amount);
        order.setStatus(0);  // 待支付
        orderMapper.insert(order);
        
        // 2. 扣减库存(远程调用)
        inventoryClient.decrease(productId, quantity);
        
        // 3. 扣减余额(远程调用)
        accountClient.decrease(userId, amount);
        
        // 假设这里抛出异常,订单会回滚,但库存和余额已经扣减了!
        // throw new RuntimeException("订单创建失败");
        
        return order;
    }
}

运行这段代码,在订单创建成功后抛出一个异常,你会发现库存和余额已经被扣减,但订单却没有生成。这就是典型的分布式事务问题。

5.2 Seata AT模式实现

AT模式是Seata的核心模式,它通过代理数据源自动生成回滚日志,对业务代码几乎零侵入。

配置Seata

在application.yml中配置Seata:

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        
seata:
  enabled: true
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848

数据源代理配置

AT模式需要Seata代理数据源,以便自动生成undo_log:

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }
    
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
        // 用Seata代理数据源
        return new DataSourceProxy(druidDataSource);
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSourceProxy);
        factory.setTypeAliasesPackage("com.example.entity");
        return factory.getObject();
    }
}

使用全局事务注解

@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private InventoryClient inventoryClient;
    
    @Autowired
    private AccountClient accountClient;
    
    /**
     * 创建订单 - Seata AT模式
     * 只需添加@GlobalTransactional注解,Seata会自动管理分布式事务
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public Order createOrder(Long userId, Long productId, Integer quantity) {
        log.info("开始创建订单,XID: {}", RootContext.getXID());
        
        // 1. 创建订单(本地事务)
        BigDecimal amount = getProductPrice(productId).multiply(BigDecimal.valueOf(quantity));
        Order order = new Order();
        order.setOrderNo(OrderNoGenerator.generate());
        order.setUserId(userId);
        order.setProductId(productId);
        order.setQuantity(quantity);
        order.setAmount(amount);
        order.setStatus(0);
        orderMapper.insert(order);
        log.info("订单创建成功: {}", order.getOrderNo());
        
        // 2. 扣减库存(远程调用,分支事务)
        inventoryClient.decrease(productId, quantity);
        log.info("库存扣减成功");
        
        // 3. 扣减余额(远程调用,分支事务)
        accountClient.decrease(userId, amount);
        log.info("余额扣减成功");
        
        // 如果任何步骤抛出异常,Seata会自动触发全局回滚
        // 所有的分支事务都会被回滚
        
        return order;
    }
}

库存服务的AT模式实现

库存服务也需要同样的配置,业务代码非常简洁:

@RestController
public class InventoryController {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 扣减库存 - AT模式
     * Seata会自动记录前后镜像,回滚时自动恢复
     */
    @PostMapping("/inventory/decrease")
    public boolean decrease(@RequestParam Long productId, @RequestParam Integer quantity) {
        // 普通的数据库更新操作
        // Seata会拦截这个SQL,记录修改前后的数据快照
        int result = inventoryMapper.decreaseStock(productId, quantity);
        if (result == 0) {
            throw new RuntimeException("库存不足");
        }
        return true;
    }
}

AT模式工作原理

  1. 一阶段:Seata拦截业务SQL,解析SQL语义,在更新前记录数据快照(前镜像),执行业务SQL,再记录更新后的数据快照(后镜像),然后将前镜像和后镜像一起写入undo_log表,最后提交本地事务。

  2. 二阶段

    • 如果全局事务提交,Seata会异步删除undo_log记录

    • 如果全局事务回滚,Seata根据undo_log中的前后镜像生成回滚SQL,将数据恢复到更新前的状态

AT模式的最大优势是对业务代码零侵入,开发者只需添加@GlobalTransactional注解即可。

5.3 TCC模式实现

TCC模式需要业务显式实现Try、Confirm、Cancel三个阶段,但可以更精细地控制资源。

定义TCC接口

@LocalTCC
public interface AccountTccService {
    
    /**
     * Try阶段:预留资源
     * @param context 事务上下文,Seata自动注入
     * @param userId 用户ID
     * @param amount 扣款金额
     */
    @TwoPhaseBusinessAction(
        name = "decreaseAccount",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryDecrease(
        BusinessActionContext context,
        @Param("userId") Long userId,
        @Param("amount") BigDecimal amount
    );
    
    /**
     * Confirm阶段:确认提交
     */
    boolean confirm(BusinessActionContext context);
    
    /**
     * Cancel阶段:回滚释放
     */
    boolean cancel(BusinessActionContext context);
}

实现TCC服务

@Service
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {
    
    @Autowired
    private AccountMapper accountMapper;
    
    @Override
    public boolean tryDecrease(BusinessActionContext context, Long userId, BigDecimal amount) {
        log.info("TCC Try阶段: 冻结用户{}的金额{}", userId, amount);
        
        // 幂等性检查
        String xid = context.getXid();
        if (isExecuted(xid, "try")) {
            log.info("Try已执行过,跳过");
            return true;
        }
        
        // 检查余额并冻结
        int result = accountMapper.freezeBalance(userId, amount);
        if (result == 0) {
            log.warn("用户{}余额不足,无法冻结", userId);
            return false;
        }
        
        // 记录执行状态
        recordExecution(xid, "try");
        return true;
    }
    
    @Override
    public boolean confirm(BusinessActionContext context) {
        log.info("TCC Confirm阶段: 确认扣款,XID: {}", context.getXid());
        
        String xid = context.getXid();
        // 幂等性检查
        if (isExecuted(xid, "confirm")) {
            log.info("Confirm已执行过,跳过");
            return true;
        }
        
        // 获取Try阶段传递的参数
        Map<String, Object> actionContext = context.getActionContext();
        Long userId = (Long) actionContext.get("userId");
        BigDecimal amount = (BigDecimal) actionContext.get("amount");
        
        // 将冻结金额转为实际扣款
        accountMapper.confirmDecrease(userId, amount);
        
        recordExecution(xid, "confirm");
        return true;
    }
    
    @Override
    public boolean cancel(BusinessActionContext context) {
        log.info("TCC Cancel阶段: 解冻金额,XID: {}", context.getXid());
        
        String xid = context.getXid();
        // 空回滚检查:如果Try未执行,Cancel不应该执行
        if (!isExecuted(xid, "try")) {
            log.info("Try未执行,Cancel跳过(空回滚)");
            return true;
        }
        
        // 悬挂检查:如果Confirm已执行,Cancel不应执行
        if (isExecuted(xid, "confirm")) {
            log.info("Confirm已执行,Cancel跳过");
            return true;
        }
        
        // 幂等性检查
        if (isExecuted(xid, "cancel")) {
            log.info("Cancel已执行过,跳过");
            return true;
        }
        
        // 解冻金额
        Map<String, Object> actionContext = context.getActionContext();
        Long userId = (Long) actionContext.get("userId");
        BigDecimal amount = (BigDecimal) actionContext.get("amount");
        accountMapper.unfreezeBalance(userId, amount);
        
        recordExecution(xid, "cancel");
        return true;
    }
    
    /**
     * 幂等性记录表
     */
    private boolean isExecuted(String xid, String phase) {
        // 查询执行记录表
        return executionRecordMapper.exists(xid, phase);
    }
    
    private void recordExecution(String xid, String phase) {
        ExecutionRecord record = new ExecutionRecord();
        record.setXid(xid);
        record.setPhase(phase);
        record.setExecuteTime(new Date());
        executionRecordMapper.insert(record);
    }
}

对应的SQL操作

-- 冻结余额(Try阶段)
UPDATE t_account 
SET frozen_balance = frozen_balance + #{amount},
    balance = balance - #{amount}
WHERE user_id = #{userId} AND balance >= #{amount}

-- 确认扣款(Confirm阶段)
UPDATE t_account 
SET frozen_balance = frozen_balance - #{amount}
WHERE user_id = #{userId}

-- 解冻余额(Cancel阶段)
UPDATE t_account 
SET balance = balance + #{amount},
    frozen_balance = frozen_balance - #{amount}
WHERE user_id = #{userId}

调用TCC服务

@Service
public class OrderService {
    
    @Autowired
    private AccountTccService accountTccService;
    
    @GlobalTransactional
    public void createOrderWithTcc(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);
        
        // 2. TCC扣款 - 会自动执行Try-Confirm-Cancel流程
        accountTccService.tryDecrease(null, order.getUserId(), order.getAmount());
        
        // 3. TCC扣库存
        inventoryTccService.tryDecrease(null, order.getProductId(), order.getQuantity());
    }
}

AT vs TCC 对比

维度AT模式TCC模式
业务侵入性极低,只需添加注解高,需要实现三个接口
性能中等,需要写undo_log高,无额外日志开销
一致性最终一致最终一致
资源锁定时间短(一阶段提交即释放)极短(Try可自定义)
适用场景通用业务高性能、高并发场景

5.4 消息队列实现可靠消息

消息队列方案适合对实时性要求不高、可以接受最终一致性的场景。

配置RocketMQ事务消息

@Configuration
public class RocketMqConfig {
    
    @Bean
    public RocketMQTemplate rocketMQTemplate() {
        RocketMQTemplate template = new RocketMQTemplate();
        // 设置事务监听器
        template.setTransactionListener(orderTransactionListener());
        return template;
    }
    
    @Bean
    public TransactionListener orderTransactionListener() {
        return new OrderTransactionListener();
    }
}

实现事务消息监听器

@Component
@Slf4j
public class OrderTransactionListener implements TransactionListener {
    
    @Autowired
    private OrderMapper orderMapper;
    
    /**
     * 执行本地事务
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 解析消息内容
            OrderMessage orderMsg = JSON.parseObject((String) arg, OrderMessage.class);
            
            // 执行本地事务:创建订单
            Order order = new Order();
            order.setOrderNo(orderMsg.getOrderNo());
            order.setUserId(orderMsg.getUserId());
            order.setProductId(orderMsg.getProductId());
            order.setQuantity(orderMsg.getQuantity());
            order.setAmount(orderMsg.getAmount());
            order.setStatus(0);
            orderMapper.insert(order);
            
            log.info("本地事务执行成功,提交消息");
            return LocalTransactionState.COMMIT_MESSAGE;
            
        } catch (Exception e) {
            log.error("本地事务执行失败", e);
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
    
    /**
     * 回查本地事务状态
     * 当消息队列长时间未收到提交或回滚请求时,会调用此方法
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 从消息中获取业务标识
        String orderNo = msg.getUserProperty("orderNo");
        
        // 查询订单是否存在
        Order order = orderMapper.selectByOrderNo(orderNo);
        
        if (order != null) {
            // 订单存在,说明本地事务成功
            return LocalTransactionState.COMMIT_MESSAGE;
        } else {
            // 订单不存在,说明本地事务失败
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
}

发送事务消息

@Service
@Slf4j
public class OrderService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 使用事务消息创建订单
     */
    public void createOrderWithTransactionMessage(OrderMessage orderMsg) {
        // 构建消息
        MessageBuilder builder = MessageBuilder.withPayload(JSON.toJSONString(orderMsg));
        builder.setHeader("orderNo", orderMsg.getOrderNo());
        
        // 发送事务消息
        // 参数:目的地、消息、业务参数(会传给executeLocalTransaction)
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
            "order-topic",
            builder.build(),
            orderMsg  // 业务参数
        );
        
        log.info("事务消息发送结果: {}", result.getLocalTransactionState());
    }
}

消费消息:扣减库存

@Component
@Slf4j
public class InventoryConsumer {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    /**
     * 消费订单消息,扣减库存
     */
    @RocketMQMessageListener(
        topic = "order-topic",
        consumerGroup = "inventory-consumer-group"
    )
    public class InventoryMessageListener implements RocketMQListener<String> {
        
        @Override
        public void onMessage(String message) {
            // 解析消息
            OrderMessage orderMsg = JSON.parseObject(message, OrderMessage.class);
            
            // 幂等性处理:使用Redis记录已处理的消息ID
            String msgId = orderMsg.getMessageId();
            if (redisTemplate.opsForValue().setIfAbsent("msg:" + msgId, "1", 24, TimeUnit.HOURS)) {
                try {
                    // 扣减库存
                    int result = inventoryMapper.decreaseStock(
                        orderMsg.getProductId(), 
                        orderMsg.getQuantity()
                    );
                    
                    if (result == 0) {
                        log.warn("库存不足,消息将重试");
                        throw new RuntimeException("库存不足");
                    }
                    
                    log.info("库存扣减成功,订单号: {}", orderMsg.getOrderNo());
                    
                } catch (Exception e) {
                    log.error("库存扣减失败", e);
                    // 抛出异常,消息会进入重试队列
                    throw e;
                }
            } else {
                log.info("消息已处理过,跳过。消息ID: {}", msgId);
            }
        }
    }
}

可靠消息方案的执行流程

  1. 订单服务发送半消息到RocketMQ

  2. RocketMQ返回消息发送成功

  3. 订单服务执行本地事务(创建订单)

  4. 根据本地事务结果提交或回滚消息

  5. 库存服务消费消息,扣减库存

  6. 如果库存扣减失败,消息队列自动重试

第六章 最佳实践与总结

6.1 方案选型建议

在实际项目中,如何选择合适的分布式事务方案?以下是一些建议:

场景推荐方案理由
金融支付、转账Seata AT / TCC需要较高的一致性保证
电商下单核心流程Seata AT开发简单,一致性较好
积分、日志、通知可靠消息/本地消息表可接受最终一致性
跨行转账(长事务)Saga业务流程长,需要补偿
短信、邮件发送最大努力通知业务容忍度高

选型决策树

是否需要强一致性?
├── 是 → 2PC/Seata XA(性能要求不高)或 TCC(性能要求高)
└── 否 → 能否接受开发成本?
    ├── 开发成本低 → Seata AT(推荐)
    └── 开发成本可接受 → 本地消息表/可靠消息

6.2 常见问题与解决方案

问题1:事务超时

分布式事务涉及远程调用,网络延迟可能导致事务超时。

解决方案:

  • 合理设置事务超时时间,根据业务场景调整

  • 异步化处理非核心操作

  • 优化远程调用性能

seata:
  client:
    tm:
      commit-retry-count: 5
      rollback-retry-count: 5
    rm:
      report-retry-count: 5
      lock:
        retry-interval: 10
        retry-times: 30

问题2:幂等性问题

网络重试可能导致同一操作被执行多次。

解决方案:

  • 使用唯一业务标识(如订单号)作为幂等键

  • 在数据库层面使用唯一约束

  • 使用Redis记录已处理的操作ID

// 幂等处理示例
public void processWithIdempotent(String idempotentKey, Runnable business) {
    // 使用Redis的SETNX命令
    Boolean success = redisTemplate.opsForValue()
        .setIfAbsent("idempotent:" + idempotentKey, "1", 1, TimeUnit.HOURS);
    
    if (Boolean.TRUE.equals(success)) {
        business.run();
    } else {
        log.info("重复请求,已忽略: {}", idempotentKey);
    }
}

问题3:悬挂事务

Cancel在Try之前执行,导致Try执行时资源已经被释放。

解决方案:

  • 记录事务状态,拒绝在Cancel后执行的Try

  • 使用事务ID判断操作顺序

6.3 性能优化技巧

减少事务范围

将非核心操作移出分布式事务,改为异步处理。

@GlobalTransactional
public void createOrder(Order order) {
    // 核心操作:订单、库存、扣款
    orderService.create(order);
    inventoryService.decrease(order.getProductId(), order.getQuantity());
    accountService.decrease(order.getUserId(), order.getAmount());
    
    // 非核心操作:发送通知,移到事务外异步处理
    eventPublisher.publishEvent(new OrderCreatedEvent(order));
}

异步化处理

使用消息队列处理非实时操作,减少事务阻塞时间。

批量操作优化

将多个小事务合并为批量操作,减少网络开销。

合理使用缓存

对于频繁读取的数据使用缓存,减少数据库访问。

6.4 总结与展望

核心要点回顾

  1. 分布式事务是微服务架构下的必然产物,用于解决跨服务数据一致性问题。

  2. CAP定理和BASE理论是分布式事务的理论基础,理解这些理论有助于方案选型。

  3. 分布式事务方案多种多样,从2PC的强一致性到最大努力通知的最终一致性,各有适用场景。

  4. Seata框架极大地简化了分布式事务的开发,AT模式对业务代码几乎零侵入。

  5. 消息队列方案适合异步场景,通过本地事务+消息重试保证最终一致性。

  6. 分布式事务设计需要在一致性、性能和可用性之间做出权衡。

未来发展趋势

随着云原生和Serverless架构的普及,分布式事务也在演进:

  • 事务服务下沉:分布式事务能力将下沉到Service Mesh层,对业务代码完全透明。

  • 智能化事务管理:利用AI算法优化事务路由、超时设置和补偿策略。

  • 混合事务模式:根据业务场景动态选择最优的事务模式。

分布式事务是构建可靠分布式系统的基石。希望通过本文的学习,你能够理解分布式事务的核心原理,掌握各种解决方案的适用场景,并在实际项目中灵活运用。

请记住:没有银弹。分布式事务方案的选择需要结合具体的业务场景、性能要求和团队技术能力来综合考量。从简单场景开始,逐步积累经验,你会找到最适合自己业务的技术方案。


🌟 感谢您耐心阅读到这里!

🚀 技术成长没有捷径,但每一次的阅读、思考和实践,都在默默缩短您与成功的距离。

💡 如果本文对您有所启发,欢迎点赞👍、收藏📌、分享📤给更多需要的伙伴!

🗣️ 期待在评论区看到您的想法、疑问或建议,我会认真回复,让我们共同探讨、一起进步~

🔔 关注我,持续获取更多干货内容!

🤗 我们下篇文章见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值