Seata AT 模式在资金划扣场景中的使用
1. 对应简历段落
对应简历中可以这样描述:
在资金划扣与订单结算链路中,参与基于 Seata AT 模式的分布式事务治理,覆盖订单状态更新、账户余额扣减、资金流水落库、营销权益核销等跨服务操作。通过全局事务、分支事务、undo_log、全局锁、幂等流水和异常补偿设计,解决多库多服务下局部成功导致资金状态不一致的问题,并结合超时控制、事务分组、监控告警和人工对账降低资金链路风险。
这段经历的难点在于资金场景天然高风险。面试时不能只说“加了 @GlobalTransactional 就行”,而要讲清楚 AT 模式适用边界、隔离级别、回滚机制、脏写风险、悬挂空回滚等问题。
2. 业务背景
资金划扣场景通常涉及多个系统:订单系统负责订单状态,账户系统负责余额和冻结金额,资金系统负责流水,营销系统负责优惠券或权益核销,风控系统负责校验。一个支付或划扣动作看似简单,实际可能要更新多张表、多个库、多个服务。
如果不用分布式事务,常见问题是订单已经改成支付中,账户扣款失败;账户扣了钱,资金流水没写成功;优惠券核销成功,订单最终失败。资金链路不能只靠“之后再修”,因为用户和财务都会直接感知。
但资金链路也不是所有动作都必须放在一个大事务里。强一致部分通常包括订单状态、账户余额、资金流水;通知、报表、积分、短信可以异步。Seata AT 模式适合改造成本较低、基于关系型数据库、本地事务清晰的场景。
3. 核心原理
Seata AT 模式包括 TC、TM、RM 三个角色。TM 开启全局事务,TC 负责协调全局事务状态,RM 负责管理分支事务和资源。
AT 模式的一阶段会代理业务 SQL,记录修改前后的镜像,生成 undo_log,同时提交本地事务。二阶段如果全局提交,异步删除 undo_log;如果全局回滚,根据 undo_log 反向补偿,把数据恢复到修改前状态。
它的优势是业务侵入相对低,不需要像 TCC 那样为每个接口写 Try、Confirm、Cancel。但代价是依赖数据库代理和 undo_log,对 SQL 类型、隔离级别、全局锁和长事务比较敏感。
资金场景里必须理解全局锁。比如账户余额扣减时,Seata 会在一阶段提交前申请全局锁,防止其他全局事务同时修改同一行。否则两个分布式事务交叉提交和回滚,可能导致余额不一致。
4. 项目落地
以“订单资金划扣”为例,链路可以拆为:
- 订单服务校验订单可支付,状态从
WAIT_PAY改为PAYING。 - 账户服务扣减可用余额,增加已扣金额。
- 资金服务写资金流水,记录划扣请求号、账户号、金额、状态。
- 营销服务核销优惠券或权益。
- 全局事务成功后订单改为
PAID,并发送出单或履约消息。
这里要谨慎处理事务边界。可以把强一致的订单预更新、账户扣款、资金流水放入 Seata 全局事务;营销权益如果和资金强绑定,也进入事务;短信、报表、通知不要放进全局事务。
关键表设计上,账户扣款必须有幂等流水号。即使用了 Seata,也要防止接口重试导致重复扣款。账户表可以有 available_amount、frozen_amount、version,资金流水表用 request_no 唯一索引。
undo_log 表要在每个参与 AT 模式的数据库里创建,并保证字段类型、字符集、索引符合 Seata 版本要求。生产环境还要关注 undo_log 堆积,因为二阶段清理异常会导致表膨胀。
5. 关键配置或伪代码
全局事务入口:
@GlobalTransactional(name = "fund-deduct-tx", rollbackFor = Exception.class, timeoutMills = 8000)
public DeductResult deduct(DeductCommand command) {
Order order = orderService.markPaying(command.getOrderId(), command.getRequestNo());
accountService.deduct(order.getAccountId(), command.getAmount(), command.getRequestNo());
fundFlowService.createDeductFlow(command.getRequestNo(), order.getId(), command.getAmount());
couponService.useCoupon(order.getCouponId(), command.getRequestNo());
orderService.markPaid(order.getId(), command.getRequestNo());
return DeductResult.success(order.getId());
}
账户扣款要同时做幂等和余额校验:
@Transactional(rollbackFor = Exception.class)
public void deduct(String accountId, BigDecimal amount, String requestNo) {
if (flowRepository.exists(requestNo, "ACCOUNT_DEDUCT")) {
return;
}
int updated = accountRepository.deduct(accountId, amount);
if (updated == 0) {
throw new InsufficientBalanceException(accountId);
}
flowRepository.insert(requestNo, accountId, amount, "ACCOUNT_DEDUCT", "SUCCESS");
}
SQL 要带条件,避免扣成负数:
update account
set available_amount = available_amount - ?,
deducted_amount = deducted_amount + ?
where account_id = ?
and available_amount >= ?;
基础配置示例:
seata.tx-service-group=fund_tx_group
seata.service.vgroup-mapping.fund_tx_group=default
seata.client.tm.default-global-transaction-timeout=8000
seata.client.rm.report-success-enable=true
这些配置只是入口,生产还要关注 TC 高可用、事务分组映射、注册中心、配置中心、数据源代理和 undo_log 表。
6. 常见坑
第一个坑是把长流程都塞进全局事务。外部支付、短信、风控 HTTP 慢调用、人工审核都不适合长时间占用全局事务。事务越长,全局锁冲突越多,回滚风险越高。
第二个坑是没有幂等。Seata 能处理事务回滚,但不能替你识别同一个划扣请求是否重复提交。资金请求必须有全局唯一 requestNo。
第三个坑是忽略 AT 的 SQL 限制。复杂 SQL、存储过程、跨库 join、部分批量语句可能无法正确生成 undo 镜像。上线前要覆盖真实 SQL 测试。
第四个坑是 undo_log 和业务表不在同一个本地事务中。AT 模式依赖业务更新和 undo_log 一起提交,否则回滚就失去依据。
第五个坑是全局锁冲突没有预案。热点账户、批量扣款、同一订单反复操作会导致锁等待或失败。要有重试、排队或业务串行化策略。
第六个坑是资金场景只依赖技术事务,没有对账。资金系统必须有日终对账、差错处理、人工审核和审计日志。
7. 面试追问
常见追问包括:
- Seata AT、TCC、Saga、XA 的区别是什么?
- AT 模式为什么需要 undo_log?
- 一阶段已经提交本地事务,为什么还能回滚?
- 全局锁解决什么问题?
- AT 模式适不适合资金核心系统?
- 如果二阶段回滚失败怎么办?
- 如何防止重复扣款?
- 事务超时后业务怎么处理?
这些问题需要你既懂原理,也能表达资金场景的谨慎态度。
8. 推荐回答
可以这样回答:
“我们用 Seata AT 主要是解决多服务多库下局部成功的问题。比如订单状态更新、账户扣款、资金流水写入分布在不同服务,如果账户扣了钱但订单失败,风险很高。AT 模式下,入口方法开启全局事务,每个服务本地事务提交时由 Seata 记录前后镜像和 undo_log,并向 TC 注册分支。全局提交时删除 undo_log,全局回滚时根据 undo_log 反向恢复。”
“资金场景里我不会只依赖 @GlobalTransactional。我们同时做了请求号幂等,账户扣款 SQL 带余额条件,资金流水有唯一索引,订单状态有状态机限制。这样即使接口重试,也不会重复扣款。全局事务只覆盖强一致的短链路,短信、报表、通知都通过 MQ 异步处理,避免长事务。”
“AT 模式的边界也要讲清楚。它对数据库和 SQL 有要求,一阶段本地事务已经提交,所以隔离性依赖全局锁,热点行会有锁冲突。对于特别核心的支付清结算,有些团队会更倾向 TCC 或账户系统内部强一致加外部补偿。我们项目里还保留了对账任务,按订单、账户流水、资金流水核对,二阶段失败或异常状态会进入差错处理。”
9. 延伸学习路线
第一步,理解 Seata 的 TC、TM、RM 架构,以及全局事务、分支事务、XID、事务分组的含义。
第二步,重点学习 AT 模式的一阶段、二阶段、undo_log、前后镜像和全局锁。
第三步,对比 AT、TCC、Saga、XA。知道每种模式适合什么业务,不要把一个方案套所有场景。
第四步,补充资金系统设计,包括幂等流水、账户模型、冻结与解冻、余额校验、状态机、对账和差错处理。
第五步,做故障演练:分支提交失败、二阶段回滚失败、TC 宕机、全局事务超时、热点账户锁冲突。能讲出这些异常怎么恢复,面试可信度会明显提升。