分布式事务终极指南:2PC/XA/TCC/SAGA 从底层原理到生产选型全拆解

简介: 本文系统解析分布式事务四大主流方案:XA/2PC(强一致但性能差)、TCC(高并发柔性事务)、SAGA(长事务最终一致)及理论基石(ACID/CAP/BASE),涵盖原理、流程、实战代码、优劣对比与生产选型标准,助你深入掌握核心逻辑。

在微服务架构与分库分表全面普及的今天,单体应用的本地事务已无法满足跨服务、跨数据源的原子性保障需求。分布式事务作为分布式系统的核心难题,是后端开发进阶的必备核心技能。本文从底层理论出发,全面拆解2PC、XA、TCC、SAGA四大主流分布式事务方案的核心原理、执行流程、实战落地、优缺点对比,最终给出生产环境的选型标准,帮你彻底吃透分布式事务的核心逻辑。

一、分布式事务的理论基石

1.1 本地事务的ACID原则

本地事务是分布式事务的基础,其核心是数据库层面的ACID四大特性,通过InnoDB的undo log(回滚日志)和redo log(重做日志)实现:

  • 原子性(Atomicity):事务内的所有操作,要么全部执行成功,要么全部回滚失败,不存在中间状态。
  • 一致性(Consistency):事务执行前后,数据的完整性约束没有被破坏,比如库存扣减后不能出现负数。
  • 隔离性(Isolation):多个并发事务之间相互隔离,互不干扰,数据库通过隔离级别解决脏读、不可重复读、幻读问题。
  • 持久性(Durability):事务提交成功后,对数据的修改会永久落盘,即使系统宕机也不会丢失。

1.2 CAP定理

CAP定理是分布式系统的基础理论,由加州大学伯克利分校Eric Brewer教授提出,明确了分布式系统的三大核心特性:

  • 一致性(Consistency):所有节点在同一时间看到的数据是完全一致的。
  • 可用性(Availability):系统提供的服务必须一直处于可用状态,每次请求都能在有限时间内获得响应。
  • 分区容错性(Partition tolerance):当网络出现分区故障时,系统仍然能够继续运行,不会因为网络问题导致整体崩溃。

核心结论:在分布式系统中,网络分区是不可避免的客观事实,因此分区容错性P是必须满足的前提。我们只能在一致性C和可用性A之间做权衡,要么选择CP(强一致性,牺牲部分可用性),要么选择AP(高可用,牺牲强一致性,保证最终一致性),不存在同时满足CAP三个特性的分布式系统。

1.3 BASE理论

BASE理论是对CAP中AP方案的工程化补充,是大规模分布式系统的实践总结,也是柔性分布式事务的核心指导思想:

  • Basically Available(基本可用):分布式系统出现故障时,允许损失部分非核心功能的可用性,通过降级、限流等手段保证核心功能正常运行。
  • Soft State(软状态):允许系统存在中间状态,该状态不会影响系统的整体可用性,比如数据的异步同步过程,允许不同节点之间存在短暂的数据不一致。
  • Eventually Consistent(最终一致性):系统中的所有数据副本,经过一段时间的同步后,最终能够达到一致的状态,不需要实时保证强一致性。

二、XA & 2PC 两阶段提交协议

2.1 核心定义与底层逻辑

XA是X/Open组织定义的分布式事务处理(DTP)标准规范,明确了事务管理器(TM)和资源管理器(RM)之间的交互接口;2PC(两阶段提交)是XA规范默认采用的事务提交协议,是实现XA规范的核心执行流程。

X/Open DTP模型包含三个核心角色:

  • AP(Application Program):应用程序,业务逻辑的发起者。
  • RM(Resource Manager):资源管理器,通常是支持XA规范的数据库(如MySQL InnoDB),负责管理本地资源,提供事务的提交和回滚能力。
  • TM(Transaction Manager):事务管理器,分布式事务的协调者,负责管理全局事务,分配全局唯一的事务ID(XID),协调所有RM的提交和回滚。

2.2 2PC的两阶段执行流程

2PC将分布式事务的提交拆分为两个严格的阶段,确保所有资源的操作要么全提交,要么全回滚。

