恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Seata+SpringBoot实战:微服务分布式事务原理与踩坑指南

  • 首页
  • 资讯中心
  • /
  • Seata+SpringBoot实战:微服务分布式事务原理与踩坑指南

相关资讯

基于主从博弈的电热综合能源系统动态定价与能量管理Matlab实现 2026/9/10 1:09:58
Spring Boot多环境配置实战:彻底解决开发与生产环境切换难题 2026/9/10 1:09:58
9款降AI率工具实测对比:从原理到选型,帮你告别AI检测焦虑 2026/9/10 1:04:58

最新资讯

豆包工作AI生成PPT实操教程:从Prompt设计到避坑指南
OpenCode 如何把会话 /share 分享成公开链接,并切换 share 的 manual、auto 与 disabled 模式
深入理解进程就绪状态:从CPU调度到系统性能排查实战
DeepSeek V4接入Claude Code实战:编程Agent平替方案与性能评测
AI视觉开发的工业落地路径:从算法到运动控制的闭环实践
CANN/ge可选输入图构建示例

今日推荐

AI搜索重构内容生态:企业从“流量争夺”转向“答案共建”
AI搜索的信任缺口:企业内容如何在答案时代自证可信
Spring Boot+Vue+Node.js售后服务系统开发实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

Seata+SpringBoot实战:微服务分布式事务原理与踩坑指南

