分布式事务终极指南: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步。

总结

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

目录
相关文章
|
SQL 关系型数据库 数据库
学习分布式事务Seata看这一篇就够了,建议收藏
学习分布式事务Seata看这一篇就够了,建议收藏
26434 2
|
开发框架 架构师 Java
《深入理解分布式事务:原理与实战》,不可错过的精品!
在分布式应用系统中,特别是在金融相关的场景下,分布式事务是大家都关注的核心技术,同样也是系统的技术难点。本书从数据库和服务的分布式基础开始,由浅入深阐述了分布式事务的原理、解决方案。这种以框架开发者视角分享的分布式事务实现的源码和实践用例,对于应用架构师和开发者都有极大的价值。
5882 2
《深入理解分布式事务:原理与实战》,不可错过的精品!
|
存储 关系型数据库 数据库
聊多版本并发控制(MVCC)
MVCC是数据库并发控制技术,用于减少读写冲突。它维护数据的多个版本,使事务能读旧数据而写新数据,无需锁定记录。当前读获取最新版本,加锁防止修改;快照读不加锁,根据读取时的读视图(readview)决定读哪个版本。InnoDB通过隐藏字段(DB_TRX_ID, DB_ROLL_PTR)和undo log存储版本,readview记录活跃事务ID。读已提交每次读取都创建新视图,可重复读则在整个事务中复用一个视图,确保一致性。MVCC通过undo log版本链和readview规则决定事务可见性,实现了非阻塞并发读。
2134 5
聊多版本并发控制(MVCC)
|
8月前
|
存储 算法 关系型数据库
吃透分布式 ID:雪花算法、号段模式的底层逻辑与全场景架构避坑
本文深度解析分布式ID两大主流方案——雪花算法与号段模式,涵盖核心设计准则(唯一性、趋势递增、高性能等)、底层原理、代码实现、6大生产避坑指南及场景化选型建议,助你构建稳定可靠的分布式ID服务。
1043 4
|
8月前
|
SQL 关系型数据库 Java
吃透 Seata 分布式事务:原理拆解 + 生产级落地 + 全场景避坑实战
本文深度解析阿里开源分布式事务框架Seata:剖析TC/TM/RM三大角色与全局事务流程,详解AT(零侵入)、TCC(强控制)、SAGA(长事务)、XA(强一致)四大模式原理、适用场景及核心对比,并通过电商下单实战演示AT模式落地,最后系统梳理生产环境高可用、SQL限制、幂等处理、XID传播等全链路避坑指南。
1881 4
|
8月前
|
存储 缓存 安全
synchronized 底层全解:从对象头、锁升级到内核实现,击穿并发编程的核心基石
本文深度剖析Java中synchronized的底层原理:从三种使用范式、字节码实现,到对象内存布局、Mark Word状态切换,详解锁升级(偏向→轻量→重量)全流程及JVM优化(锁消除/粗化),并结合JOL实战验证,兼顾理论深度与生产实用性。
791 2
|
7月前
|
SQL 存储 监控
避坑必看!MySQL 三大日志(redo/undo/binlog)底层原理全拆解,事务一致性再也不懵
MySQL事务ACID的基石是redo、undo、binlog三大日志:redo log保障持久性(宕机不丢数据),undo log保障原子性(支持回滚与MVCC),binlog保障可追溯与主从同步。三者协同工作,缺一不可。
924 4
|
8月前
|
消息中间件 缓存 NoSQL
秒杀系统高并发核心优化与落地全指南
本文系统阐述秒杀系统架构设计:剖析瞬时高并发、库存超卖等核心痛点,提出漏斗过滤、读写分离、强一致性等设计原则;详解前端、Nginx、网关、业务、缓存、消息队列及数据库七层优化方案;并给出Redis预扣减+异步落库等生产级解决方案与完整代码实现。
931 3
|
8月前
|
缓存 Java Spring
详细解析Spring如何解决循环依赖问题
详细解析 Spring 循环依赖原理,从三级缓存、核心源码到完整流程一步步讲解,带你彻底搞懂 Spring 如何解决 Bean 循环依赖及 AOP 代理场景下的实现细节。
909 2
|
8月前
|
缓存 NoSQL 算法
扛住亿级流量的核心防线:限流、熔断、降级全链路深度拆解与实战
本文系统讲解分布式系统亿级流量治理,涵盖限流(固定/滑动窗口、漏桶、令牌桶及Redis分布式实现)、熔断(状态机、Resilience4j实战)与降级(功能开关、读/写降级)三大核心能力,并提供全链路分层架构、生产避坑指南与最佳实践,助力系统稳定扛压。
895 2

热门文章

最新文章