在微服务架构与分库分表全面普及的今天,单体应用的本地事务已无法满足跨服务、跨数据源的原子性保障需求。分布式事务作为分布式系统的核心难题,是后端开发进阶的必备核心技能。本文从底层理论出发,全面拆解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)
- TM向所有参与全局事务的RM发送Prepare指令,携带全局唯一的XID。
- 每个RM收到指令后,执行本地事务操作,但不提交,同时锁定事务涉及的资源,持久化undo和redo日志。
- 如果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 优缺点与适用场景
优点
- 强一致性:严格遵循ACID原则,所有资源要么全提交,要么全回滚,数据一致性等级最高。
- 业务无侵入:事务逻辑由TM和RM实现,业务代码无需编写回滚逻辑,开发成本极低。
- 标准化程度高:XA是行业通用标准,绝大多数主流关系型数据库都提供完整支持。
缺点
- 性能极差:准备阶段需要锁定所有涉及的资源,直到全局事务结束才释放,长事务会导致资源长时间阻塞,并发能力极低。
- 同步阻塞:所有RM在等待指令的过程中处于阻塞状态,无法处理其他请求,极端情况下会引发系统雪崩。
- 协调者单点风险:TM是全局事务的核心,一旦TM宕机,所有RM都会持续持有资源锁,导致系统不可用。
- 适用范围有限:仅支持实现了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的执行流程
正常执行流程
- 主业务服务向TCC协调者发起全局事务,获取全局唯一的XID。
- TCC协调者调用所有分支服务的Try接口,执行资源预留操作。
- 所有分支服务的Try接口执行成功,TCC协调者调用所有分支服务的Confirm接口,执行最终提交。
- 所有Confirm接口执行成功,全局TCC事务完成。
异常回滚流程
- 任意一个分支服务的Try接口执行失败或超时未响应,TCC协调者触发回滚流程。
- TCC协调者调用所有已成功执行Try接口的分支服务的Cancel接口,释放预留资源。
- 所有Cancel接口执行成功,全局TCC事务回滚完成。
3.3 TCC的三大核心保障
TCC的落地难点在于异常场景处理,必须实现三大核心保障,否则会出现数据不一致问题:
- 幂等性:Confirm和Cancel接口可能因网络超时、协调者重试被多次调用,必须保证多次调用的结果与一次调用完全一致。实现方案:通过全局XID+分支事务ID作为唯一键,记录事务执行状态,执行前先判断状态,避免重复执行。
- 空回滚:分支服务的Try接口未被调用,但Cancel接口被触发,此时Cancel接口必须正常返回,不能抛出异常。出现原因:网络延迟导致Try请求超时,协调者先触发了Cancel回滚,之后Try请求才到达分支服务。实现方案:通过事务日志表记录分支事务执行状态,Cancel执行时未找到Try执行记录,直接返回成功并标记为空回滚。
- 防悬挂:Cancel接口执行完成后,Try接口才被调用,此时Try接口不能执行资源预留,否则会导致资源永久冻结无法释放。出现原因:网络拥堵导致Try请求被延迟,先触发了Cancel回滚,之后Try请求才到达分支服务。实现方案:Cancel执行时记录空回滚状态,Try执行前先判断是否已执行过Cancel,若已执行则直接拒绝。
3.4 优缺点与适用场景
优点
- 性能优异:锁的粒度是业务层面的,而非数据库行锁,资源锁定时间仅在Try阶段,不会持有锁到全局事务结束,并发能力远高于2PC。
- 适用范围广:完全由业务代码实现,不依赖底层资源的XA支持,可跨数据库、跨服务、跨中间件实现分布式事务。
- 隔离性好:通过Try阶段的资源预留,有效避免脏写、脏读等隔离性问题,数据安全性高。
- 柔性可控:在一致性和可用性之间实现了平衡,既保证了较高的一致性,又兼顾了系统可用性。
缺点
- 业务侵入性极高:每个分布式事务都需要编写Try、Confirm、Cancel三个接口,开发成本极高,对开发人员的能力要求严苛。
- 回滚逻辑复杂:所有回滚逻辑都需要业务代码实现,不同业务场景的回滚逻辑差异大,极易出现bug。
- 异常场景处理复杂:必须完整实现幂等性、空回滚、防悬挂三大核心保障,否则会出现数据不一致问题。
适用场景
- 高并发、短事务的核心业务场景
- 对数据一致性有较高要求的电商交易、支付充值场景
- 跨多个异构资源、无法使用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模式有两种实现方式:
- 协同式(Choreography):无中心协调者,每个服务执行完自身的正向操作后,发布事件触发下一个服务的执行,异常时发布补偿事件触发反向补偿。
- 编排式(Orchestration):有中央SAGA协调者,负责控制全局事务的执行流程,按顺序调用各个服务的正向操作,异常时按反向顺序调用补偿操作,是生产环境的主流实现方式。
4.2 SAGA的执行流程
以生产环境最常用的编排式SAGA为例,核心执行流程如下:
- 业务发起者向SAGA协调者发起全局事务,协调者创建事务实例,初始化事务状态机。
- SAGA协调者按事务流程定义,顺序调用第一个服务的正向操作T1。
- T1执行成功,协调者调用下一个服务的正向操作T2,以此类推,直到所有正向操作执行成功,全局事务完成。
- 若某个正向操作Ti执行失败,协调者触发回滚流程,按反向顺序调用补偿操作Ci、Ci-1、...、C1,所有补偿操作执行成功,全局事务回滚完成。
- 若补偿操作执行失败,协调者会进行重试,重试失败后触发告警与人工介入流程。
4.3 SAGA的核心保障
- 幂等性:正向操作和补偿操作都必须保证幂等性,应对网络超时、重试等场景。
- 可补偿性:补偿操作必须能够完全撤销正向操作带来的业务影响,不能出现部分撤销的情况。
- 状态持久化:SAGA协调者必须将全局事务的执行状态、每个步骤的执行结果持久化到数据库,避免服务宕机后丢失事务状态。
- 重试机制:对于执行失败的操作,采用指数退避的重试策略,避免瞬时故障导致事务失败,同时设置最大重试次数,避免无限重试。
- 人工兜底:对于重试多次仍然失败的事务,必须有告警和人工介入的兜底方案,避免数据不一致。
4.4 优缺点与适用场景
优点
- 完美支持长事务:将长事务拆分为多个短本地事务,没有长时间的资源锁定,不会出现长事务阻塞问题。
- 性能优异:每个步骤都是本地事务,执行速度快,并发能力高,没有全局锁的开销。
- 业务侵入性较低:每个业务仅需实现正向操作和补偿操作两个方法,开发成本远低于TCC。
- 适用范围广:不依赖底层资源的XA支持,可跨服务、跨数据库、跨中间件实现分布式事务。
缺点
- 无隔离性:SAGA模式没有资源预留阶段,直接执行正向操作,极易出现脏写、脏读、不可重复读等隔离性问题,数据冲突风险高。
- 一致性等级低:仅能保证最终一致性,无法实现强一致性,不适合对一致性要求高的核心业务。
- 补偿逻辑复杂:长事务的步骤越多,异常场景越复杂,补偿逻辑的开发和排查难度越大。
- 不可逆操作难以处理:部分业务操作是不可逆的(如发送短信、调用第三方支付接口),无法实现完美的补偿操作。
适用场景
- 长事务场景,跨多个服务、执行时间长的业务流程
- 高并发、对一致性要求不高,仅需最终一致性的业务场景
- 业务流程可逆向补偿的供应链、物流、工单审批场景
五、四大方案对比与生产选型指南
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 所有分布式事务都必须遵守的核心原则
- 幂等性是底线:所有分布式事务的接口,无论是正向操作、Confirm、Cancel还是补偿操作,都必须实现幂等性,这是分布式事务的基础。
- 事务状态必须持久化:所有全局事务的执行状态、每个步骤的执行结果都必须持久化到数据库,避免服务宕机后丢失事务状态,无法恢复。
- 完善的重试机制:对于执行失败的操作,采用指数退避的重试策略,避免瞬时故障导致事务失败,同时设置最大重试次数,避免无限重试。
- 全链路监控与告警:必须对分布式事务的执行情况进行全链路监控,对失败的事务、超时的事务、补偿失败的事务设置告警,及时发现问题。
- 人工兜底方案:所有分布式事务方案都必须有人工介入的兜底方案,对于重试多次仍然失败的事务,必须有告警和人工处理流程,避免数据不一致。
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步。
总结
分布式事务是分布式系统的核心难题,不存在银弹。所有的分布式事务方案,都是在一致性、可用性、性能、开发成本之间做权衡。我们需要根据业务的实际场景,选择最合适的方案,而不是盲目追求高大上的技术。同时,优先通过业务设计避免分布式事务,才是解决分布式事务问题的最佳方式。