引言
在微服务架构成为主流的今天,业务系统被拆分为多个独立部署的服务,原本单体应用内的本地事务,演变为跨服务、跨数据库的分布式事务场景。分布式事务的核心挑战,是在网络不可靠、服务节点独立的前提下,保证多个独立数据源操作的原子性与一致性,避免出现订单创建成功但库存未扣减、余额扣减但订单未生成等数据不一致问题。
Seata作为阿里开源的轻量级分布式事务框架,凭借低侵入、高性能、易接入的特性,成为国内微服务体系下分布式事务的首选方案。本文将从底层原理出发,拆解Seata的核心架构与四大事务模式,通过实战案例完成落地,最后梳理全场景生产避坑指南,帮你彻底掌握Seata的核心能力与生产实践。
一、Seata核心架构与分布式事务本质
1.1 分布式事务的核心矛盾
本地事务依赖数据库的ACID特性保证原子性,而分布式场景下,多个数据库实例、多个服务节点的操作相互独立,单个节点的成功无法保证整体操作的成功。经典的电商下单场景就完美体现了这一矛盾:用户下单需要依次执行扣减商品库存、扣减用户账户余额、生成订单记录三个操作,三个操作分属不同的服务与数据库,任何一个环节失败,都需要所有已执行的操作全部回滚,否则就会出现数据不一致。
分布式事务的本质,就是通过一套协调机制,保证跨节点、跨数据源的多个操作,要么全部成功,要么全部失败,最终实现数据的最终一致性。
1.2 Seata三大核心角色
Seata的分布式事务模型,基于三个核心角色协同实现,三者分工明确,共同完成全局事务的协调与执行:
- TC(Transaction Coordinator 事务协调器):维护全局事务的运行状态,负责接收TM的指令,协调并驱动所有分支事务的提交或回滚,是Seata的大脑。
- TM(Transaction Manager 事务管理器):负责全局事务的生命周期管理,向TC发起全局事务的开启、提交或回滚指令,是全局事务的发起者。
- RM(Resource Manager 资源管理器):部署在每个业务服务节点,负责分支事务的执行,向TC注册分支事务、汇报分支事务状态,接收TC的指令,驱动本地事务的提交或回滚,同时管理本地的undo_log回滚日志。
三者的协同架构如下:
1.3 Seata全局事务的核心流程
一个完整的Seata全局事务,执行流程如下:
- TM向TC发起请求,开启一个全局事务,TC生成唯一的全局事务ID(XID);
- XID通过微服务的调用链路,在所有相关的服务节点中传播;
- 每个服务节点的RM,向TC注册分支事务,将该分支事务纳入对应XID的全局事务管理;
- RM执行本地的业务操作,完成分支事务的执行,并向TC汇报分支事务的执行状态;
- 当所有分支事务执行完成后,TM根据所有分支的执行状态,向TC发起全局提交或全局回滚指令;
- TC接收到指令后,协调所有对应的RM,完成分支事务的提交或回滚。
二、Seata四大事务模式深度拆解
Seata提供了四种不同的事务模式,分别适配不同的业务场景,每种模式的底层实现、一致性保证、性能表现都有显著差异,下面逐一拆解其核心原理与适用场景。
2.1 AT模式:无侵入自动事务模式
AT模式是Seata的默认模式,也是生产环境使用最广泛的模式,基于支持本地ACID特性的关系型数据库实现,对业务代码零侵入,通过自动生成回滚日志完成分布式事务的协调。
2.1.1 核心原理:两阶段提交
AT模式的核心是改进后的两阶段提交模型,大幅优化了传统XA模式的性能问题:
- 一阶段:业务执行与快照生成RM通过代理数据源拦截业务SQL,解析SQL的类型、表结构、查询条件,在执行业务SQL前,查询对应数据生成前镜像;执行业务SQL后,再次查询对应数据生成后镜像;将前镜像、后镜像与业务SQL信息组装为undo_log回滚日志,与业务数据的修改在同一个本地事务中提交到数据库。提交完成后,向TC申请对应数据的全局锁,申请成功后释放本地数据库锁与数据库连接,完成一阶段执行。
- 二阶段:提交/回滚执行
- 全局提交:当TM发起全局提交指令时,TC通知所有RM异步清理对应分支事务的undo_log日志,整个过程无需操作业务数据,执行速度极快。
- 全局回滚:当TM发起全局回滚指令时,TC通知所有RM执行分支回滚,RM通过一阶段生成的undo_log日志,校验数据的后镜像与当前数据库数据的一致性,校验通过后,根据前镜像生成反向补偿SQL,执行回滚操作,恢复业务数据到事务执行前的状态,完成后清理undo_log并释放全局锁。
AT模式的完整执行流程如下:
2.1.2 隔离级别保证
AT模式通过两层机制实现事务隔离:
- 写隔离:通过全局锁实现,一阶段本地事务提交前,必须成功获取对应数据的全局锁,否则无法提交本地事务;全局锁在全局事务结束(提交/回滚)后才会释放,保证同一时间只有一个全局事务能修改同一行数据,彻底避免脏写。
- 读隔离:默认使用数据库本地的隔离级别,若需要实现全局的读已提交,可通过
SELECT FOR UPDATE语句触发全局锁检查,避免读取到未提交的全局事务数据。
2.1.3 适用场景
绝大多数基于关系型数据库的微服务业务场景,尤其是希望对业务代码零侵入、快速接入分布式事务的场景,是生产环境的首选模式。
2.2 TCC模式:手动编程事务模式
TCC(Try-Confirm-Cancel)是一种侵入式的手动编程分布式事务模式,需要开发者手动实现三个阶段的业务逻辑,完全不依赖底层数据库的事务能力,适配非关系型数据库、特殊业务逻辑等AT模式无法覆盖的场景。
2.2.1 三个阶段核心职责
- Try阶段:完成业务资源的检查与预留,是预处理阶段。例如转账场景中,Try阶段不会直接扣减用户余额,而是冻结用户的转账金额,完成资源预留。
- Confirm阶段:确认执行业务操作,使用Try阶段预留的资源完成最终的业务提交,该阶段必须保证幂等性,因为TC会重复调用Confirm直到执行成功。例如转账场景中,Confirm阶段扣减冻结的金额,完成转账操作。
- Cancel阶段:取消执行业务操作,释放Try阶段预留的资源,完成业务回滚,该阶段同样必须保证幂等性,同时需要处理空回滚、事务悬挂等问题。例如转账场景中,Cancel阶段解冻用户冻结的金额,恢复到初始状态。
2.2.2 适用场景
非关系型数据库(如Redis、MongoDB)操作、跨金融机构转账、需要自定义事务逻辑的特殊业务场景,对开发者的编码能力要求较高,需要手动处理幂等、空回滚、悬挂等核心问题。
2.3 SAGA模式:长事务解决方案
SAGA模式是针对长事务场景设计的分布式事务方案,核心思想是将长事务拆分为多个正向的本地分支事务,每个分支事务都对应一个反向的补偿操作;当全局事务执行失败时,通过反向补偿操作,将已执行成功的分支事务逐一回滚,保证数据的最终一致性。
Seata的SAGA模式提供两种实现方式:
- 注解模式:通过
@SagaTransactional注解快速接入,适合简单的长事务场景。 - 状态机模式:通过JSON配置定义事务流程与分支的依赖关系,支持复杂的分支编排、异步执行、异常重试,适合复杂的长事务场景。
适用场景
业务流程长、事务参与者多、事务执行周期长的场景,例如供应链系统、跨境电商订单流程、金融信贷审批流程等。
2.4 XA模式:强一致性事务模式
XA模式基于数据库的XA协议实现,是传统的强一致性两阶段提交方案。一阶段执行业务SQL后,不提交本地事务,仅向TC汇报执行状态,持有数据库的本地锁;二阶段TC根据所有分支的执行状态,通知所有RM统一提交或回滚本地事务,释放数据库锁。
XA模式保证了全局事务的强一致性,但因为一阶段长期持有数据库锁,性能极低,并发能力差,仅适用于短事务、强一致性要求极高的核心场景。
2.5 四大模式核心对比
| 模式 | 代码侵入性 | 一致性保证 | 性能表现 | 数据库依赖 | 核心适用场景 |
| AT | 零侵入 | 最终一致性 | 高 | 关系型数据库 | 绝大多数常规微服务业务场景 |
| TCC | 高侵入 | 最终一致性 | 高 | 无依赖 | 非关系型数据库、自定义事务逻辑场景 |
| SAGA | 中侵入 | 最终一致性 | 中 | 无依赖 | 长事务、复杂业务流程场景 |
| XA | 零侵入 | 强一致性 | 低 | 支持XA协议的数据库 | 短事务、强一致性核心场景 |
三、生产级落地实战
本文以经典的电商下单场景为例,基于AT模式实现完整的分布式事务落地,包含订单、库存、账户三个微服务,实现下单时库存扣减、余额扣减、订单创建的原子性操作。
3.1 环境与版本说明
- 基础环境:JDK 17、MySQL 8.0、Maven 3.8+、Seata TC 2.2.0
- 核心组件版本:Spring Boot 3.2.4、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.0、MyBatis Plus 3.5.6、Fastjson2 2.0.52、Guava 33.1.0
3.2 数据库脚本
3.2.1 数据库创建
CREATE DATABASE IF NOT EXISTS seata_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS seata_storage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS seata_account DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3.2.2 UndoLog表创建(AT模式必须,每个业务库都需执行)
USE seata_order;
CREATE TABLE IF NOT EXISTS undo_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'increment id',
branch_id BIGINT NOT NULL COMMENT 'branch transaction id',
xid VARCHAR(100) NOT NULL COMMENT 'global transaction id',
context VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
rollback_info LONGBLOB NOT NULL COMMENT 'rollback info',
log_status INT NOT NULL COMMENT '0:normal status, 1:defense status',
log_created DATETIME NOT NULL COMMENT 'create datetime',
log_modified DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id),
KEY ix_log_created (log_created)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = 'AT transaction mode undo table';
USE seata_storage;
CREATE TABLE IF NOT EXISTS undo_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'increment id',
branch_id BIGINT NOT NULL COMMENT 'branch transaction id',
xid VARCHAR(100) NOT NULL COMMENT 'global transaction id',
context VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
rollback_info LONGBLOB NOT NULL COMMENT 'rollback info',
log_status INT NOT NULL COMMENT '0:normal status, 1:defense status',
log_created DATETIME NOT NULL COMMENT 'create datetime',
log_modified DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id),
KEY ix_log_created (log_created)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = 'AT transaction mode undo table';
USE seata_account;
CREATE TABLE IF NOT EXISTS undo_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'increment id',
branch_id BIGINT NOT NULL COMMENT 'branch transaction id',
xid VARCHAR(100) NOT NULL COMMENT 'global transaction id',
context VARCHAR(128) NOT NULL COMMENT 'undo_log context, such as serialization',
rollback_info LONGBLOB NOT NULL COMMENT 'rollback info',
log_status INT NOT NULL COMMENT '0:normal status, 1:defense status',
log_created DATETIME NOT NULL COMMENT 'create datetime',
log_modified DATETIME NOT NULL COMMENT 'modify datetime',
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id),
KEY ix_log_created (log_created)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = 'AT transaction mode undo table';
3.2.3 业务表创建与初始化
-- 订单表
USE seata_order;
CREATE TABLE IF NOT EXISTS t_order (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '订单编号',
user_id BIGINT NOT NULL COMMENT '用户ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
count INT NOT NULL COMMENT '购买数量',
amount DECIMAL(18,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 '更新时间',
UNIQUE KEY uk_order_no (order_no)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '订单表';
-- 库存表
USE seata_storage;
CREATE TABLE IF NOT EXISTS t_storage (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '库存ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
total INT NOT NULL DEFAULT 0 COMMENT '总库存',
used INT NOT NULL DEFAULT 0 COMMENT '已用库存',
residue 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 '更新时间',
UNIQUE KEY uk_product_id (product_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '库存表';
INSERT INTO t_storage (product_id, total, used, residue) VALUES (1, 100, 0, 100);
-- 账户表
USE seata_account;
CREATE TABLE IF NOT EXISTS t_account (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '账户ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
frozen DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '冻结金额',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_user_id (user_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '账户表';
INSERT INTO t_account (user_id, balance, frozen) VALUES (1, 1000.00, 0.00);
3.3 Maven工程结构
工程采用父子结构,父工程统一管理依赖版本,子模块分为三个业务服务:order-service、storage-service、account-service。
3.3.1 父工程pom.xml
<?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.4</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>seata-demo-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>order-service</module>
<module>storage-service</module>
<module>account-service</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
<seata.version>2.2.0</seata.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql.version>8.3.0</mysql.version>
<lombok.version>1.18.30</lombok.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.1.0-jre</guava.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.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>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</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>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.3.2 子模块公共pom依赖(三个服务通用)
<?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>com.jam.demo</groupId>
<artifactId>seata-demo-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>
注:storage-service和account-service的pom.xml仅需修改artifactId即可。
3.4 核心配置文件
3.4.1 订单服务application.yml
server:
port: 8081
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: your_password
seata:
service:
vgroup-mapping:
order_tx_group: default
registry:
type: file
config:
type: file
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
3.4.2 库存服务application.yml
server:
port: 8082
spring:
application:
name: storage-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_storage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: your_password
seata:
service:
vgroup-mapping:
storage_tx_group: default
registry:
type: file
config:
type: file
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
3.4.3 账户服务application.yml
server:
port: 8083
spring:
application:
name: account-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: your_password
seata:
service:
vgroup-mapping:
account_tx_group: default
registry:
type: file
config:
type: file
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
api-docs:
path: /v3/api-docs
3.5 核心代码实现
3.5.1 库存服务核心代码
实体类
package com.jam.demo.storage.entity;
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_storage")
@Schema(description = "库存实体")
public class Storage {
@TableId(type = IdType.AUTO)
@Schema(description = "库存ID")
private Long id;
@Schema(description = "商品ID")
private Long productId;
@Schema(description = "总库存")
private Integer total;
@Schema(description = "已用库存")
private Integer used;
@Schema(description = "剩余库存")
private Integer residue;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口
package com.jam.demo.storage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.storage.entity.Storage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 库存Mapper接口
* @author ken
*/
@Mapper
public interface StorageMapper extends BaseMapper<Storage> {
/**
* 扣减库存
* @param productId 商品ID
* @param count 扣减数量
* @return 影响行数
*/
@Update("UPDATE t_storage SET used = used + #{count}, residue = residue - #{count} WHERE product_id = #{productId} AND residue >= #{count}")
int deductStorage(@Param("productId") Long productId, @Param("count") Integer count);
}
Service层
package com.jam.demo.storage.service;
import com.jam.demo.storage.mapper.StorageMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
/**
* 库存服务
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StorageService {
private final StorageMapper storageMapper;
private final TransactionTemplate transactionTemplate;
/**
* 扣减商品库存
* @param productId 商品ID
* @param count 扣减数量
* @return 扣减结果
*/
public Boolean deductStorage(Long productId, Integer count) {
if (ObjectUtils.isEmpty(productId) || ObjectUtils.isEmpty(count) || count <= 0) {
log.error("库存扣减参数异常,productId:{}, count:{}", productId, count);
return Boolean.FALSE;
}
return transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
int rows = storageMapper.deductStorage(productId, count);
if (rows <= 0) {
log.error("库存扣减失败,商品库存不足,productId:{}, count:{}", productId, count);
status.setRollbackOnly();
return Boolean.FALSE;
}
log.info("库存扣减成功,productId:{}, count:{}", productId, count);
return Boolean.TRUE;
} catch (Exception e) {
log.error("库存扣减异常,productId:{}, count:{}", productId, count, e);
status.setRollbackOnly();
return Boolean.FALSE;
}
}
});
}
}
Controller层
package com.jam.demo.storage.controller;
import com.jam.demo.storage.service.StorageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 库存控制器
* @author ken
*/
@RestController
@RequestMapping("/storage")
@RequiredArgsConstructor
@Tag(name = "库存管理", description = "库存相关操作接口")
public class StorageController {
private final StorageService storageService;
@PostMapping("/deduct")
@Operation(summary = "扣减库存", description = "根据商品ID扣减对应数量的库存")
public Boolean deductStorage(
@Parameter(description = "商品ID", required = true) @RequestParam Long productId,
@Parameter(description = "扣减数量", required = true) @RequestParam Integer count) {
return storageService.deductStorage(productId, count);
}
}
启动类
package com.jam.demo.storage;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 库存服务启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.storage.mapper")
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
3.5.2 账户服务核心代码
实体类
package com.jam.demo.account.entity;
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_account")
@Schema(description = "账户实体")
public class Account {
@TableId(type = IdType.AUTO)
@Schema(description = "账户ID")
private Long id;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "账户余额")
private BigDecimal balance;
@Schema(description = "冻结金额")
private BigDecimal frozen;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口
package com.jam.demo.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.account.entity.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.math.BigDecimal;
/**
* 账户Mapper接口
* @author ken
*/
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
/**
* 扣减账户余额
* @param userId 用户ID
* @param amount 扣减金额
* @return 影响行数
*/
@Update("UPDATE t_account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount}")
int deductBalance(@Param("userId") Long userId, @Param("amount") BigDecimal amount);
}
Service层
package com.jam.demo.account.service;
import com.jam.demo.account.mapper.AccountMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
/**
* 账户服务
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountMapper accountMapper;
private final TransactionTemplate transactionTemplate;
/**
* 扣减账户余额
* @param userId 用户ID
* @param amount 扣减金额
* @return 扣减结果
*/
public Boolean deductBalance(Long userId, BigDecimal amount) {
if (ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(amount) || amount.compareTo(BigDecimal.ZERO) <= 0) {
log.error("余额扣减参数异常,userId:{}, amount:{}", userId, amount);
return Boolean.FALSE;
}
return transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
int rows = accountMapper.deductBalance(userId, amount);
if (rows <= 0) {
log.error("余额扣减失败,账户余额不足,userId:{}, amount:{}", userId, amount);
status.setRollbackOnly();
return Boolean.FALSE;
}
log.info("余额扣减成功,userId:{}, amount:{}", userId, amount);
return Boolean.TRUE;
} catch (Exception e) {
log.error("余额扣减异常,userId:{}, amount:{}", userId, amount, e);
status.setRollbackOnly();
return Boolean.FALSE;
}
}
});
}
}
Controller层
package com.jam.demo.account.controller;
import com.jam.demo.account.service.AccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
/**
* 账户控制器
* @author ken
*/
@RestController
@RequestMapping("/account")
@RequiredArgsConstructor
@Tag(name = "账户管理", description = "账户相关操作接口")
public class AccountController {
private final AccountService accountService;
@PostMapping("/deduct")
@Operation(summary = "扣减账户余额", description = "根据用户ID扣减对应金额的账户余额")
public Boolean deductBalance(
@Parameter(description = "用户ID", required = true) @RequestParam Long userId,
@Parameter(description = "扣减金额", required = true) @RequestParam BigDecimal amount) {
return accountService.deductBalance(userId, amount);
}
}
启动类
package com.jam.demo.account;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 账户服务启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.account.mapper")
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
3.5.3 订单服务核心代码
实体类
package com.jam.demo.order.entity;
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 count;
@Schema(description = "订单金额")
private BigDecimal amount;
@Schema(description = "订单状态:0-待支付,1-已支付,2-已取消")
private Integer status;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
Mapper接口
package com.jam.demo.order.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
/**
* 订单Mapper接口
* @author ken
*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
Feign客户端
package com.jam.demo.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* 库存服务Feign客户端
* @author ken
*/
@FeignClient(name = "storage-service", url = "http://127.0.0.1:8082")
public interface StorageFeignClient {
@PostMapping("/storage/deduct")
Boolean deductStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.jam.demo.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
/**
* 账户服务Feign客户端
* @author ken
*/
@FeignClient(name = "account-service", url = "http://127.0.0.1:8083")
public interface AccountFeignClient {
@PostMapping("/account/deduct")
Boolean deductBalance(@RequestParam("userId") Long userId, @RequestParam("amount") BigDecimal amount);
}
Service层
package com.jam.demo.order.service;
import com.jam.demo.order.entity.Order;
import com.jam.demo.order.feign.AccountFeignClient;
import com.jam.demo.order.feign.StorageFeignClient;
import com.jam.demo.order.mapper.OrderMapper;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.util.UUID;
/**
* 订单服务
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final StorageFeignClient storageFeignClient;
private final AccountFeignClient accountFeignClient;
private final TransactionTemplate transactionTemplate;
private static final BigDecimal UNIT_PRICE = new BigDecimal("10");
/**
* 创建订单
* @param userId 用户ID
* @param productId 商品ID
* @param count 购买数量
* @return 订单创建结果
*/
@GlobalTransactional(name = "order-create-tx", rollbackFor = Exception.class)
public Boolean createOrder(Long userId, Long productId, Integer count) {
if (ObjectUtils.isEmpty(userId) || ObjectUtils.isEmpty(productId) || ObjectUtils.isEmpty(count) || count <= 0) {
log.error("订单创建参数异常,userId:{}, productId:{}, count:{}", userId, productId, count);
return Boolean.FALSE;
}
// 1. 计算订单金额
BigDecimal orderAmount = UNIT_PRICE.multiply(new BigDecimal(count));
log.info("开始创建订单,userId:{}, productId:{}, count:{}, orderAmount:{}", userId, productId, count, orderAmount);
// 2. 远程调用库存服务扣减库存
Boolean storageResult = storageFeignClient.deductStorage(productId, count);
if (!storageResult) {
log.error("库存扣减失败,终止订单创建");
throw new RuntimeException("库存扣减失败");
}
// 3. 远程调用账户服务扣减余额
Boolean accountResult = accountFeignClient.deductBalance(userId, orderAmount);
if (!accountResult) {
log.error("余额扣减失败,终止订单创建");
throw new RuntimeException("余额扣减失败");
}
// 4. 本地事务创建订单
return transactionTemplate.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
try {
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString().replace("-", ""));
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setAmount(orderAmount);
order.setStatus(0);
int rows = orderMapper.insert(order);
if (rows <= 0) {
log.error("订单创建失败,数据插入异常");
status.setRollbackOnly();
throw new RuntimeException("订单创建失败");
}
log.info("订单创建成功,orderNo:{}", order.getOrderNo());
return Boolean.TRUE;
} catch (Exception e) {
log.error("订单创建异常", e);
status.setRollbackOnly();
throw e;
}
}
});
}
}
Controller层
package com.jam.demo.order.controller;
import com.jam.demo.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单控制器
* @author ken
*/
@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单相关操作接口")
public class OrderController {
private final OrderService orderService;
@PostMapping("/create")
@Operation(summary = "创建订单", description = "创建订单并完成库存扣减与余额扣减的分布式事务")
public Boolean createOrder(
@Parameter(description = "用户ID", required = true) @RequestParam Long userId,
@Parameter(description = "商品ID", required = true) @RequestParam Long productId,
@Parameter(description = "购买数量", required = true) @RequestParam Integer count) {
return orderService.createOrder(userId, productId, count);
}
}
启动类
package com.jam.demo.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 订单服务启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.order.mapper")
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
四、生产环境全链路避坑指南
4.1 TC服务端高可用避坑
- 集群部署强制要求:生产环境严禁使用单点TC,必须采用集群部署,通过注册中心(Nacos)实现集群节点的发现与负载均衡;TC的存储模式必须使用DB模式(MySQL),严禁使用File模式,File模式仅适用于本地测试,无法实现集群数据同步与持久化。
- 事务分组隔离:生产环境必须按业务集群、环境划分独立的事务分组,避免测试环境与生产环境、不同业务线的TC集群混用,防止事务请求路由错误导致的事务失效。
- 核心参数调优:
globalLockTimeout:全局锁超时时间,高并发场景建议调整为3000-5000ms,避免锁等待时间过长导致的吞吐量下降,同时防止锁超时导致的事务回滚。maxCommitRetryTimeout、maxRollbackRetryTimeout:提交/回滚最大重试时间,建议设置为86400000ms(24小时),保证异常场景下TC有足够的时间重试,避免事务悬挂。store.db.maxConn:TC数据库连接池最大连接数,根据集群规模调整,建议设置为100-200,避免连接池耗尽导致TC无法处理事务请求。
4.2 AT模式核心避坑
- 数据源代理必须正确:AT模式的核心是数据源代理,Spring Boot 3.x中Seata会自动代理数据源,但若业务代码中自定义了DataSource Bean,必须手动配置Seata的DataSourceProxy,否则无法拦截SQL生成undo_log,导致分布式事务完全失效。
- 业务表主键强制要求:业务表必须有显式的主键,严禁使用无主键表,否则Seata无法准确定位数据行,无法生成正确的前镜像与后镜像,最终导致回滚失败、数据不一致;不建议使用联合主键,联合主键会增加镜像生成的复杂度,容易出现解析异常。
- undo_log表运维:undo_log表必须设置定时清理任务,生产环境建议每日清理7天以上的已完成事务的undo_log数据,避免表数据量过大导致的查询、回滚性能下降,甚至数据库磁盘空间耗尽;必须给undo_log表的
log_created字段建立索引,提升清理任务的执行效率。 - 脏写问题规避:严禁通过非Seata代理的数据源操作AT模式管理的业务表,否则会绕过Seata的全局锁检查,直接修改数据库数据,导致脏写、数据不一致;即使是数据订正、离线任务,也必须通过Seata代理的数据源执行操作。
- SQL支持限制:AT模式不支持动态表名、存储过程、多表关联更新、INSERT INTO ... SELECT等复杂SQL,此类SQL无法被Seata正确解析,会导致undo_log生成失败,事务回滚异常;生产环境使用前必须对业务SQL进行充分测试。
- 全局锁粒度优化:严禁大事务、长事务,全局锁的持有时间越长,并发性能越差,死锁概率越高;建议将非事务性操作(如参数校验、非核心查询、文件操作)提前到全局事务之外执行,缩小全局事务的范围,减少全局锁的持有时间。
4.3 TCC模式核心避坑
- 三大核心问题必须处理:
- 幂等性:Confirm和Cancel阶段会被TC重复调用,必须通过事务控制表记录分支事务的执行状态,重复调用时直接返回,严禁重复执行业务逻辑,否则会导致数据重复扣减、重复补偿。
- 空回滚:Cancel阶段在Try阶段未执行的情况下被调用,必须在事务控制表中校验,若没有Try阶段的执行记录,直接返回成功,不执行回滚逻辑,避免抛出异常导致TC无限重试。
- 事务悬挂:Cancel阶段比Try阶段先执行,导致Try阶段预留的资源永远无法释放;解决方案是Cancel阶段执行时,若没有Try阶段的记录,插入一条状态为已回滚的记录,Try阶段执行时,若发现已存在回滚记录,直接不执行资源预留逻辑。
- 三个阶段事务隔离:Try、Confirm、Cancel三个阶段必须使用独立的本地事务,严禁将多个阶段的逻辑放在同一个事务中,否则会导致事务状态异常。
- 异常处理规范:Try阶段业务校验失败时,必须抛出明确的业务异常,严禁返回null或false,否则TC会认为分支事务执行成功,不会触发Cancel阶段,导致数据不一致。
4.4 微服务调用链路避坑
- XID传播必须保证:Seata的XID必须在微服务调用链路中正确传播,Feign、Dubbo框架Seata已提供自动适配,但若使用自定义RPC框架、跨线程池执行任务,必须手动传递XID,通过请求头、ThreadLocal传递,否则分支事务无法加入全局事务,导致事务失效。
- 全局事务超时设置:全局事务的超时时间必须大于所有分支事务的总执行时间,包括服务调用的超时时间、数据库执行时间,否则会出现分支事务还在执行,TC已经发起全局回滚,最终导致数据不一致。
- 异常处理规范:
@GlobalTransactional注解必须指定rollbackFor = Exception.class,否则非RuntimeException不会触发全局回滚。- 业务代码中严禁捕获异常后不抛出,必须将异常向上抛出,否则Seata无法感知业务异常,不会触发全局回滚,导致数据不一致。
- 分支事务执行失败时,必须抛出RuntimeException,严禁返回错误码,否则Seata会认为分支事务执行成功,不会触发全局回滚。
4.5 版本与运维避坑
- 版本兼容要求:Seata客户端与TC服务端的大版本必须保持一致,严禁跨大版本混用,否则会出现协议不兼容、事务请求解析失败、事务状态异常等问题,甚至导致数据丢失。
- 监控体系搭建:生产环境必须搭建Seata监控体系,核心监控指标包括:全局事务提交率、回滚率、超时率、分支事务平均执行时间、TC节点CPU/内存/连接数、undo_log表数据量;通过监控及时发现异常事务、性能瓶颈,避免故障扩大。
- 死事务处理:生产环境会出现悬挂的死事务(如TC宕机、网络中断导致的事务状态未知),必须定期扫描TC的全局事务表,处理超过24小时的未完成事务,根据分支事务的执行状态,手动发起提交或回滚,避免全局锁长期持有、数据长期冻结。
- 灰度发布规范:Seata相关的代码变更、版本升级,必须进行灰度发布,先在小范围节点验证,确认事务执行正常后再全量发布,避免全量发布后出现大规模事务失效问题。
五、总结
Seata作为轻量级分布式事务框架,通过清晰的角色划分、灵活的事务模式,解决了微服务架构下的分布式事务核心痛点。AT模式实现了业务代码零侵入,大幅降低了分布式事务的接入成本;TCC、SAGA、XA模式则覆盖了更多特殊的业务场景,满足不同的一致性与性能需求。
生产环境落地Seata的核心,不仅是完成代码接入,更重要的是理解其底层的两阶段提交、全局锁、undo_log机制,提前规避本文梳理的各类坑点,做好TC集群高可用、参数调优、监控运维、事务异常处理等工作,才能保证分布式事务的稳定运行,最终实现业务数据的一致性。