电商返利场景下的资金池模型:分布式事务 Seata AT 模式与 TCC 模式权衡
·
电商返利场景下的资金池模型:分布式事务 Seata AT 模式与 TCC 模式权衡
大家好,我是 微赚淘客系统3.0 的研发者省赚客!
在返利系统中,用户完成订单后需执行 资金池扣减(平台账户)→ 个人余额增加(用户账户)→ 佣金记录写入 三步操作。由于涉及多个微服务(fund-service、account-service、rebate-record-service),必须保证数据一致性。我们基于 Seata 实现分布式事务,并在 AT 模式 与 TCC 模式 之间进行深度权衡与落地。
一、业务模型与一致性要求
- 资金池表(fund_pool):记录平台可分配佣金总额;
- 用户账户表(user_account):记录用户可提现余额;
- 返利记录表(rebate_record):记录每笔返利明细。
核心约束:
- 资金池余额 ≥ 0;
- 用户余额变更必须与记录一致;
- 不允许重复发放同一笔返利。
二、AT 模式实现:简单但存在脏读风险
AT 模式通过 全局锁 + undo log 自动回滚。
服务调用链
package juwatech.cn.rebate.service;
@Service
public class RebateDistributeService {
@GlobalTransactional(timeoutMills = 30000)
public void distributeRebate(RebateRequest request) {
// 1. 扣减资金池
fundClient.decreaseFundPool(request.getTradeId(), request.getAmount());
// 2. 增加用户余额
accountClient.increaseBalance(request.getUserId(), request.getAmount());
// 3. 记录返利
rebateRecordService.createRecord(request);
}
}
资金池服务(AT 模式)
package juwatech.cn.fund.service;
@Service
public class FundPoolService {
@Transactional
public void decreaseFundPool(String tradeId, BigDecimal amount) {
// 先查再更新(存在并发超扣风险)
FundPool pool = fundPoolMapper.selectForUpdate(); // 加行锁
if (pool.getAvailable().compareTo(amount) < 0) {
throw new InsufficientFundException();
}
fundPoolMapper.decreaseAvailable(amount);
// Seata 自动生成 before image / after image
}
}
问题:
selectForUpdate导致高并发下数据库锁竞争;- 全局事务期间,其他读请求可能读到中间状态(如余额已加但记录未写);
- 无法支持幂等性校验(重复请求会重复扣款)。
三、TCC 模式实现:强一致性但开发复杂
TCC 通过 Try-Confirm-Cancel 三阶段手动控制资源。
定义 TCC 接口
package juwatech.cn.fund.api;
@LocalTCC
public interface FundPoolTccService {
@TwoPhaseBusinessAction(name = "decreaseFundPoolTcc", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDecrease(@BusinessActionContextParameter(paramName = "tradeId") String tradeId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
Try 阶段:冻结资金
@Override
public boolean tryDecrease(String tradeId, BigDecimal amount) {
// 幂等检查:防止重复 Try
if (fundFreezeRecordMapper.existsByTradeId(tradeId)) {
return true; // 已冻结,直接成功
}
// 冻结资金(可用余额 - amount,冻结余额 + amount)
int updated = fundPoolMapper.freezeAmount(amount);
if (updated == 0) {
throw new InsufficientFundException();
}
// 记录冻结流水
freezeRecordMapper.insert(new FreezeRecord(tradeId, amount));
return true;
}
对应 SQL:
UPDATE fund_pool
SET available = available - #{amount},
frozen = frozen + #{amount}
WHERE available >= #{amount};
Confirm 阶段:扣除冻结
@Override
public boolean confirm(BusinessActionContext context) {
String tradeId = context.getActionContext("tradeId", String.class);
FreezeRecord record = freezeRecordMapper.getByTradeId(tradeId);
if (record != null && !record.isConfirmed()) {
fundPoolMapper.confirmDeduct(record.getAmount()); // frozen -= amount
freezeRecordMapper.markConfirmed(tradeId);
}
return true;
}
Cancel 阶段:解冻
@Override
public boolean cancel(BusinessActionContext context) {
String tradeId = context.getActionContext("tradeId", String.class);
FreezeRecord record = freezeRecordMapper.getByTradeId(tradeId);
if (record != null && !record.isCancelled()) {
fundPoolMapper.unfreeze(record.getAmount()); // available += amount, frozen -= amount
freezeRecordMapper.markCancelled(tradeId);
}
return true;
}
用户账户服务同理实现 TCC 接口。
主业务调用
@GlobalTransactional
public void distributeRebateTcc(RebateRequest request) {
fundPoolTccService.tryDecrease(request.getTradeId(), request.getAmount());
userAccountTccService.tryIncrease(request.getUserId(), request.getAmount());
rebateRecordService.createRecord(request); // 本地事务,无需 TCC
}
四、AT 与 TCC 对比
| 维度 | AT 模式 | TCC 模式 |
|---|---|---|
| 开发成本 | 低(几乎无侵入) | 高(需实现 3 个方法) |
| 性能 | 中(全局锁 + undo log) | 高(无长事务,仅冻结) |
| 一致性 | 最终一致(存在脏读窗口) | 强一致(业务层控制) |
| 幂等性 | 需额外处理 | Try 阶段天然支持 |
| 适用场景 | 低并发、非核心资金 | 高并发、资金核心链路 |
五、生产选型结论
- 资金池扣减、用户余额变更:采用 TCC 模式,确保资金安全;
- 返利记录写入、通知发送:采用 AT 模式 或异步最终一致;
- 防重设计:所有入口基于
trade_id做幂等拦截,避免重复触发。
通过 TCC 模式,系统在大促期间处理 8 万+ 返利事务/分钟,资金零差错。
本文著作权归 微赚淘客系统3.0 研发团队,转载请注明出处!
更多推荐




所有评论(0)