第一阶段:准备阶段(Prepare)

  1. TM向所有参与全局事务的RM发送Prepare指令,携带全局唯一的XID。
  2. 每个RM收到指令后,执行本地事务操作,但不提交,同时锁定事务涉及的资源,持久化undo和redo日志。
  3. 如果RM执行成功,向TM返回Ready状态;如果执行失败,返回Abort状态。

第二阶段:提交/回滚阶段(Commit/Rollback)

  • 全成功场景:TM收到所有RM的Ready状态后,向所有RM发送Commit指令,每个RM提交本地事务,释放锁定的资源,向TM返回Done状态,全局事务完成。
  • 失败场景:只要有一个RM返回Abort状态,或者TM超时未收到某个RM的响应,TM立即向所有RM发送Rollback指令,每个RM根据undo日志回滚本地事务,释放锁定的资源,向TM返回Done状态,全局事务回滚完成。

2.3 XA 2PC 实战落地

环境说明

  • JDK 17、Spring Boot 3.2.5、MySQL 8.0
  • 持久层框架:MyBatis-Plus 3.5.9
  • XA事务管理器:Atomikos 6.0.0
  • 业务场景:下单操作,同时创建订单和扣减库存,跨两个数据库实现分布式事务

1. 数据库脚本

-- 订单库
CREATE DATABASE IF NOT EXISTS jam_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jam_order;
DROP TABLE IF EXISTS t_order;
CREATE TABLE t_order (
   id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   order_no VARCHAR(64) NOT NULL COMMENT '订单编号',
   user_id BIGINT NOT NULL COMMENT '用户ID',
   product_id BIGINT NOT NULL COMMENT '商品ID',
   num 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 COMMENT '创建时间',
   update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (id),
   UNIQUE KEY uk_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';

-- 库存库
CREATE DATABASE IF NOT EXISTS jam_stock DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE jam_stock;
DROP TABLE IF EXISTS t_stock;
CREATE TABLE t_stock (
   id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   product_id BIGINT NOT NULL COMMENT '商品ID',
   total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
   available_stock INT NOT NULL DEFAULT 0 COMMENT '可用库存',
   frozen_stock INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
   create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   PRIMARY KEY (id),
   UNIQUE KEY uk_product_id (product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='库存表';
INSERT INTO t_stock (product_id, total_stock, available_stock, frozen_stock) VALUES (1, 1000, 1000, 0);

2. Maven依赖配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.5</version>
       <relativePath/>
   </parent>
   <groupId>com.jam.demo</groupId>
   <artifactId>distributed-transaction-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>distributed-transaction-demo</name>
   <properties>
       <java.version>17</java.version>
       <mybatis-plus.version>3.5.9</mybatis-plus.version>
       <atomikos.version>6.0.0</atomikos.version>
       <fastjson2.version>2.0.52</fastjson2.version>
       <guava.version>33.2.1-jre</guava.version>
       <springdoc.version>2.5.0</springdoc.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jta-atomikos</artifactId>
       </dependency>
       <dependency>
           <groupId>com.atomikos</groupId>
           <artifactId>transactions-jdbc</artifactId>
           <version>${atomikos.version}</version>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.30</version>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>${springdoc.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

3. 应用配置文件

spring:
 application:
   name: distributed-transaction-demo
 jta:
   atomikos:
     properties:
       log-base-dir: ./logs/atomikos
     transaction-manager:
       default-jta-timeout: 30000
 datasource:
   order:
     driver-class-name: com.mysql.cj.jdbc.Driver
     jdbc-url: jdbc:mysql://localhost:3306/jam_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
     password: root
     unique-resource-name: orderDataSource
     xa-properties:
       url: jdbc:mysql://localhost:3306/jam_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
       user: root
       password: root
     xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
     min-pool-size: 5
     max-pool-size: 20
     borrow-connection-timeout: 30000
   stock:
     driver-class-name: com.mysql.cj.jdbc.Driver
     jdbc-url: jdbc:mysql://localhost:3306/jam_stock?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
     password: root
     unique-resource-name: stockDataSource
     xa-properties:
       url: jdbc:mysql://localhost:3306/jam_stock?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
       user: root
       password: root
     xa-data-source-class-name: com.mysql.cj.jdbc.MysqlXADataSource
     min-pool-size: 5
     max-pool-size: 20
     borrow-connection-timeout: 30000

mybatis-plus:
 configuration:
   map-underscore-to-camel-case: true
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 global-config:
   db-config:
     id-type: auto
     logic-delete-field: deleted
     logic-delete-value: 1
     logic-not-delete-value: 0

springdoc:
 swagger-ui:
   path: /swagger-ui.html
   tags-sorter: alpha
   operations-sorter: alpha
 api-docs:
   path: /v3/api-docs
 packages-to-scan: com.jam.demo.controller

4. 数据源与MyBatis-Plus配置

package com.jam.demo.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
* 订单数据源配置
* @author ken
*/

@Configuration
@MapperScan(basePackages = "com.jam.demo.mapper.order", sqlSessionFactoryRef = "orderSqlSessionFactory")
public class OrderDataSourceConfig {

   @Bean(name = "orderDataSource")
   @ConfigurationProperties(prefix = "spring.datasource.order")
   @Primary
   public DataSource orderDataSource() {
       return new AtomikosDataSourceBean();
   }

   @Bean(name = "orderSqlSessionFactory")
   @Primary
   public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource,
                                                     GlobalConfig globalConfig,
                                                     MybatisConfiguration mybatisConfiguration) throws Exception {
       MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
       sessionFactory.setDataSource(dataSource);
       sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
               .getResources("classpath:mapper/order/*.xml"));
       sessionFactory.setConfiguration(mybatisConfiguration);
       sessionFactory.setGlobalConfig(globalConfig);
       return sessionFactory.getObject();
   }
}

package com.jam.demo.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
* 库存数据源配置
* @author ken
*/

@Configuration
@MapperScan(basePackages = "com.jam.demo.mapper.stock", sqlSessionFactoryRef = "stockSqlSessionFactory")
public class StockDataSourceConfig {

   @Bean(name = "stockDataSource")
   @ConfigurationProperties(prefix = "spring.datasource.stock")
   public DataSource stockDataSource() {
       return new AtomikosDataSourceBean();
   }

   @Bean(name = "stockSqlSessionFactory")
   public SqlSessionFactory stockSqlSessionFactory(@Qualifier("stockDataSource") DataSource dataSource,
                                                     GlobalConfig globalConfig,
                                                     MybatisConfiguration mybatisConfiguration) throws Exception {
       MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
       sessionFactory.setDataSource(dataSource);
       sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
               .getResources("classpath:mapper/stock/*.xml"));
       sessionFactory.setConfiguration(mybatisConfiguration);
       sessionFactory.setGlobalConfig(globalConfig);
       return sessionFactory.getObject();
   }
}

package com.jam.demo.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* MyBatis-Plus配置类
* @author ken
*/

@Configuration
public class MybatisPlusConfig {

   @Bean
   public MybatisPlusInterceptor mybatisPlusInterceptor() {
       MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
       interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
       interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
       return interceptor;
   }

   @Bean
   public GlobalConfig globalConfig() {
       GlobalConfig globalConfig = new GlobalConfig();
       GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
       dbConfig.setIdType(com.baomidou.mybatisplus.annotation.IdType.AUTO);
       globalConfig.setDbConfig(dbConfig);
       return globalConfig;
   }

   @Bean
   public MybatisConfiguration mybatisConfiguration() {
       MybatisConfiguration configuration = new MybatisConfiguration();
       configuration.setMapUnderscoreToCamelCase(true);
       return configuration;
   }
}

5. 实体类与Mapper

package com.jam.demo.entity.order;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 订单实体类
* @author ken
*/

@Data
@TableName("t_order")
@Schema(description = "订单实体")
public class Order {

   @TableId(type = IdType.AUTO)
   @Schema(description = "主键ID")
   private Long id;

   @Schema(description = "订单编号")
   private String orderNo;

   @Schema(description = "用户ID")
   private Long userId;

   @Schema(description = "商品ID")
   private Long productId;

   @Schema(description = "购买数量")
   private Integer num;

   @Schema(description = "订单金额")
   private BigDecimal amount;

   @Schema(description = "订单状态:0-待支付,1-已支付,2-已取消")
   private Integer status;

   @Schema(description = "创建时间")
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   private LocalDateTime updateTime;
}

package com.jam.demo.entity.stock;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.time.LocalDateTime;

/**
* 库存实体类
* @author ken
*/

@Data
@TableName("t_stock")
@Schema(description = "库存实体")
public class Stock {

   @TableId(type = IdType.AUTO)
   @Schema(description = "主键ID")
   private Long id;

   @Schema(description = "商品ID")
   private Long productId;

   @Schema(description = "总库存")
   private Integer totalStock;

   @Schema(description = "可用库存")
   private Integer availableStock;

   @Schema(description = "冻结库存")
   private Integer frozenStock;

   @Schema(description = "创建时间")
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   private LocalDateTime updateTime;
}

package com.jam.demo.mapper.order;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.order.Order;
import org.apache.ibatis.annotations.Mapper;

/**
* 订单Mapper
* @author ken
*/

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

package com.jam.demo.mapper.stock;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.stock.Stock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

/**
* 库存Mapper
* @author ken
*/

@Mapper
public interface StockMapper extends BaseMapper<Stock> {

   @Update("UPDATE t_stock SET available_stock = available_stock - #{num} WHERE product_id = #{productId} AND available_stock >= #{num}")
   int deductAvailableStock(@Param("productId") Long productId, @Param("num") Integer num);
}

6. 业务服务层实现

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.order.Order;
import com.jam.demo.mapper.order.OrderMapper;
import com.jam.demo.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* 订单服务实现类
* @author ken
*/

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

   @Override
   public boolean createOrder(Order order) {
       boolean result = this.save(order);
       if (result) {
           log.info("订单创建成功,订单编号:{}", order.getOrderNo());
       } else {
           log.error("订单创建失败,订单编号:{}", order.getOrderNo());
       }
       return result;
   }
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.stock.Stock;
import com.jam.demo.mapper.stock.StockMapper;
import com.jam.demo.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* 库存服务实现类
* @author ken
*/

@Slf4j
@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {

   @Override
   public boolean deductStock(Long productId, Integer num) {
       int rows = this.baseMapper.deductAvailableStock(productId, num);
       if (rows > 0) {
           log.info("库存扣减成功,商品ID:{},扣减数量:{}", productId, num);
           return true;
       } else {
           log.error("库存扣减失败,商品ID:{},扣减数量:{},库存不足", productId, num);
           return false;
       }
   }
}

package com.jam.demo.service.impl;

import com.jam.demo.entity.order.Order;
import com.jam.demo.service.OrderBusinessService;
import com.jam.demo.service.OrderService;
import com.jam.demo.service.StockService;
import jakarta.transaction.UserTransaction;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.UUID;

/**
* 下单业务服务实现类
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderBusinessServiceImpl implements OrderBusinessService {

   private final OrderService orderService;
   private final StockService stockService;
   private final UserTransaction userTransaction;

   @Override
   public boolean placeOrder(Order order) {
       if (!StringUtils.hasText(order.getOrderNo())) {
           order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
       }
       if (order.getUserId() == null || order.getProductId() == null || order.getNum() == null || order.getNum() <= 0) {
           log.error("下单参数异常,订单信息:{}", order);
           return false;
       }

       try {
           userTransaction.begin();
           log.info("开启XA全局事务,订单编号:{}", order.getOrderNo());

           boolean deductResult = stockService.deductStock(order.getProductId(), order.getNum());
           if (!deductResult) {
               throw new RuntimeException("库存扣减失败");
           }

           boolean orderResult = orderService.createOrder(order);
           if (!orderResult) {
               throw new RuntimeException("订单创建失败");
           }

           userTransaction.commit();
           log.info("XA全局事务提交成功,订单编号:{}", order.getOrderNo());
           return true;
       } catch (Exception e) {
           log.error("XA全局事务执行异常,订单编号:{},异常信息:", order.getOrderNo(), e);
           try {
               userTransaction.rollback();
               log.info("XA全局事务回滚成功,订单编号:{}", order.getOrderNo());
           } catch (Exception rollbackEx) {
               log.error("XA全局事务回滚异常,订单编号:{},异常信息:", order.getOrderNo(), rollbackEx);
           }
           return false;
       }
   }
}

7. 控制层实现

package com.jam.demo.controller;

import com.jam.demo.entity.order.Order;
import com.jam.demo.service.OrderBusinessService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 订单控制器
* @author ken
*/

@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单相关操作接口")
public class OrderController {

   private final OrderBusinessService orderBusinessService;

   @PostMapping("/place")
   @Operation(summary = "下单接口", description = "XA分布式事务实现下单操作")
   public ResponseEntity<Boolean> placeOrder(@RequestBody Order order) {
       boolean result = orderBusinessService.placeOrder(order);
       return ResponseEntity.ok(result);
   }
}

2.4 优缺点与适用场景

优点

  1. 强一致性:严格遵循ACID原则,所有资源要么全提交,要么全回滚,数据一致性等级最高。
  2. 业务无侵入:事务逻辑由TM和RM实现,业务代码无需编写回滚逻辑,开发成本极低。
  3. 标准化程度高:XA是行业通用标准,绝大多数主流关系型数据库都提供完整支持。

缺点

  1. 性能极差:准备阶段需要锁定所有涉及的资源,直到全局事务结束才释放,长事务会导致资源长时间阻塞,并发能力极低。
  2. 同步阻塞:所有RM在等待指令的过程中处于阻塞状态,无法处理其他请求,极端情况下会引发系统雪崩。
  3. 协调者单点风险:TM是全局事务的核心,一旦TM宕机,所有RM都会持续持有资源锁,导致系统不可用。
  4. 适用范围有限:仅支持实现了XA规范的资源管理器,无法适配Redis、MQ、NoSQL等异构资源。

适用场景

  • 低并发、短事务的核心业务场景
  • 对数据强一致性有极高要求的金融核心交易、资金划转场景
  • 所有参与事务的资源都支持XA规范的场景

三、TCC 补偿型事务

3.1 核心定义与底层逻辑

TCC(Try-Confirm-Cancel)是业务层的两阶段提交协议,完全由业务代码实现分布式事务控制,不依赖底层数据库的XA支持,是高并发场景下应用最广泛的柔性事务方案。

TCC将分布式事务拆分为三个阶段,与2PC的两阶段逻辑一一对应:

  • Try阶段:对应2PC的准备阶段,核心是资源预留与业务校验。完成所有业务的合法性校验,预留业务所需的核心资源,锁定关键数据,但不执行最终的业务操作。例如电商场景中,Try阶段仅冻结库存,不直接扣减。
  • Confirm阶段:对应2PC的提交阶段,核心是确认提交。使用Try阶段预留的资源,执行最终的业务操作,必须保证幂等性,应对重试调用场景。
  • Cancel阶段:对应2PC的回滚阶段,核心是取消回滚。释放Try阶段预留的资源,回滚业务操作,必须保证幂等性,同时支持空回滚和防悬挂。

TCC的核心角色:

  • 主业务服务:事务的发起者,负责发起全局TCC事务。
  • TCC协调者:管理全局事务状态,协调所有分支事务的Confirm和Cancel执行。
  • 分支业务服务:实现Try、Confirm、Cancel三个接口的事务参与者。

3.2 TCC的执行流程

正常执行流程

  1. 主业务服务向TCC协调者发起全局事务,获取全局唯一的XID。
  2. TCC协调者调用所有分支服务的Try接口,执行资源预留操作。
  3. 所有分支服务的Try接口执行成功,TCC协调者调用所有分支服务的Confirm接口,执行最终提交。
  4. 所有Confirm接口执行成功,全局TCC事务完成。

异常回滚流程

  1. 任意一个分支服务的Try接口执行失败或超时未响应,TCC协调者触发回滚流程。
  2. TCC协调者调用所有已成功执行Try接口的分支服务的Cancel接口,释放预留资源。
  3. 所有Cancel接口执行成功,全局TCC事务回滚完成。

3.3 TCC的三大核心保障

TCC的落地难点在于异常场景处理,必须实现三大核心保障,否则会出现数据不一致问题:

  1. 幂等性:Confirm和Cancel接口可能因网络超时、协调者重试被多次调用,必须保证多次调用的结果与一次调用完全一致。实现方案:通过全局XID+分支事务ID作为唯一键,记录事务执行状态,执行前先判断状态,避免重复执行。
  2. 空回滚:分支服务的Try接口未被调用,但Cancel接口被触发,此时Cancel接口必须正常返回,不能抛出异常。出现原因:网络延迟导致Try请求超时,协调者先触发了Cancel回滚,之后Try请求才到达分支服务。实现方案:通过事务日志表记录分支事务执行状态,Cancel执行时未找到Try执行记录,直接返回成功并标记为空回滚。
  3. 防悬挂:Cancel接口执行完成后,Try接口才被调用,此时Try接口不能执行资源预留,否则会导致资源永久冻结无法释放。出现原因:网络拥堵导致Try请求被延迟,先触发了Cancel回滚,之后Try请求才到达分支服务。实现方案:Cancel执行时记录空回滚状态,Try执行前先判断是否已执行过Cancel,若已执行则直接拒绝。

3.4 优缺点与适用场景

优点

  1. 性能优异:锁的粒度是业务层面的,而非数据库行锁,资源锁定时间仅在Try阶段,不会持有锁到全局事务结束,并发能力远高于2PC。
  2. 适用范围广:完全由业务代码实现,不依赖底层资源的XA支持,可跨数据库、跨服务、跨中间件实现分布式事务。
  3. 隔离性好:通过Try阶段的资源预留,有效避免脏写、脏读等隔离性问题,数据安全性高。
  4. 柔性可控:在一致性和可用性之间实现了平衡,既保证了较高的一致性,又兼顾了系统可用性。

缺点

  1. 业务侵入性极高:每个分布式事务都需要编写Try、Confirm、Cancel三个接口,开发成本极高,对开发人员的能力要求严苛。
  2. 回滚逻辑复杂:所有回滚逻辑都需要业务代码实现,不同业务场景的回滚逻辑差异大,极易出现bug。
  3. 异常场景处理复杂:必须完整实现幂等性、空回滚、防悬挂三大核心保障,否则会出现数据不一致问题。

适用场景

  • 高并发、短事务的核心业务场景
  • 对数据一致性有较高要求的电商交易、支付充值场景
  • 跨多个异构资源、无法使用XA方案的场景

四、SAGA 长事务解决方案

4.1 核心定义与底层逻辑

SAGA模式由普林斯顿大学Hector Garcia-Molina和Kenneth Salem在1987年的论文《Sagas》中提出,是专门针对长事务场景的分布式事务解决方案,核心思想是将一个长分布式事务拆分为多个短的本地事务,每个本地事务对应一个补偿动作,通过正向执行和反向补偿实现数据最终一致性。

SAGA事务的核心模型: 一个完整的SAGA事务由N个正向操作T1、T2、...、Tn组成,每个正向操作Ti对应一个补偿操作Ci。

  • 正常执行流程:按顺序执行T1 → T2 → ... → Tn,所有正向操作执行成功,全局事务完成。
  • 异常回滚流程:若某个正向操作Ti执行失败,按反向顺序执行补偿操作Ci → Ci-1 → ... → C1,撤销之前所有正向操作的影响,保证数据最终一致性。

SAGA模式有两种实现方式:

  1. 协同式(Choreography):无中心协调者,每个服务执行完自身的正向操作后,发布事件触发下一个服务的执行,异常时发布补偿事件触发反向补偿。
  2. 编排式(Orchestration):有中央SAGA协调者,负责控制全局事务的执行流程,按顺序调用各个服务的正向操作,异常时按反向顺序调用补偿操作,是生产环境的主流实现方式。

4.2 SAGA的执行流程

以生产环境最常用的编排式SAGA为例,核心执行流程如下:

  1. 业务发起者向SAGA协调者发起全局事务,协调者创建事务实例,初始化事务状态机。
  2. SAGA协调者按事务流程定义,顺序调用第一个服务的正向操作T1。
  3. T1执行成功,协调者调用下一个服务的正向操作T2,以此类推,直到所有正向操作执行成功,全局事务完成。
  4. 若某个正向操作Ti执行失败,协调者触发回滚流程,按反向顺序调用补偿操作Ci、Ci-1、...、C1,所有补偿操作执行成功,全局事务回滚完成。
  5. 若补偿操作执行失败,协调者会进行重试,重试失败后触发告警与人工介入流程。

4.3 SAGA的核心保障

  1. 幂等性:正向操作和补偿操作都必须保证幂等性,应对网络超时、重试等场景。
  2. 可补偿性:补偿操作必须能够完全撤销正向操作带来的业务影响,不能出现部分撤销的情况。
  3. 状态持久化:SAGA协调者必须将全局事务的执行状态、每个步骤的执行结果持久化到数据库,避免服务宕机后丢失事务状态。
  4. 重试机制:对于执行失败的操作,采用指数退避的重试策略,避免瞬时故障导致事务失败,同时设置最大重试次数,避免无限重试。
  5. 人工兜底:对于重试多次仍然失败的事务,必须有告警和人工介入的兜底方案,避免数据不一致。

4.4 优缺点与适用场景

优点

  1. 完美支持长事务:将长事务拆分为多个短本地事务,没有长时间的资源锁定,不会出现长事务阻塞问题。
  2. 性能优异:每个步骤都是本地事务,执行速度快,并发能力高,没有全局锁的开销。
  3. 业务侵入性较低:每个业务仅需实现正向操作和补偿操作两个方法,开发成本远低于TCC。
  4. 适用范围广:不依赖底层资源的XA支持,可跨服务、跨数据库、跨中间件实现分布式事务。

缺点

  1. 无隔离性:SAGA模式没有资源预留阶段,直接执行正向操作,极易出现脏写、脏读、不可重复读等隔离性问题,数据冲突风险高。
  2. 一致性等级低:仅能保证最终一致性,无法实现强一致性,不适合对一致性要求高的核心业务。
  3. 补偿逻辑复杂:长事务的步骤越多,异常场景越复杂,补偿逻辑的开发和排查难度越大。
  4. 不可逆操作难以处理:部分业务操作是不可逆的(如发送短信、调用第三方支付接口),无法实现完美的补偿操作。

适用场景

  • 长事务场景,跨多个服务、执行时间长的业务流程
  • 高并发、对一致性要求不高,仅需最终一致性的业务场景
  • 业务流程可逆向补偿的供应链、物流、工单审批场景

五、四大方案对比与生产选型指南

5.1 核心维度对比表

对比维度 XA/2PC TCC SAGA
一致性等级 强一致性(CP) 柔性一致性(接近CP) 最终一致性(AP)
业务侵入性 极高 中等
开发成本 极高 中等
性能 极差 优秀 优秀
隔离性 极好
长事务支持 不支持 不支持 完美支持
异构资源支持 仅支持XA兼容资源 全支持 全支持
异常处理难度 极高
对开发能力要求 极高

5.2 易混淆点明确区分

1. XA与2PC的关系

XA是X/Open组织定义的分布式事务处理标准规范,定义了TM和RM之间的交互接口;2PC是XA规范默认采用的事务提交协议,是实现XA规范的执行流程。简单来说:XA是规范,2PC是XA规范的实现方式

2. 2PC与TCC的核心区别

  • 层级不同:2PC是资源层的两阶段提交,依赖底层数据库的XA支持;TCC是业务层的两阶段提交,完全由业务代码实现,不依赖底层资源。
  • 锁粒度不同:2PC锁定的是数据库行资源,直到全局事务结束才释放;TCC锁定的是业务层面的资源,Try阶段完成后即可释放数据库锁,锁的粒度更细、时间更短。
  • 侵入性不同:2PC对业务代码无侵入;TCC对业务代码侵入性极高,需要编写三个接口。

3. TCC与SAGA的核心区别

  • 执行阶段不同:TCC是两阶段提交,有Try资源预留阶段;SAGA是一阶段直接执行正向操作,没有资源预留阶段,靠补偿实现回滚。
  • 隔离性不同:TCC通过Try阶段的资源预留保证了良好的隔离性;SAGA没有隔离性,极易出现脏写、脏读问题。
  • 长事务支持不同:TCC不适合长事务,Try阶段会锁定资源,长事务会导致资源锁定时间过长;SAGA天生适合长事务,拆分为多个短本地事务,无长时间资源锁定。
  • 一致性等级不同:TCC的一致性等级更高,接近强一致性;SAGA仅能保证最终一致性。

5.3 生产环境选型标准

优先选择XA/2PC的场景

  • 业务对数据强一致性有极高要求,不允许出现任何数据不一致
  • 事务流程短、执行时间快,无长事务风险
  • 并发量不高,对性能要求不苛刻
  • 所有参与事务的资源都支持XA规范
  • 典型场景:金融核心交易、资金划转、银行对账

优先选择TCC的场景

  • 业务对数据一致性有较高要求,需要保证隔离性
  • 高并发场景,对性能要求高
  • 事务流程短、步骤少、执行时间快
  • 跨多个异构资源,无法使用XA方案
  • 典型场景:电商交易、支付充值、订单履约核心环节

优先选择SAGA的场景

  • 长事务场景,事务流程长、步骤多、执行时间久
  • 业务对一致性要求不高,仅需最终一致性
  • 高并发场景,对性能要求极高
  • 业务流程可逆向补偿,无非可逆操作
  • 典型场景:供应链管理、物流配送、工单审批、非核心金融业务

选型避坑核心原则

  • 能不用分布式事务就不用:优先通过业务设计避免分布式事务,比如将相关业务放在同一个服务、同一个数据库中,用本地事务解决。
  • 能选最终一致性就不选强一致性:强一致性方案性能差、可用性低,绝大多数业务场景最终一致性即可满足需求。
  • 不要过度设计:简单的最终一致性场景,优先用事务消息、本地消息表实现,比SAGA更简单、更稳定。

六、生产环境最佳实践与避坑指南

6.1 所有分布式事务都必须遵守的核心原则

  1. 幂等性是底线:所有分布式事务的接口,无论是正向操作、Confirm、Cancel还是补偿操作,都必须实现幂等性,这是分布式事务的基础。
  2. 事务状态必须持久化:所有全局事务的执行状态、每个步骤的执行结果都必须持久化到数据库,避免服务宕机后丢失事务状态,无法恢复。
  3. 完善的重试机制:对于执行失败的操作,采用指数退避的重试策略,避免瞬时故障导致事务失败,同时设置最大重试次数,避免无限重试。
  4. 全链路监控与告警:必须对分布式事务的执行情况进行全链路监控,对失败的事务、超时的事务、补偿失败的事务设置告警,及时发现问题。
  5. 人工兜底方案:所有分布式事务方案都必须有人工介入的兜底方案,对于重试多次仍然失败的事务,必须有告警和人工处理流程,避免数据不一致。

6.2 常见坑点避坑指南

XA/2PC坑点

  • 避免在XA事务中执行耗时操作,比如远程调用、大数据量查询,会导致资源长时间锁定,引发系统雪崩。
  • TM必须做集群部署,避免单点故障,否则会导致所有RM持有资源锁,系统不可用。
  • MySQL 5.7及以下版本的XA事务存在崩溃恢复bug,必须使用MySQL 8.0及以上版本。

TCC坑点

  • 90%的TCC数据不一致问题,都是因为没有处理空回滚和防悬挂,这是TCC落地的必做项,不能省略。
  • Confirm和Cancel接口必须实现幂等性,网络超时、协调者重试会导致接口被多次调用,无幂等性会导致数据重复扣减、重复创建。
  • Try阶段必须只做资源预留,不能执行最终的业务操作,否则Cancel阶段无法回滚,失去了TCC的核心意义。

SAGA坑点

  • 不能忽略隔离性问题,多个事务同时操作同一条数据会出现脏写、覆盖更新的问题,必须通过乐观锁、悲观锁、状态机控制解决。
  • 不可逆操作必须放在事务的最后一步,避免出现需要补偿但无法补偿的情况。
  • SAGA事务的步骤越多,异常场景越复杂,补偿难度越大,建议步骤不要超过10步。

总结

分布式事务是分布式系统的核心难题,不存在银弹。所有的分布式事务方案,都是在一致性、可用性、性能、开发成本之间做权衡。我们需要根据业务的实际场景,选择最合适的方案,而不是盲目追求高大上的技术。同时,优先通过业务设计避免分布式事务,才是解决分布式事务问题的最佳方式。

目录
相关文章
|
9天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5295 11
|
16天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
21389 116
|
13天前
|
人工智能 安全 前端开发
Team 版 OpenClaw:HiClaw 开源,5 分钟完成本地安装
HiClaw 基于 OpenClaw、Higress AI Gateway、Element IM 客户端+Tuwunel IM 服务器(均基于 Matrix 实时通信协议)、MinIO 共享文件系统打造。
8181 7

热门文章

最新文章