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. 项目落地

以“订单资金划扣”为例,链路可以拆为:

  1. 订单服务校验订单可支付,状态从 WAIT_PAY 改为 PAYING
  2. 账户服务扣减可用余额,增加已扣金额。
  3. 资金服务写资金流水,记录划扣请求号、账户号、金额、状态。
  4. 营销服务核销优惠券或权益。
  5. 全局事务成功后订单改为 PAID,并发送出单或履约消息。

这里要谨慎处理事务边界。可以把强一致的订单预更新、账户扣款、资金流水放入 Seata 全局事务;营销权益如果和资金强绑定,也进入事务;短信、报表、通知不要放进全局事务。

关键表设计上,账户扣款必须有幂等流水号。即使用了 Seata,也要防止接口重试导致重复扣款。账户表可以有 available_amountfrozen_amountversion,资金流水表用 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. 面试追问

常见追问包括:

  1. Seata AT、TCC、Saga、XA 的区别是什么?
  2. AT 模式为什么需要 undo_log?
  3. 一阶段已经提交本地事务,为什么还能回滚?
  4. 全局锁解决什么问题?
  5. AT 模式适不适合资金核心系统?
  6. 如果二阶段回滚失败怎么办?
  7. 如何防止重复扣款?
  8. 事务超时后业务怎么处理?

这些问题需要你既懂原理,也能表达资金场景的谨慎态度。

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 宕机、全局事务超时、热点账户锁冲突。能讲出这些异常怎么恢复,面试可信度会明显提升。