前段时间一位读者跟我聊起他的面试经历:10年 Java 经验,去面广发银行的外包岗位,技术面从项目介绍开始,一路深挖到分布式事务,从 2PC 问到 TCC,从 Seata 问到 Saga,最后还追问了本地消息表和事务消息的区别。他觉得“招个外包而已,问这么深有必要吗”,但反过来想,这也说明银行类项目对数据一致性的重视程度远超普通互联网项目。
实际上,分布式事务是 Java 后端面试中“性价比”极高的一个考点。它能同时考察你的底层原理功底、中间件使用经验、业务设计能力和排查问题的思路。本文围绕分布式事务展开,结合面试中常见的追问方式,梳理核心概念、主流方案、Seata 实战、银行金融场景的特殊考法,最后给出高频问题和面试复盘建议。不管你是准备面试,还是在项目中需要落地分布式事务,这篇文章都值得认真读一遍。
1. 分布式事务:它到底在解决什么问题
1.1 从本地事务说起
在单机单库时代,事务很简单。以 MySQL 的 InnoDB 为例,一个事务要么全部提交,要么全部回滚,通过 undolog、redolog 和锁机制保证 ACID 特性。
-- 经典的转账场景:扣款 + 入账,必须在一个事务里 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE user_id = 1; UPDATE account SET balance = balance + 100 WHERE user_id = 2; COMMIT;这段 SQL 如果在执行第二条 UPDATE 时报错,事务会整体回滚,不会出现“钱扣了但没到账”的情况。这就是本地事务的强一致性。
但分布式场景下,事情变复杂了。所谓分布式事务,通俗地讲,就是一次业务操作需要跨多个数据库、多个服务、甚至多个系统共同完成,这些参与者之间的事务一致性需要额外机制来保证。
1.2 产生分布式事务的典型场景
举几个最常见的场景:
场景一:微服务拆分后的跨库操作
订单服务对应订单库,库存服务对应库存库,支付服务对应支付库。用户下单时要同时扣库存、创建订单、生成支付单,三个动作落在三个数据库里,任何一个失败都需要全局回滚。
场景二:多数据源操作
同一个应用里配置了多个数据源,一个 service 方法里先写 A 库再写 B 库,A 库提交成功、B 库异常,A 库的数据无法自动回滚。
场景三:异构系统集成
金融行业经常要对接外部渠道、第三方支付、银联等系统。你调用对方接口后,对方的结果是异步返回的,本地数据库事务已经提交了,无法和外部系统保持同一个事务边界。
1.3 分布式事务为什么这么难
本地事务靠的是数据库自身的锁和日志。分布式事务的核心难点在于:没有一种全局的、单一的控制点能让所有参与者做到原子提交与回滚。
具体表现为:
- 网络不确定性:协调者和参与者之间的通信可能超时、丢包,无法确定对方是成功还是失败。
- 资源隔离:各个服务和数据库是独立部署的,无法直接操作对方的本地事务。
- 状态不一致:某个参与者的本地事务提交了,但全局事务最终判定失败,需要把已提交的数据“补偿”回去。
- 性能损耗:越强的一致性方案,加锁时间越长,吞吐量越低。
正因为这些难点,分布式事务在业界没有“银弹”。不同场景要选择不同的一致性模型。
1.4 理解两个关键概念:强一致性与最终一致性
这是面试时概念层高频考察点。
强一致性是指任何一次读都能读到某个数据的最新写结果,例如单库事务、2PC 理想状态下的提交。在分布式环境下,强一致的代价通常是高延迟和低可用性。
最终一致性是指数据在一段时间内可能是不一致的,但在没有新的更新操作之后,经过一段时间,系统内所有副本最终会达到一致状态。这是大多数分布式事务方案的现实选择。
分布式事务方案的取舍,本质上是在“一致性强度”和“可用性/性能”之间做权衡。不存在既完全强一致、又高性能、还高可用的方案,面试官让你对比方案时,其实就是在问你看不看得到这个本质。
2. 主流分布式事务方案原理与对比
2.1 两阶段提交(2PC)
2PC 是最经典的强一致性方案,核心角色是协调者(Coordinator)和参与者(Participant)。
第一阶段:准备阶段(Prepare)
协调者向所有参与者发送 Prepare 请求,参与者执行本地事务但先不提交,并记录 undo/redo 日志,然后向协调者回复“可以提交”或“准备失败”。
第二阶段:提交/回滚阶段(Commit/Rollback)
协调者根据所有参与者的回复做决策:
- 全部回复成功 -> 发送 Commit,参与者正式提交。
- 任何一方失败或超时 -> 发送 Rollback,参与者回滚。
存在的主要问题:
| 问题 | 说明 |
|---|---|
| 同步阻塞 | 参与者执行 Prepare 后一直持有数据库资源锁,期间无法释放,吞吐量受限 |
| 单点故障 | 协调者宕机后,参与者无法决策,事务卡住 |
| 数据不一致 | Commit 阶段如果协调者只发出了部分 Commit,网络异常会导致部分提交部分未提交 |
2PC 的实现代表是 XA 协议。Java 生态里可以通过 JTA(Java Transaction API)配合 Atomikos、Bitronix 等事务管理器对接。但 XA 在微服务场景下用得不多,因为跨服务调用链路太长、锁时间太不可控。
2.2 三阶段提交(3PC)
3PC 是 2PC 的改进版,引入超时机制和准备提交(PreCommit)阶段,降低参与者的阻塞范围,解决协调者单点时参与者无法释放资源的问题。
它分成三步:CanCommit、PreCommit、DoCommit。3PC 的理论完整性优于 2PC,但工程实现复杂,实际生产中用得很少,面试时一般只需了解它解决了 2PC 的哪些问题即可。
2.3 TCC 模式
TCC 是一种应用层面的补偿方案,把每个业务操作拆成三个方法:
- Try:资源预留或业务检查。
- Confirm:确认执行,真正执行业务。
- Cancel:取消执行,释放 Try 阶段预留的资源。
以一个经典的“账户扣款 + 余额入账”转账场景为例:
try: 检查账户A余额是否充足 冻结账户A的100元(冻结金额=100,可用余额=可用余额-100) 检查账户B是否存在 预留账户B的入账记录(状态=待入账) confirm: 确认冻结:账户A冻结金额清理,实际扣减余额 确认入账:账户B的待入账记录状态改为已入账 cancel: 释放冻结:账户A冻结金额恢复为可用余额 取消入账:账户B的待入账记录状态改为已取消TCC 的优势是性能比 2PC 好,不需要持有数据库锁,最终一致性和业务可控性都较好。但难点在于:
- 业务侵入性强,每个事务性接口都要实现 Try/Confirm/Cancel 三个方法。
- 要处理空回滚问题:Try 还没执行到,Cancel 就已经被调用了。
- 要处理悬挂问题:Cancel 先执行完,Try 后到达。
- 要保证 Confirm 和 Cancel 的幂等性。
空回滚、悬挂、幂等是 TCC 面试中最常被追问的三板斧,后面实战部分会详细展开。
2.4 Saga 模式
Saga 的核心思想是:把一个长事务拆成一组有序的子事务,每个子事务都有对应的补偿事务(Compensation)。正常流程依次执行子事务;如果某个子事务失败,就反向执行之前已完成子事务的补偿事务。
Saga 有两种协调方式:
编排式(Choreography):每个服务在本地事务完成后,发出事件触发下一个服务,彼此通过消息总线协作。实现简单,但流程分散在多个服务里,不好追踪。
编制式(Orchestration):引入一个 Saga 执行器(Orchestrator),由它集中编排各个参与者的调用顺序和补偿逻辑。适合复杂流程,管控集中,但执行器本身需要做成高可用。
Saga 适用于流程长、中间状态多、可以接受最终一致性的场景,比如订单拆单、旅游预订、贷款审批流程等。它的业务侵入性比 TCC 低,因为不需要资源预留,只需要为每个子事务写好补偿方法。
2.5 本地消息表
这是早期非常经典的最终一致性方案。核心思路如下:
- 本地业务数据和消息记录在同一个本地事务中提交。
- 有一个后台任务定时扫描消息表,把未发送的消息发给 MQ。
- 消费者处理成功后向生产者回执,消息表里的消息标记为已确认。
- 消费者要保证幂等,因为消息可能重复投递。
-- 生产者本地同时写业务表和消息表 START TRANSACTION; -- 业务操作 UPDATE order SET status = 'PAID' WHERE order_id = 123; -- 写消息表 INSERT INTO local_message (msg_id, content, status, retry_count) VALUES ('uuid-123', '{"orderId":123}', 0, 0); COMMIT;后台任务扫描 status=0 的记录发送到 MQ,收到 ACK 后更新 status=1。如果发送失败或超时则重试。消费者取出消息,处理业务,记录消费状态。
这个方案的问题在于:消息表对业务表有侵入、需要自己维护重试和扫描任务、消息积压可能造成延迟。但它理解门槛低、不依赖额外中间件,很多传统企业项目中仍然能看到。
2.6 事务消息
RocketMQ 的事务消息是目前互联网公司最常用的最终一致性方案。它将“本地事务 + 消息发送”原子化,原理如下:
- 生产者发送半消息(half message),消息先到 MQ 但消费者不可见。
- 生产者执行本地事务。
- 本地事务成功 -> 提交半消息,消费者可见。
- 本地事务失败 -> 回滚半消息,消息被丢弃。
- 如果生产者执行本地事务后没来得及返回结果(宕机),MQ 会定时回查生产者,由生产者反查本地事务状态,决定提交还是回滚。
// RocketMQ 事务消息示例骨架 TransactionMQProducer producer = new TransactionMQProducer("tx-producer"); producer.setTransactionListener(new TransactionListener() { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { // 执行本地事务:写订单表 + 写库存扣减记录 boolean success = doLocalBusiness(arg); return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE; } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // MQ 回查本地事务结果 return checkLocalBusinessByMsgId(msg.getKeys()) ? COMMIT_MESSAGE : ROLLBACK_MESSAGE; } });事务消息避免了本地消息表对业务库的侵入,比本地消息表更优雅,但仍然需要消费者侧做幂等。
2.7 方案选型对比
| 方案 | 一致性 | 性能 | 业务侵入 | 主要适用场景 |
|---|---|---|---|---|
| 2PC/XA | 强一致 | 低 | 低(框架层面) | 单体多库、对延迟不敏感的内部系统 |
| TCC | 最终一致 | 较高 | 高 | 资金类、资源预留类业务,如支付、扣款 |
| Saga | 最终一致 | 较高 | 中 | 长流程、业务周期长的订单/审批流 |
| 本地消息表 | 最终一致 | 高 | 中 | 传统企业、无 MQ 事务能力 |
| 事务消息 | 最终一致 | 高 | 较低 | 互联网高并发下的下单、异步解耦 |
面试时如果能用自己的话把这套对比讲清楚,再结合项目经验说明选型理由,基本就能让你的面试表现超出大多数候选人。
3. Seata 分布式事务框架实战
3.1 Seata 核心概念
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源、目前国内使用最广的分布式事务框架。Seata 把一个全局事务拆成三个核心角色:
- TC(Transaction Coordinator)事务协调者:维护全局事务状态,负责协调分支事务的提交和回滚。
- TM(Transaction Manager)事务管理器:定义全局事务的边界,发起提交或回滚。
- RM(Resource Manager)资源管理器:管理分支事务的资源,向 TC 注册分支并报告状态。
Seata 支持多种事务模式,最常见的三种是 AT、TCC、Saga。其中 AT 模式对业务代码侵入最小,也是大多数项目选择 Seata 的原因。
3.2 AT 模式的运行机制
AT 模式与 XA 一样是“强一致”的,但为了避免 XA 的锁资源问题,AT 在三阶段上做了改良,核心思路是业务数据 + 前后镜像 + undo_log。
AT 模式一个分支事务的执行过程:
- 业务 SQL 执行前,RM 解析 SQL,生成该行的前镜像(查询原数据)。
- 执行业务 SQL。
- RM 生成后镜像(查询修改后的数据),并将前后镜像和 SQL 一起写入 undo_log 表。
- 分支事务注册到 TC,并提交本地事务(此时已经释放了本地锁)。
- 全局事务提交时,异步删除各分支的 undo_log。
- 全局事务回滚时,TC 通知各分支回滚,RM 根据 undo_log 中的前后镜像构造反向 SQL,恢复数据。
-- AT 模式要求每个业务表对应的分支事务库中必须有 undo_log 表 CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) 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(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';AT 模式的优势是开发成本低,只需要在全局事务入口加上@GlobalTransactional注解。它的局限在于:
- 只支持单库事务内的 SQL 回滚,不支持跨库 join 的场景。
- 对 SQL 有约束,DDL 操作、非标准 SQL 有可能不支持。
- 使用全局锁,高并发下热点数据仍是瓶颈。
3.2 环境准备
由于 Seata 版本更新较快,这里不写死具体版本,以常见稳定版本为准。你本地实践时注意检查版本兼容性。
准备清单:
- 一个可用的 Nacos(或 file 配置模式)做 Seata Server 的配置与注册中心。
- Seata Server 1.6+/1.7+,下载后启动。
- 三个 Spring Boot 服务:订单服务、库存服务、账户服务。
- 一个 MySQL 实例,创建三个业务库,每个库建对应的业务表和 undo_log 表。
建议动手前先把版本对应关系理清楚。Spring Cloud Alibaba、Spring Boot、Seata 三者的版本必须匹配,否则经常会出现No available service或can not connect to service之类的问题。
3.3 数据库与建表脚本
这里以“下单扣库存扣余额”的经典案例来演示。三个库分别是 seata_order、seata_stock、seata_account。
-- seata_order 库 CREATE TABLE `t_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `user_id` BIGINT DEFAULT NULL COMMENT '用户ID', `product_id` BIGINT DEFAULT NULL COMMENT '商品ID', `count` INT DEFAULT NULL COMMENT '数量', `amount` DECIMAL(10,2) DEFAULT NULL COMMENT '金额', `status` INT DEFAULT NULL COMMENT '订单状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- seata_stock 库 CREATE TABLE `t_stock` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `product_id` BIGINT DEFAULT NULL COMMENT '商品ID', `total_count` INT DEFAULT NULL COMMENT '总库存', `lock_count` INT DEFAULT NULL COMMENT '锁定库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- seata_account 库 CREATE TABLE `t_account` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT DEFAULT NULL COMMENT '用户ID', `balance` DECIMAL(10,2) DEFAULT NULL COMMENT '余额', `freeze_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '冻结金额', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;三个库都需要有一份 undo_log 表,SQL 在上文 3.2 小节已给出。
3.4 服务端配置要点
Seata Server 的配置方式比较多样。早期版本 conf 文件 + registry.conf 是常见套路,新版更推荐使用 Nacos 配置中心。
在 registry.conf 中指定注册中心和配置中心:
registry { type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" } } config { type = "nacos" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" dataId = "seataServer.properties" } }启动 Seata Server:
# 进入 Seata Server 解压目录 bin 下,Linux/Mac 执行 sh seata-server.sh -p 8091 -m file # Windows 执行 seata-server.bat -p 8091 -m file启动成功后,在 Nacos 服务列表里能看到seata-server服务,说明注册成功。
3.5 客户端集成
三个服务的依赖与配置基本一致,以订单服务为例。
第一步:引入依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>第二步: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&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 cloud: nacos: discovery: server-addr: 127.0.0.1:8848 alibaba: seata: # 事务分组,自定义即可,需与服务端配置一致 tx-service-group: my_test_tx_group seata: service: # 事务分组对应的 Seata 集群名称 vgroup-mapping: my_test_tx_group: default registry: type: nacos nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos config: type: nacos nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos注意tx-service-group和vgroup-mapping必须正确对应,这是很多初学者容易踩坑的地方。Seata 客户端启动时会根据事务分组找到真实的 TC 服务地址,找不到或者分组不对就直接报错。
第三步:编写业务代码
订单服务入口:
// 文件路径:order-service/src/main/java/com/example/order/OrderServiceImpl.java @Service public class OrderServiceImpl { @Autowired private OrderDao orderDao; @Autowired private StockFeignClient stockFeignClient; @Autowired private AccountFeignClient accountFeignClient; @Override @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class) public void createOrder(Order order) { // 1. 本地创建订单 orderDao.insert(order); // 2. 远程调用库存服务,扣减库存 stockFeignClient.deduct(order.getProductId(), order.getCount()); // 3. 远程调用账户服务,扣减余额 accountFeignClient.debit(order.getUserId(), order.getAmount()); // 4. 模拟异常,观察全局回滚 // int i = 1 / 0; } }库存服务接口:
// 文件路径:stock-service/src/main/java/com/example/stock/StockService.java @Service public class StockService { @Autowired private StockDao stockDao; @Transactional(rollbackFor = Exception.class) public void deduct(Long productId, Integer count) { // 使用乐观锁思想:只更新库存充足的商品,影响行数为 0 说明库存不足 int rows = stockDao.deductStock(productId, count); if (rows == 0) { throw new RuntimeException("库存不足"); } } }<!-- stock-service 的 mapper 示例 --> <update id="deductStock"> UPDATE t_stock SET total_count = total_count - #{count} WHERE product_id = #{productId} AND total_count >= #{count} </update>账户服务接口:
// 文件路径:account-service/src/main/java/com/example/account/AccountService.java @Service public class AccountService { @Autowired private AccountDao accountDao; @Transactional(rollbackFor = Exception.class) public void debit(Long userId, BigDecimal amount) { int rows = accountDao.debitBalance(userId, amount); if (rows == 0) { throw new RuntimeException("余额不足"); } } }<!-- account-service 的 mapper 示例 --> <update id="debitBalance"> UPDATE t_account SET balance = balance - #{amount} WHERE user_id = #{userId} AND balance >= #{amount} </update>第四步:运行验证
依次启动 Seata Server、Nacos、账户服务、库存服务、订单服务。调用下单接口:
curl -X POST http://127.0.0.1:8081/order/create \ -H "Content-Type: application/json" \ -d '{"userId":1,"productId":1,"count":1,"amount":100}'正常响应后,查看三张业务表数据,订单已创建、库存已扣减、余额已扣减。
然后在createOrder方法里打开注释掉的异常代码int i = 1 / 0;,重新调用。观察日志会发现 Seata 依次通知三个分支事务回滚。再次查询数据,订单、库存、余额都恢复到调用前的状态,说明 AT 模式的全局回滚生效了。
3.6 Seata TCC 模式落地要点
AT 模式虽然侵入小,但在资金类业务中,很多团队更倾向 TCC。Seata 对 TCC 也提供了原生支持,需要实现@TwoPhaseBusinessAction注解描述的 Try/Confirm/Cancel 方法。
// 文件路径:account-service/src/main/java/com/example/account/TccAccountService.java public interface TccAccountService { @TwoPhaseBusinessAction(name = "tccDebit", commitMethod = "confirmDebit", rollbackMethod = "cancelDebit") void tryDebit(BusinessActionContext context, @BusinessActionContextParameter(paramName = "userId") Long userId, @BusinessActionContextParameter(paramName = "amount") BigDecimal amount); boolean confirmDebit(BusinessActionContext context); boolean cancelDebit(BusinessActionContext context); }TCC 落地时的三个核心问题,面试官很爱问,这里展开说一下。
空回滚:如果 Try 方法还没执行完,因为网络等原因,TC 直接调用了 Cancel,此时 Cancel 需要能识别出“分支事务不存在”,直接返回成功。常见做法是在事务记录表中先查记录,不存在则先插入一条状态为“已取消”的记录再返回成功,避免重复回滚。
悬挂:如果 Cancel 先执行完了,Try 的方法才到达,此时 Try 应该直接返回,不再继续执行业务逻辑。常见做法是同样依赖事务记录表,读取到存在“已取消”记录时,Try 直接返回。
幂等:Confirm 和 Cancel 可能会被重试调用,业务侧必须保证多次执行与一次执行结果相同。常见做法是状态机流转 + 唯一键约束。
TCC 确实比 AT 麻烦,但正因为麻烦,才能在面试中拉开差距。能够讲清楚空回滚和悬挂的人,才是真正写过 TCC 的人。
4. 银行外包场景中的分布式事务考点
4.1 为什么金融行业面试偏爱分布式事务
回到广发外包面试这个场景。银行类项目对数据一致性极度敏感,账务数据不允许出现任何差错。一笔交易从渠道端到核心系统,往往跨了多个系统:前置系统、账务系统、风控系统、清结算系统,任何一个环节出问题,后果都是资金损失或监管处罚。
因此在银行项目的技术面试中,分布式事务不是一个“加分项”,而是一个“必考题”。外包岗虽然不在银行内部编制,但同样要写银行核心链路的代码,招聘方当然要确保候选人有支撑这类业务的技术深度。
4.2 金融场景下分布式事务的特点
第一,对账与冲正是兜底方案。银行系统通常不会完全依赖分布式事务框架保证最终一致,而是在业务链路之外,建立日终对账、差错账处理、冲正交易等补偿机制。面试时可以说:“除了用 Seata,我们还会做每日对账,对不平的交易自动或人工冲正。”
第二,幂等设计是必答题。支付回调、重试发送、消息重复消费,在金融场景里都是常态。你必须保证同一笔交易处理多次不会产生额外影响。常用手段是:交易流水表 + 唯一交易号 + 状态字段。插入前先查状态,或者在数据库层面依赖唯一索引,重复插入直接报错被捕获。
第三,事务尽量缩短。分布式事务的性能损耗比本地事务大得多。金融系统里设计接口时,要尽量减少事务内调用外部系统的次数,把不必要的耗时操作移出事务,例如短信通知、异步账单生成、营销推送都可以放在事务提交后异步执行。
第四,合规与审计日志。银行项目对操作留痕有硬性要求。关键业务表的每次变更都需要记录操作日志或审计日志,包括操作人、操作时间、变更前后值、交易流水号。这个习惯在写代码时就要养成,字段设计阶段就要考虑进去。
4.3 面试追问的典型路径
面试官考察分布式事务,通常按这个路径深入:
“你的项目里用过分布式事务吗?” -> “用的什么方案,为什么选这个方案?” -> “AT 模式底层原理是什么?” -> “如果全局事务回滚失败怎么办?” -> “消息消费怎么保证不丢不重?” -> “有没有考虑过不用分布式事务,用最终一致性 + 对账来替代?”如果第一层回答“我们用了 Seata”,但说不清原理和替代方案,基本上就会被认定是“只用了工具、不懂原理”。反之,如果能主动说出 AT 模式的全局锁、undo_log、脏写问题,再对比 TCC 和 Saga 的优缺点,面试官对你的技术评价会明显上升。
4.4 常见问题与排查思路
分布式事务在实际落地中问题不少,下面整理几个高频问题,以及完整的排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
启动报错no available service | Seata Server 没启动成功;客户端 tx-service-group 与服务端分组不一致 | 检查 Nacos 服务列表里是否有 seata-server;核对tx-service-group和vgroup-mapping配置 |
| 全局事务不生效,异常不回滚 | 漏加@GlobalTransactional;事务方法被同类内部方法调用,AOP 代理失效 | 确保入口方法加注解;避免同类自调用;通过不同 Bean 调用或使用 AopContext |
| 回滚失败,提示 undo_log 缺失 | 业务表没有建 undo_log 表;事务在 RM 侧本地提交前丢失镜像 | 每个分支事务库都必须有 undo_log 表;检查表结构是否与 Seata 版本匹配 |
| AT 模式回滚把其他请求的更新也覆盖了 | 并发更新同一行数据时出现脏写 | 使用全局锁或改人工处理;高并发热点场景考虑换 TCC 或最终一致性方案 |
| 消息重复消费导致重复扣款 | 缺少幂等控制 | 消费者侧引入交易流水表 + 唯一索引,重复消息直接忽略 |
| TCC 的 Cancel 被先调用 | 网络超时导致 Try 请求延迟到达 | 事务记录表中记录状态,Cancel 先入则标记“已取消”,Try 到达后直接返回 |
| 分布式事务超时 | 分支事务过多、单分支执行时间过长、TC 负载高 | 缩小事务范围;把耗时操作异步化;调整 Seata 的超时参数 |
4.5 排查分布式事务问题的一般步骤
如果线上分布式事务出了数据不一致,第一反应不是翻日志,而是先确认边界,再按顺序排查:
- 确认不一致的数据范围和涉及的服务:从报错接口、监控告警或对账平台找到事务 ID / XID。
- 查全局事务状态:看 TC 日志或 Seata 的全局事务会话,确认全局事务是提交了还是回滚了。
- 查分支事务状态:定位到是哪个分支提交成功但未收到回滚通知,还是分支回滚失败。
- 查业务日志:确认每个参与者的业务方法执行到哪一步、异常是什么。
- 查 undo_log 和业务表数据:根据 XID 和 branch_id 看 undo_log 是否有记录、日志状态是否为 1(已防御)。
- 针对原因处理:如果是代码异常,修复后手工补偿或通过批量脚本对账修复;如果是框架问题,调整配置或升级版本。
- 验证恢复结果:对账平台重新核对数据差值,两边数据对齐后才算闭环。
5. 最佳实践与工程建议
5.1 尽可能缩小分布式事务范围
分布式事务不是银弹,过度使用会给系统带来巨大的性能损耗。最好的优化其实是:能不跨库就不跨库,能不分库就不分库。
有些场景可以通过表结构调整避免跨库事务。比如订单和订单明细本来可能会分到两个服务,但如果查询和写入都集中在订单维度,就在一个库里建两张表,用本地事务解决。微服务拆分不能只按表拆,要考虑业务内聚性,边界划分要能圈住强一致性的数据。
5.2 优先考虑最终一致性方案
除非业务上确实需要强一致,否则优先选最终一致性。下单、发券、积分、通知这类业务,完全可以用事务消息或本地消息表。资金类业务优先 TCC,不强上 XA。
原因很简单:最终一致性方案把一致性检查从“实时阻塞”变成“异步补偿”,系统吞吐量高得多,且具备更好的故障隔离性。流程编排复杂的时候,再考虑 Saga。
5.3 幂等设计必须内置
无论用哪种方案,幂等都必须做。分布式事务的重试、消息的重复投递、Callbacks 的多次调用,都要求业务接口天然幂等。
一个实际可落地的做法:所有的核心交易表都带上request_id或tx_id字段,并建唯一索引。插入前先查状态,插入时靠唯一索引挡住重复。状态变更通过乐观锁WHERE status = 期望值来实现,避免并发覆盖。
5.4 全链路日志与监控
分布式事务的链路比普通接口长,出了问题如果没有日志,排查会非常痛苦。每个分支事务的入口方法要打印:
- 全局事务 XID。
- 分支事务 branchId。
- 业务流水号。
- 入参关键字段。
- 返回结果或异常码。
同时要监控 Seata Server 本身的指标:全局事务数量、平均耗时、回滚率、分支事务注册量。回滚率突然升高,通常说明业务侧出现了新的异常场景,需要第一时间检查发布和调用方。
5.5 对账机制是最后的防线
不管分布式事务方案多完善,都要有对账系统兜底。日终跑批,把各系统的流水拉出来按交易号做匹配,找出两边不一致的数据,再走自动冲正或工单处理。
在金融场景下,“分布式事务 + 对账 + 人工补偿”才是完整的一致性保障体系。面试时能提到这一层,相当于告诉面试官你不只会用中间件,而是有系统性的线上治理思路。
5.6 生产环境变更注意事项
修改 Seata 配置、升级 Seata Server、切换事务模式,都属于影响生产数据一致性的变更,必须走变更审批流程,先在测试环境完整验证再灰度发布。
涉及资金操作的代码,不要在生产环境直接改 SQL。任何时候补数据,先备份再做变更,变更脚本要经过 review。优先通过已有的补偿接口或后台任务处理,不要随手连生产库执行 UPDATE。
6. 面试复盘与学习路线
6.1 这场面试真正在考什么
回看“10年 Java 去面广发外包,分布式事务被深挖”这个场景,表面上是在考技术方案,实际上是在考以下几件事:
- 你有没有真正写过分布式事务,还是只在简历上写了“熟悉分布式事务”。
- 遇到数据不一致问题时,你能不能有条理地排查。
- 在金融业务的强合规场景里,你有没有相关意识和经验。
- 技术选型时,你能否根据业务场景做权衡,而不是只会套模板。
10 年经验的 Java 开发者,面试官期待的不只是会配置 Seata,而是能讲清楚方案机制、能说出适用边界、能设计补偿与对账机制。外包岗位也不例外,因为代码要跑在银行的生产环境里,容错空间非常小。
6.2 后续学习建议
如果你还没用过分布式事务,建议按以下路线循序渐进:
- 先把本地事务吃透:ACID、隔离级别、MVCC、undo log、redo log。
- 动手搭建 Seata + Nacos + Spring Cloud Alibaba 环境,跑通 AT 模式。
- 阅读 AT 模式源码,理解全局锁、前后镜像、undo_log 的回滚机制。
- 自己实现一个简单的 TCC 接口,刻意去写空回滚和悬挂的处理逻辑。
- 用 RocketMQ 事务消息改造一个下单场景,对比它和 Seata 的差别。
- 读几篇 Saga 相关文章,理解编制式与编排式的实现差异。
- 设计一张对账报表,把“分布式事务 + 对账”串起来形成完整方案。
每一步都动手实践,不要停留在看文章。分布式事务只有在真实故障和生产问题中才能体会到它的复杂性和工程意义。等你真正排查过一次数据不一致,再回到面试题时,你讲出来的每个细节都会有说服力。
这篇文章从分布式事务的核心概念讲到了 Seata AT 模式的完整实战,也整理了银行金融场景下的特殊考法和高频排查表格。如果你正在准备 Java 后端面试,重点掌握方案对比和 TCC 三个核心问题;如果你在项目里落地分布式事务,优先把幂等、对账、日志监控做扎实。如果觉得本文对你有帮助,可以收藏备用,也欢迎在实际落地后回来交流你遇到的新问题。