发布时间:2026/9/10 1:09:58
Seata+SpringBoot实战:微服务分布式事务原理与踩坑指南 上周在压测环境排查一个订单服务的问题压测跑到一半发现订单表和库存表对不上账订单明明创建成功了库存却扣了两份另一笔反过来库存扣了订单没建出来。两边数据不在同一个库里本地事务根本管不过来。后来翻日志问题全卡在分布式事务这一层——要么接口超时要么某个服务抛了异常但异常并没有让整条调用链一起回滚。这种场景在微服务团队里太常见了。今天直接拿最经典的“下单扣库存”场景用 Seata SpringBoot 从零搭一套分布式事务方案把原理、配置、代码、踩坑一次讲清楚。适合正在做微服务改造、被分布式事务折磨到怀疑人生的后端同学也适合准备分布式事务面试的 Java 工程师。1. 分布式事务为什么难搞一个下单扣库存的典型事故1.1 单体时代的本地事务为什么迁到微服务就失灵了在单体应用阶段订单表和库存表在同一数据库里一个下单方法内部改两张表代码上只需要在方法上标记Transactional数据库的本地事务就能保证原子性。要么订单和库存一起成功要么一起失败完全没有一致性的烦恼。MySQL 的 InnoDB 引擎通过 undo log 和 redo log 实现了 ACID这套机制在单库场景下是非常可靠的。微服务拆分之后情况完全变了。订单归订单服务管库存归库存服务管两个服务各连各的数据库。一个下单请求过来订单服务先往订单库里插一条记录然后通过 Feign 或 HTTP 调用库存服务的扣减接口这条调用链跨越了数据库、网络、进程三个边界。此时订单服务上的Transactional只能管住本地订单库的事务根本管不到库存服务那边的提交状态。一旦库存服务抛异常、接口超时或者网络抖动就会出现下面这两种让人头疼的错误第一种是订单创建成功但库存没扣继续卖就会超卖第二种是库存扣了但订单没建成功用户钱扣了却没买到东西。这两种错误都会直接伤害业务而且靠单服务的日志还不太好排查。围绕这个问题行业内所有方案的本质都是想让多个数据库上的本地事务在逻辑上组合成一个“伪全局事务”。这中间需要协调者、参与者、通信协议和补偿机制也就是分布式事务要解决的核心问题。1.2 一个下单请求背后到底经历了什么为了把问题说透我用一个具体的下单时序来描述。假设用户点了“立即购买”前端调用订单服务订单服务内部按顺序做这几件事在 order_tbl 表插入一条新建订单记录。调用库存服务接口传入商品编码和购买数量。库存服务在 storage_tbl 表执行库存扣减本地提交。库存服务返回成功。订单服务把本地事务提交返回下单成功。这个链路里第 1 步和第 3 步是两次独立的本地事务中间隔了一次 RPC。如果第 3 步扣减库存成功但第 5 步订单本地事务提交失败那么库存少了订单却不存在用户没有买到商品这是典型的资金损失反过来如果第 1 步订单插入成功但第 3 步库存服务异常订单缴活但库存没扣商品可能超卖。很多时候为了防超卖库里往往还会加上库存字段的判断条件比如update storage_tbl set count count - ? where commodify_code ? and count ?。这条 SQL 确实能防止超卖但无法解决“订单成功库存没扣”这个跨库一致性问题。我当时排查压测环境里那个问题时数据库里已经是两个服务各执一词的状态。更麻烦的是因为压测并发高现场日志里很多请求都报超时但超时之后订单服务有的重试了有的没重试重试的那批库存又被多扣了一次。这种“超时 重试 分布式事务缺失”的组合拳打出来的数据问题最隐蔽也最难修复。1.3 CAP 与 BASE分布式事务的理论边界聊分布式事务之前必须先理解两个基础理论CAP 和 BASE。CAP 理论告诉我们在分布式环境下分区容错性P是必须满足的因为网络分区总会发生所以团队只能在一致性C和可用性A之间做取舍。传统强一致的方案通常牺牲可用性比如数据库层面的 XA 协议它要求在事务提交过程中所有资源都可用否则整个事务就一直挂起这在互联网高并发场景下很容易把系统拖垮。BASE 理论是另一种思路基本可用、软状态、最终一致。它不要求任何时刻数据都强一致而是允许系统在一段时间内处于中间状态通过补偿机制最后达到一致。分布式事务的主流方案包括 Seata 的 AT 模式、TCC、Saga、本地消息表本质都是在 BASE 理论的框架下做最终一致性只是实现方式和强度不同。理解了这一层后续看到 Seata 里“一阶段先提交分支事务二阶段通过镜像回滚”的设计就不会觉得奇怪了它是为了保证可用性把一致性延后到提交阶段来保证。2. 不只有 Seata主流方案对比与选型逻辑2.1 先聊方案2PC、TCC、Saga、本地消息表在决定用 Seata 之前我先把市面上常见的分布式事务方案梳理了一遍因为不同方案对应不同场景用错了地方是要出大事的。方案一致性业务侵入性能适用场景XA / 2PC强一致低差锁资源时间长传统企业级系统低并发TCC最终一致高每接口写 Try/Confirm/Cancel 三套逻辑中资金类、金融类核心链路Saga最终一致中需要补偿逻辑较高长流程、多步骤业务本地消息表最终一致中需处理消息幂等高可容忍延迟的异步场景Seata AT最终一致低注解即可较高大部分微服务业务场景XA 协议是数据库原生支持的官方就叫强一致性分布式事务。它的问题是太“笨重”事务执行期间所有参与数据库的行都要锁住提交前不能释放。我早年在一个订单项目里试过 XA并发稍微一上来数据库连接池直接被打满因为全局事务把连接占用时间拉得很长。TCC 是业务级别的两阶段提交Try 阶段预占资源Confirm 阶段确认Cancel 阶段补偿。它的一致性控制最精细但工作量大到让人崩溃。一个下单接口你得额外写库存预扣、库存确认扣减、库存释放三套逻辑。业务逻辑一变三套代码同步改。我们当时评估下来核心支付链路用 TCC 可以接受但普普通通的订单、购物车、积分这种业务全用 TCC项目根本排期排不过来。Saga 是把一个长事务拆成多个本地短事务每个短事务都有对应的补偿事务。它的优点是每个短事务都能立即提交锁时间短但中间状态对外是可见的。比如下单和发优惠券如果下单成功、发券失败系统会执行“取消订单”这个补偿逻辑。问题是Saga 需要你自己设计补偿逻辑而且流程编排多了之后理解整个事务链路会变得比较费劲。本地消息表则通过数据库表和消息队列配合实现最终一致性。核心思路是把“发送消息”和“业务操作”放在同一个本地事务里然后消息消费者保证幂等。这个方案很可靠但只能解决“异步最终一致”的问题不适用于用户同步感知的强交互场景。2.2 为什么最终选择了 SeataSeata 进入我们的视野是因为它主打“对业务代码侵入性小”。使用 AT 模式时核心业务代码几乎不用动只要在全局事务发起方的方法上添加一个GlobalTransactional注解数据源被 Seata 自动代理后它就能拦截到 SQL自动生成前后镜像、自动注册分支事务、自动提交或回滚。当时团队里不少人一开始是抗拒引入新框架的觉得又多了一个重依赖多了一个服务要运维。但真正跑下来之后发现Seata 的使用成本和理解成本都比 TCC 低很多。对于大多数业务场景订单、库存、积分、优惠券、支付流水AT 模式提供的一致性已经足够。只有在极少数资金类强一致场景我们才会用 TCC 作为补充。这也是我后来给别人分享时特别强调的一个点Seata 不是万能的但它能覆盖 80% 以上的分布式事务需求性价比最高。另外Seata 本身就是一个开源生态在 GitHub 上有足够的社区活跃度遇到问题基本都能搜到解决方案不会出现“框架出 Bug 只能自己啃源码”的尴尬。2.3 Seata 的架构角色与 AT 模式运行原理Seata 里有三个核心角色和传统 2PC 的协调者、参与者非常像但对使用者更友好TCTransaction Coordinator事务协调者独立部署的服务负责维护全局事务状态、协调分支事务提交或回滚。TMTransaction Manager事务管理器嵌入在业务发起方服务里负责开启全局事务、提交或回滚全局事务。RMResource Manager资源管理器嵌入在参与事务的分支服务里负责管理分支事务操作注册分支事务上报分支执行结果。AT 模式的执行过程简单说分“两阶段”第一阶段事务发起方通过 TM 向 TC 申请全局事务TC 生成一个全局唯一 XID。这个 XID 会沿着调用链传递从订单服务传到库存服务。每个参与服务在执行本地 SQL 时数据源代理会拦截 SQL生成执行前镜像before image和执行后镜像after image并把这两份镜像连同分支事务信息一起写入 undo_log 表然后在同一个本地事务里执行业务 SQL 并提交。注意这里分支事务是本地提交的不像 2PC 那样先预提交。第二阶段有两种结果。如果全局事务最后要提交TC 通知所有分支提交各 RM 只需异步删除自己的 undo_log 记录如果某个分支失败导致全局事务要回滚TC 通知所有分支回滚各 RM 会根据 undo_log 里的 before image 生成反向 SQL把数据恢复成执行前的样子。这套设计的巧妙之处有两个。第一一阶段本地提交意味着数据库连接能被快速释放并发能力比 XA 强很多第二基于镜像回滚业务代码本身不需要写补偿逻辑这就是“低侵入”的来源。但它也有代价为了在回滚时能准确地恢复数据Seata 在执行 SQL 时会在数据行上加全局锁避免其他全局事务并发修改同一行数据。这个全局锁的事务隔离级别默认比较低后面我会专门讲怎么调整。3. Seata SpringBoot 落地实战订单库存全局事务从头搭3.1 版本选型与环境准备版本选择是新手最容易踩坑的地方。Seata、SpringBoot、Spring Cloud、Spring Cloud Alibaba 之间相互有兼容性要求乱配很容易出现一启动就报类找不到、配置不识别的问题。我在写这套示例时用的组合是经过多次验证的稳定版本组件版本JDK1.8Spring Boot2.3.7.RELEASESpring CloudHoxton.SR9Spring Cloud Alibaba2.2.5.RELEASESeata Server1.3.0MySQL5.7 或 8.0需要说明的是Spring Cloud Alibaba 2.2.5.RELEASE 内置了 Seata 1.3.0 的客户端所以不需要再单独引入 seata-spring-boot-starter避免版本冲突。如果项目需要在更高版本的 Spring Boot 下运行建议查一下 Seata 官方仓库的版本兼容表别直接照搬老项目的依赖。环境方面本机需要装好 JDK 1.8、MySQL、Maven以及一个可用的 IDE。我这次演示采用 Seata 的 file 模式做注册和配置中心目的是让整个流程最简单、最容易跑通。生产环境建议把 Seata 的注册中心和配置中心都切换到 Nacos原理类似只是把本地配置文件搬到了配置中心里。3.2 启动 Seata Serverfile 模式先从 Seata GitHub Releases 页面下载 seata-server-1.3.0 的压缩包解压到服务器或本机目录。启动之前需要简单修改两个文件registry.conf 和 file.conf。registry.conf 里把注册中心和配置中心的类型都改成 fileregistry { type file file { name file.conf } } config { type file file { name file.conf } }file.conf 里重点配置事务分组映射和 TC 服务地址示例配置如下service { vgroupMapping.my_test_tx_group default default.grouplist 127.0.0.1:8091 enableDegrade false disableGlobalTransaction false }这里的vgroupMapping.my_test_tx_group含义是客户端自己定义了一个事务分组叫my_test_tx_group把这个分组映射到名为default的 TC 集群上而default集群的服务地址就是本机的 8091 端口。事务分组机制的好处是客户端不用关心 TC 实际地址只要配置一个逻辑分组名就行后续切换集群环境非常方便。配置文件改好后进入 bin 目录启动sh seata-server.sh -p 8091 -h 127.0.0.1 -m file参数-p指定端口-h指定注册 IP-m file指定 Seata Server 自身的事务日志存储方式为文件模式。如果为了生产环境高可用建议把-m改成db把事务日志持久化到数据库这样 TC 重启后全局事务状态不丢。启动成功后日志里会出现类似Server started successfully ...的输出说明 TC 已经就绪。3.3 初始化业务库与 undo_log 表接下来在 MySQL 里创建两个业务库一个给订单服务一个给库存服务。这里特意保持两个库互相独立模拟真实微服务拆分后的数据库结构。订单库初始化脚本CREATE DATABASE seata_order DEFAULT CHARACTER SET utf8mb4; USE seata_order; CREATE TABLE order_tbl ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id varchar(32) DEFAULT NULL, commodity_code varchar(32) DEFAULT NULL, count int(11) DEFAULT 0 COMMENT 数量, money int(11) DEFAULT 0 COMMENT 金额, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENTSeata回滚日志表;库存库初始化脚本CREATE DATABASE seata_storage DEFAULT CHARACTER SET utf8mb4; USE seata_storage; CREATE TABLE storage_tbl ( id bigint(20) NOT NULL AUTO_INCREMENT, commodity_code varchar(32) DEFAULT NULL, count int(11) DEFAULT 0 COMMENT 库存, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT库存表; INSERT INTO storage_tbl (commodity_code, count) VALUES (P001, 100); CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENTSeata回滚日志表;这里有一个特别重要的细节undo_log 表必须建在每一个参与分布式事务的业务库中不能只在某个库建。因为 AT 模式回滚时是每个分支事务自己读取自己数据库里的 undo_log 来生成回滚 SQL。如果某个库里没有这张表全局回滚时 TC 会收到该分支回滚失败的错误你会看到满屏的Failed to rollback日志。这个坑我见过很多次尤其是新同事接手项目时只照抄了业务表漏了 undo_log。3.4 订单服务与库存服务核心代码我创建了两个 SpringBoot 工程分别叫 order-service 和 storage-service。order-service 端口 8081storage-service 端口 8082两个服务都用 MyBatis-Plus 操作数据库用 OpenFeign 做服务间调用。pom.xml 里的核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.7.RELEASE/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId version2.2.5.RELEASE/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.4.3.4/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.25/version /dependency /dependenciesorder-service 的 application.ymlserver: 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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: file config: type: file feign: client: config: default: connectTimeout: 5000 readTimeout: 5000注意seata.tx-service-group配置成了my_test_tx_group这个值必须和前面 file.conf 中vgroupMapping的 key 对应。如果两者不一致客户端启动时会报no available service的错误因为它根据事务分组找不到可用的 TC 服务地址。这是新手最容易犯的错误之一。storage-service 的 application.yml 和 order-service 类似只是端口改成 8082数据库连接指向 seata_storage。两个服务的 resources 目录下都要放一份相同的 registry.conf 和 file.conf。虽然这看起来有些重复但每个服务是独立进程Seata 客户端启动时都需要加载这些配置所以必须各自带一份。用一个简单的 Feign 接口作为订单服务和库存服务之间的桥梁FeignClient(name storage-service, url http://127.0.0.1:8082) public interface StorageClient { PostMapping(/storage/deduct) String deduct(RequestParam(commodityCode) String commodityCode, RequestParam(count) Integer count); }为了让演示更简单这里直接用 URL 指定库存服务地址。实际生产环境会结合 Nacos 或 Consul 做服务发现用服务名代替 URL。订单服务核心业务代码Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private StorageClient storageClient; Override GlobalTransactional(name create-order, rollbackFor Exception.class) public void createOrder(String userId, String commodityCode, int count, int money) { // 1. 本地业务插入订单 Order order new Order(); order.setUserId(userId); order.setCommodityCode(commodityCode); order.setCount(count); order.setMoney(money); orderMapper.insert(order); // 2. 远程调用库存服务扣减库存 storageClient.deduct(commodityCode, count); // 3. 模拟业务校验异常下单数量大于10时触发全局回滚 if (count 10) { throw new RuntimeException(下单数量过大触发全局回滚); } } }GlobalTransactional注解是整个全局事务的入口。TM 在有这个注解的方法执行前会向 TC 申请全局事务并把 XID 绑定到当前线程。方法内所有参与服务的 SQL 操作都会作为该全局事务的分支来管理。rollbackFor Exception.class表示任意异常都触发回滚这里一定要设置避免只对 RuntimeException 生效的默认行为漏掉受检异常。库存服务核心代码RestController RequestMapping(/storage) public class StorageController { Autowired private StorageService storageService; PostMapping(/deduct) public String deduct(RequestParam(commodityCode) String commodityCode, RequestParam(count) Integer count) { storageService.deduct(commodityCode, count); return success; } }Service public class StorageServiceImpl implements StorageService { Autowired private StorageMapper storageMapper; Override public void deduct(String commodityCode, int count) { Storage storage storageMapper.selectByCommodityCode(commodityCode); if (storage.getCount() count) { throw new RuntimeException(库存不足); } storageMapper.deduct(commodityCode, count); } }这里deduct方法并不需要加GlobalTransactional因为它已经作为订单服务全局事务的一个分支存在。如果你在库存服务上也加了GlobalTransactional就等于嵌套开启了一个新的全局事务反而会造成事务边界混乱。记住一个原则全局事务注解只需要加在最开始触发业务链路的方法上子服务作为参与者即可。3.5 数据源代理与 XID 传递机制Seata 客户端之所以能拦截 SQL 并生成镜像是因为它需要拿到真正执行的 SQL才能解析前后镜像。实现方式是使用DataSourceProxy包装我们配置的原始数据源。如果是用spring-cloud-starter-alibaba-seata依赖这个自动配置已经帮你完成了不需要手动写配置类。但如果你用的是原生seata-spring-boot-starter则需要手动加一个配置类Configuration public class SeataDataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource) public DataSource dataSource() { return new DruidDataSource(); } Primary Bean(dataSourceProxy) public DataSource dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } }这个操作很关键因为如果数据源没有被代理Seata 根本不知道分支事务在执行什么 SQLGlobalTransactional注解就形同虚设。理解这一步也能解释很多“为什么我加了注解但回滚不生效”的问题。XID 的传递是分布式事务能不能串起整条链路的关键。Seata 客户端在发起 RPC 调用时会从当前线程上下文RootContext中取出 XID放到请求头TX_XID中传递到被调用方。被调用方收到请求后从请求头解析 XID并绑定到自己的线程上下文。spring-cloud-starter-alibaba-seata已经集成了 Feign 和 SpringMVC 的 XID 传递拦截器所以你只要使用 Feign 和标准 SpringMVC 接口不需要写额外代码。但如果你的链路中用到了 RestTemplate、Apache HttpClient或者自定义了 RPC 框架就需要自己写拦截器把 XID 塞进请求头这是很多自研框架接入 Seata 时最容易漏掉的一环。4. 故障演练库存不足、服务崩溃如何回滚4.1 正常链路订单和库存同时落库先把 Seata Server、order-service、storage-service 三个进程都启动起来。然后调用下单接口curl -X POST http://127.0.0.1:8081/order/create?userIdU001commodityCodeP001count2money100正常情况下返回下单成功。接着查询两个库。订单库SELECT * FROM order_tbl;结果会看到 user_id 为 U001 的一条订单记录。库存库SELECT * FROM storage_tbl;结果会看到商品 P001 的库存从 100 变成了 98。这说明订单服务和库存服务在两个独立的数据库上都完成了写入而且这两个写入被组合到了同一个全局事务里没有出现任何一边缺失的问题。这时候去 Seata Server 的日志里看能看到全局事务创建、分支事务注册、分支提交的过程。TC 的角色在这个时候像一个账房先生记录每个分支事务的执行结果最后统一宣布提交或回滚。4.2 库存不足全局事务自动回滚接下来故意制造异常。先把 P001 的库存改成 5UPDATE storage_tbl SET count 5 WHERE commodity_code P001;然后调用下单接口数量传 10curl -X POST http://127.0.0.1:8081/order/create?userIdU002commodityCodeP001count10money50库存服务在deduct方法中检测到库存不足抛出RuntimeException。这个异常会沿着 Feign 调用链传回订单服务。由于GlobalTransactional开启了全局事务订单服务捕获到异常后通过 TM 向 TC 发起全局回滚。此时去看订单库会发现没有新增任何 user_id 为 U002 的订单记录。但奇怪的是订单服务明明是先执行了orderMapper.insert(order)再调用的库存服务按道理订单应该已经写进去了。为什么订单表没数据原因就是 Seata 的 AT 模式在回滚时根据 undo_log 里的 before image 生成了 delete 语句把之前插入的订单数据删掉了。这正是镜像回滚的威力。再看库存库库存依然是 5没有被改成负数。整个全局事务像没发生过一样所有数据都恢复到了执行前的状态。通过这个验证你能直观感受到 Seata 在异常场景下“自动补偿”的能力。4.3 服务宕机与超时TC 如何兜底生产环境的故障往往不只是业务异常还有服务宕机、网络超时等更复杂的情况。这里说两个典型案例。第一个案例库存服务扣完库存后在返回结果前进程突然宕机。此时分支事务本身已经在库存库本地提交了但 TC 还没收到分支完成的通知。TC 会一直持有这个全局事务的状态直到分支事务的超时时间到达然后触发全局回滚。回滚时库存服务已经恢复RM 会读取库存库里的 undo_log把已经扣减的库存恢复原样。需要注意Seata 的 AT 模式依赖本地 undo_log所以只要业务库没有丢数据回滚就有依据。第二个案例库存服务响应很慢Feign 客户端读超时触发异常。比如我们在deduct方法里故意加一个Thread.sleep(10000)同时把 Feign 的 readTimeout 设置成 2000 毫秒。订单服务这边 Feign 会抛出超时异常进而触发全局回滚。但库存服务那边的事务可能还在正常执行最终库存被扣减并提交了。如果此时全局事务已经回滚就会出现矛盾库存扣了订单没了。Seata 怎么处理这种情况呢其实关键在于 TC 的最终一致性判定。当订单服务发起回滚时TC 会通知所有已知分支回滚。如果库存分支还没有完成TC 会等它完成后再执行回滚。实际测试中你会发现库存服务睡眠结束后它的本地事务执行完但随后会收到 TC 的回滚指令根据 undo_log 把库存恢复。所以最终库存和订单都回到了初始状态。这让我意识到一个重要问题分布式事务的“回滚”不一定即时完成它可能需要等待某些慢分支的结束。因此在设计接口时一定要设置合理的 RPC 超时时间并配合 Seata 的事务超时参数。如果 RPC 已经超时重试多次分支事务还一直不结束TC 也只能一直持有全局锁影响后续其他事务的并发操作。5. 踩坑清单、性能调优与 Seata 面试要点5.1 高频踩坑实录我把自己和团队在实际接入 Seata 过程中遇到的典型问题整理成了速查表后面再遇到类似问题可以直接照着排查。现象原因解决方法启动报no available service客户端事务分组与 TC 配置不一致检查 application.yml 的 tx-service-group 和 file.conf 的 vgroupMapping 是否一致回滚失败日志报找不到表业务库缺少 undo_log 表在每个参与分布式事务的业务库中执行 undo_log 建表 SQLGlobalTransactional不生效异常不回滚数据源没有被 DataSourceProxy 代理确认是否使用了 seata 的自动代理或手动配置 DataSourceProxyFeign 调用后 XID 丢失自定义了 Feign 拦截器覆盖了默认 TX_XID 头在自定义拦截器中保留 RootContext.getXID() 到请求头全局事务一直阻塞直到超时分支服务 RPC 响应过慢或锁等待时间过长调整 RPC 超时、Seata 锁重试参数和全局事务超时时间回滚后自增 ID 跳段属于正常现象undo_log 回滚不会重置自增无需处理一阶段提交成功但最终全局回滚这是 AT 模式正常流程只要 undo_log 有数据且业务库正常最终会恢复5.2 性能调优与隔离级别Seata 的 AT 模式并非没有成本。它最大的额外开销来自全局锁和镜像日志。在低并发场景下几乎感觉不到但压测到几百并发时就要认真对待几个参数了。第一个是锁等待时间。Seata 客户端默认锁重试在client.rm.lock.retryInterval和client.rm.lock.retryTimes下控制默认重试 30 次间隔 10 毫秒。如果并发环境下多个全局事务同时操作同一行数据后到的事务会一直尝试获取全局锁直到超时。这个等待过程会拖慢接口响应所以要在压测时观察是否出现大量锁等待超时再适当调大重试次数或者优化事务边界让事务缩短一点。第二个是事务超时时间。全局事务默认超时时间由client.tm.timeout控制默认 60 秒。实际业务里一个同步调用的下单接口不应超过几秒钟60 秒已经足够宽松。但如果你的接口有复杂的异步逻辑千万记住全局事务超时了并不是说自动回滚而是 TC 会标记该全局事务为超时状态事务管理器才能对超时状态做出响应。真实的处理逻辑比较复杂建议把超时时间设置得比接口期望响应时间大一些避免业务还在执行事务已经被判定超时。第三个是隔离级别。Seata AT 模式默认隔离级别是读未提交这意味着在一个全局事务尚未提交时其他事务可能读到这个事务已经本地提交但最终要回滚的数据也就是脏读。对大多数业务来说这个风险可以接受但如果真的不能接受可以在查询方法上添加GlobalLock注解或者用SELECT ... FOR UPDATE把查询也纳入全局锁控制升级到读已提交的隔离效果。不过这样会增加数据库锁竞争性能会有损失需要业务侧评估。在进行性能调优时还有一点容易被忽视不要在整个调用链路的每个服务里都开启大规模本地事务。分布式事务是把多个本地事务组装在一起每个本地事务内部的操作越多、执行时间越长整体锁的持有时间就越长并发能力也就越低。最好的做法是让每个参与服务的方法只做“一件事”比如订单服务只插订单表、库存服务只扣库存表把大事务拆小。5.3 Seata 面试高频问题与回答思路面试官问分布式事务时通常不只是问“用过没有”而是会顺着你提的技术点往深里挖。我列几个高频问题并附上比较稳妥的回答思路。问Seata 的 AT 模式实现原理是什么回答思路先讲三个角色 TC、TM、RM然后讲一阶段生成 before image 和 after image并在本地事务中写入 undo_log提交分支事务二阶段根据全局事务结果决定异步删除 undo_log 还是根据 undo_log 反向生成 SQL 回滚。最后强调它通过数据源代理拦截 SQL所以对业务代码侵入小。问AT 模式一阶段为什么可以直接提交本地事务它不怕后续回滚吗回答思路因为一阶段已经把执行前镜像和执行后镜像都保存到了 undo_log并且和业务 SQL 在同一个本地事务里提交。二阶段如果需要回滚可以依据 before image 生成补偿 SQL把数据恢复原样。这种设计牺牲了一点存储空间和回滚时额外的开销但换来了数据库连接快速释放提升了并发能力。问Seata 的全局锁是什么存放在哪里回答思路全局锁是 Seata 在 TC 端维护的一种分布式行锁用于防止多个全局事务对同一行数据的并发修改造成脏写。分支事务在执行写操作前会申请全局锁获取不到则重试直到超时。它存储于 TC 的锁表或内存中取决于 TC 使用的存储模式。数据库全局锁的具体映射关系会注册在 TC 端而实际业务数据上的本地锁仍然由数据库锁机制保证。问GlobalTransactional应该加在哪个方法上回答思路加在事务发起方最外层的方法上表示开启全局事务。子服务的方法作为分支参与全局事务不需要重复添加。如果子服务自身有独立逻辑需要单独开启全局事务那是另一种场景需要重新发起一个全局事务。问XID 在服务间是如何传递的回答思路在发起 RPC 调用时从RootContext.getXID()获取当前线程绑定的 XID放入请求头TX_XID。被调用方从请求头解析 XID绑定到自己的线程上下文实现跨服务传递。如果自己封装了 RPC 框架需要手动实现这种传递。问你们项目为什么用 Seata 而不是 TCC回答思路从业务侵入度和开发成本角度回答说明大多数业务场景对事务实时性要求没有达到强一致级别AT 模式足够满足同时开发和维护成本更低。再适当点出 TCC 的适用场景比如资金支付类业务中要求幂等严格、需要自定义补偿逻辑的地方。这样的回答既展示了方案对比能力也体现了技术选型的思考深度。6. 写在最后几个实操中形成的习惯分布式事务不是一个能拿来就用的利器它更像是一把需要用熟练才能发挥效力的工具。我在实际项目中养成了几个小习惯最后分享给你。第一在系统设计阶段就要评估哪些链路真的需要分布式事务不要“一刀切”地把所有跨服务调用都包装成全局事务。很多场景用消息队列异步补偿、或者让用户在下一次查询时做对账修复成本更低、体验更好。像订单主流程这种用户实时感知、数据强相关的场景才值得引入全局事务。第二凡是加了GlobalTransactional的接口上线之前一定要做一次异常演练。不只是简单地把某个服务停掉而是模拟网络超时、数据库连接池打满、下游服务重启这些真实故障。我在压测环境里就遇到过库存服务明明已经恢复但因为 undo_log 里的数据被其他任务清掉了导致回滚时找不到镜像而失败。这类问题只有靠演练才能在事前暴露。第三监控 Seata 的关键指标尤其是事务提交成功率、回滚次数、全局锁等待时间。大促压测时这些指标能很直观地反映出哪些热点商品行被频繁争抢。如果某个商品的全局锁等待时间持续飙升说明这个商品的并发下单已经超过了分布式事务能承受的上限需要考虑从库存设计上做拆分或者引入其他手段降低事务冲突。分布式事务这个问题本质上没有银弹。Seata 也不是万能的但它确实是目前 Java 生态里性价比很高的解决方案。希望这份基于 Seata SpringBoot 的实战梳理能帮你少踩几个坑真正把分布式事务的原理和用法搞透。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号