笔者在负责某电商中台订单拆分项目时,基于Seata 1.6.1搭建分布式事务体系。经历了从初期AT模式“无脑接入”导致频繁死锁,到深入源码改造TCC防悬挂机制的完整历程。本文不重复赘述基础理论,直接复盘生产环境中三大高频痛点及其解决方案。 (本文部分调优思路参考了黑马博学谷《狂野架构师》关于分布式事务源码解析的底层逻辑)
项目初期为了快速落地,我们采用Seata AT(自动补偿)模式。上线首日大促流量进来,订单服务出现大量Lock wait timeout exceeded异常,TPS从预期的2000跌至80,且有部分分支事务一直处于UnKnown状态无法回滚。
通过Skywalking链路追踪发现,耗时卡在DataSourceProxy的execute方法上。根本原因:AT模式基于undo_log和全局锁(Global Lock) 保证隔离性。业务逻辑中@Transactional(本地锁)与@GlobalTransactional(全局锁)的加锁顺序不当,导致A服务持有本地锁等待全局锁释放,B服务持有全局锁等待本地锁释放,形成死锁环。
通过SHOW ENGINE INNODB STATUS看到典型事务:
TRANSACTION 1: 持有 t_order 的 RECORD LOCK,等待 Seata global_lock 表锁
TRANSACTION 2: 持有 Seata global_lock 表锁,等待 t_order 的 RECORD LOCK查看Seata核心类 AbstractLockManager,全局锁在业务SQL执行前由TC(Server端)获取。若业务方法加了Spring @Transactional,本地数据库连接会先开启隐式锁。
解决方案(二选一):
Propagation.REQUIRES_NEW,让本地锁在子事务中快速释放,避免长时间持有锁等待全局锁。@Service
public class OrderService {
// 正确姿势:全局事务注解只包裹业务编排,本地事务只针对单表insert
@GlobalTransactional(timeoutMills = 300000, name = "create-order")
public void createOrder(OrderDTO dto) {
// 1. 本地事务单独控制(使用编程式事务或脱离大事务)
orderDao.insert(order);
// 2. 远程调用(剥离出Spring事务上下文)
storageFeign.decrease(dto.getProductId());
accountFeign.decrease(dto.getUserId());
}
}调优结果:调整后锁等待超时异常降低98%,在同等压测下RT从2.3s降至650ms。
由于AT模式的锁性能瓶颈,核心库存扣减服务改为TCC模式。但在引入TCC后,日志中频繁出现Cancel method invoked without prior Try和Try method invoked after Cancel报警,即经典的空回滚和悬挂问题。
虽然Seata官方声称支持防悬挂,但在早期版本(1.4.x)中,如果Try阶段因网络抖动超时,事务协调者(TC)会触发Cancel。此时Cancel先执行,清除了上下文。随后迟到的Try请求到达,执行了资源预留,导致资源永远无法释放(悬挂)。
Seata 1.6.1在 TccActionInterceptor 中增加了对BusinessActionContext的isBranchRegistered校验。但仍需业务层配合。我们在每个TCC接口的Try方法里增加以下防御性逻辑:
@Slf4j
@Component
public class StorageTccActionImpl implements StorageTccAction {
@Autowired
private FreezeMapper freezeMapper;
@Override
@TwoPhaseBusinessAction(name = "deductStorage", commitMethod = "commit", rollbackMethod = "rollback")
public boolean tryDecuct(BusinessActionContext context, Long productId, Integer count) {
// 1. 【防悬挂校验】先检查是否已经有该事务ID的Cancel日志
String xid = context.getXid();
if (freezeMapper.existsCancelLog(xid)) {
log.warn("检测到Cancel已提前执行,拒绝本次Try,防止悬挂。Xid: {}", xid);
return false; // 直接返回,不预留资源
}
// 2. 【幂等校验】检查是否已Try过
if (freezeMapper.existsTryLog(xid)) {
log.info("Try已执行过,幂等返回。Xid: {}", xid);
return true;
}
// 3. 核心业务:扣减可用库存,增加冻结库存
storageMapper.decreaseAvailable(productId, count);
FreezeRecord record = new FreezeRecord();
record.setXid(xid);
record.setProductId(productId);
record.setCount(count);
freezeMapper.insert(record); // 记录Try日志
// 4. 将上下文参数存入context,供Commit/Cancel使用
context.getActionContext().put("productId", productId);
context.getActionContext().put("count", count);
return true;
}
@Override
public boolean commit(BusinessActionContext context) {
// 幂等提交:删除冻结记录,实际扣减已在Try中完成
return freezeMapper.deleteByXid(context.getXid()) > 0;
}
@Override
public boolean rollback(BusinessActionContext context) {
// 【防空回滚】若没有Try日志,则不执行任何业务回滚
if (!freezeMapper.existsTryLog(context.getXid())) {
log.warn("空回滚检测,无Try记录,直接返回成功。Xid: {}", context.getXid());
return true;
}
// 释放冻结库存,增加可用库存
FreezeRecord record = freezeMapper.selectByXid(context.getXid());
storageMapper.increaseAvailable(record.getProductId(), record.getCount());
return freezeMapper.deleteByXid(context.getXid()) > 0;
}
}existsCancelLog。existsTryLog。XID(全局事务ID)在业务表中做隔离,确保分布式事务上下文失效时的最终一致性。在订单完成后的长流程(通知ERP、清缓存、发券)中使用了Saga模式。某次ERP接口响应超时(接口未熔断),Saga状态机默认超时时间(10s)到达后触发全局补偿,但此时ERP线程池中积压的任务后续又执行成功了,导致补偿后业务又被错误执行。
调整seata: saga: state-machine: retry-policy: max-attempts: 3 delay: 1000,并在JSON状态机定义中开启isAsync同步转异步,增加timeout配置:
{
"name": "orderPostProcess",
"states": [
{
"name": "callErp",
"type": "serviceTask",
"serviceName": "erpFacade",
"serviceMethod": "syncOrder",
"timeout": 3000,
"compensateState": "erpCompensate"
}
]
}核心逻辑:在erpFacade中结合Hystrix线程池隔离,超时即抛出TimeoutException,Saga引擎捕获后立即触发补偿,而非等待最终响应,杜绝了业务不一致。
笔者经过3轮全链路压测,结合生产稳定性数据,给出如下选型底线逻辑:
场景特点 | 最终选定模式 | 代价与妥协 |
|---|---|---|
核心资产(库存/余额) | TCC(手动编码) | 需额外建设冻结表,开发量+30%,性能最优(RT < 100ms) |
非核心关联(日志/埋点) | 可靠消息(RocketMQ事务) | 异步解耦,容忍2-5s延迟 |
查询类/只读事务 | 不加任何分布式事务 | 利用CQRS模式彻底规避 |
遗留老系统改造(无源码) | AT模式(严格压测锁竞争) | 必须配置@GlobalTransactional(timeout)并拆分本地事务粒度 |
关于XA模式:虽然Seata支持,但在生产环境中不建议使用,其阻塞式协议在MySQL 5.7默认隔离级别(RR)下会放大锁范围,不适合互联网高并发项目。
分布式事务本质上是时间与一致性的博弈。作为系统架构师,面对这类问题不能仅停留在“用哪个框架”,而要深入框架源码边界(如Seata的锁机制、TCC拦截器链),结合业务特性(资金、库存、日志)做精细化治理。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。