news 2026/8/20 16:53:04

TCC 落地实战:优惠券核销的高并发、可回滚与注解式实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCC 落地实战:优惠券核销的高并发、可回滚与注解式实现

TCC 常用注解速览

  • 注解是很多 TCC 框架(如Seata、SOFARPC/Dubbo 的分布式事务扩展)提供的声明式能力,用来把一个接口标记为 TCC 资源,并把Try/Confirm/Cancel三阶段方法关联起来,减少样板代码与调用出错概率。
  • 在 Seata 中,常见的是:@LocalTCC(标记接口)、@TwoPhaseBusinessAction(标记 Try 并绑定二阶段方法)、@BusinessActionContextParameter(把 Try 参数带入二阶段)。在阿里云 DTX 等框架中,也有同名的@TwoPhaseBusinessAction等注解,用法与语义相近。

Seata 注解与上下文一览

  • 注解与用途
    • @LocalTCC:加在接口上,声明该接口包含 TCC 方法,Seata 会解析为 TCC 资源。
    • @TwoPhaseBusinessAction:加在 Try 方法上,声明二阶段方法名(如commitMethod/rollbackMethod),并给该 TCC 方法起一个全局唯一 name
    • @BusinessActionContextParameter:加在 Try 的参数上,指定参数在BusinessActionContext中的键名,供 Confirm/Cancel 读取。
  • 上下文与获取
    • BusinessActionContext:TCC 专用上下文,承载XID、BranchId以及 Try 阶段通过 @BusinessActionContextParameter 传入的参数;在 Confirm/Cancel 中以方法参数接收。
    • RootContext:全局事务的线程级上下文,常用getXID()获取全局事务 ID,贯穿 AT/TCC/Saga/XA 等模式。
  • 方法签名要点
    • Try 方法第一个参数通常是BusinessActionContext,后续参数自定义;
    • Confirm/Cancel 方法通常仅接收BusinessActionContext并返回boolean(表示二阶段是否成功)。

优惠券核销的注解式 TCC 示例(Seata)

  • 场景约定
    • 券状态:AVAILABLE/LOCKED/USED/CANCELLED;二阶段需要幂等、防悬挂、空回滚。
    • 全局事务由订单服务开启,优惠券服务作为 TCC 参与者。
  1. 定义 TCC 接口(加注解)
importio.seata.rm.tcc.api.BusinessActionContext;importio.seata.rm.tcc.api.BusinessActionContextParameter;importio.seata.rm.tcc.api.LocalTCC;importio.seata.rm.tcc.api.TwoPhaseBusinessAction;@LocalTCCpublicinterfaceCouponTccAction{/** * Try:锁定优惠券 */@TwoPhaseBusinessAction(name="couponLock",// 全局唯一commitMethod="confirm",// 二阶段确认方法名rollbackMethod="cancel"// 二阶段取消方法名)booleantryLock(BusinessActionContextcontext,@BusinessActionContextParameter(paramName="xid")Stringxid,@BusinessActionContextParameter(paramName="couponId")LongcouponId,@BusinessActionContextParameter(paramName="orderId")StringorderId);/** * Confirm:确认核销 */booleanconfirm(BusinessActionContextcontext);/** * Cancel:取消锁定(退回) */booleancancel(BusinessActionContextcontext);}
  1. 接口实现(含幂等与空回滚/防悬挂要点)
importio.seata.rm.tcc.api.BusinessActionContext;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;@ServicepublicclassCouponTccActionImplimplementsCouponTccAction{@AutowiredprivateCouponMappercouponMapper;@AutowiredprivateCouponFreezeMapperfreezeMapper;@Override@TransactionalpublicbooleantryLock(BusinessActionContextcontext,Stringxid,LongcouponId,StringorderId){// 幂等:二阶段已执行则直接成功if(freezeMapper.existsByXidAndCouponId(xid,couponId)){returntrue;}// 防悬挂:已回滚过,禁止再执行 Tryif(freezeMapper.isCancelled(xid,couponId)){thrownewIllegalStateException("禁止在 Cancel 后执行 Try,xid="+xid);}// 业务检查 + 锁定(一阶段本地事务内完成)intupdated=couponMapper.lockCoupon(xid,couponId,orderId);if(updated==0){thrownewRuntimeException("券不可用或已被占用,couponId="+couponId);}// 记录冻结流水,便于二阶段与审计CouponFreezefreeze=newCouponFreeze();freeze.setXid(xid);freeze.setCouponId(couponId);freeze.setOrderId(orderId);freeze.setStatus(FreezeStatus.TRYING.getCode());freezeMapper.insert(freeze);returntrue;}@Overridepublicbooleanconfirm(BusinessActionContextcontext){Stringxid=context.getXid();LongcouponId=Long.valueOf(context.getActionContext("couponId").toString());// 幂等:已确认直接成功CouponFreezefreeze=freezeMapper.findByXidAndCouponId(xid,couponId);if(freeze==null)returntrue;if(freeze.getStatus()==FreezeStatus.CONFIRMED.getCode())returntrue;if(freeze.getStatus()==FreezeStatus.CANCELLED.getCode())returnfalse;// 确认核销:状态迁移 + 记录使用时间intupdated=couponMapper.confirmUse(xid,couponId);if(updated>0){freezeMapper.updateStatus(xid,couponId,FreezeStatus.CONFIRMED.getCode());returntrue;}returnfalse;// 失败由调用方/框架重试}@Overridepublicbooleancancel(BusinessActionContextcontext){Stringxid=context.getXid();LongcouponId=Long.valueOf(context.getActionContext("couponId").toString());// 幂等:已回滚直接成功CouponFreezefreeze=freezeMapper.findByXidAndCouponId(xid,couponId);if(freeze==null){// 空回滚:记录日志并返回成功,避免悬挂// log.warn("空回滚,xid={}, couponId={}", xid, couponId);returntrue;}if(freeze.getStatus()==FreezeStatus.CANCELLED.getCode())returntrue;// 释放锁定:状态回滚 + 可用时间intupdated=couponMapper.cancelLock(xid,couponId);if(updated>0){freezeMapper.updateStatus(xid,couponId,FreezeStatus.CANCELLED.getCode());returntrue;}returnfalse;}}
  1. 发起方使用(开启全局事务)
importio.seata.spring.annotation.GlobalTransactional;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;@ServicepublicclassOrderService{@AutowiredprivateOrderMapperorderMapper;@AutowiredprivateCouponTccActioncouponTccAction;@GlobalTransactionalpublicvoidcreateOrderWithCoupon(CreateOrderReqreq){StringorderId=generateOrderId();// 1. 创建订单(状态 PENDING)Orderorder=buildOrder(orderId,req);orderMapper.insert(order);// 2. 锁定优惠券(TCC Try)couponTccAction.tryLock(order.getXid(),req.getCouponId(),orderId);}// 支付成功回调:触发二阶段 ConfirmpublicvoidonPaySuccess(StringorderId,Stringxid){// 查询订单使用的券(略)List<Long>couponIds=couponMapper.findCouponIdsByOrderId(orderId);for(Longcid:couponIds){couponTccAction.confirm(newBusinessActionContext(xid));}orderMapper.updateStatus(orderId,OrderStatus.PAID.getCode());}// 超时/取消:触发二阶段 CancelpublicvoidonCancel(StringorderId,Stringxid){List<Long>couponIds=couponMapper.findCouponIdsByOrderId(orderId);for(Longcid:couponIds){couponTccAction.cancel(newBusinessActionContext(xid));}orderMapper.updateStatus(orderId,OrderStatus.CANCELLED.getCode());}}
  • 要点回顾
    • @LocalTCC放在接口;@TwoPhaseBusinessAction放在 Try 方法并绑定二阶段方法名;@BusinessActionContextParameter把 Try 参数带入二阶段。
    • Confirm/Cancel 方法名需与注解配置一致,且返回boolean;二阶段接口要支持幂等可重试

常见坑与排查清单

  • 注解位置与签名
    • @LocalTCC 必须加在接口上;@TwoPhaseBusinessAction 必须加在 Try 方法;Confirm/Cancel 方法通常仅接收BusinessActionContext并返回boolean
  • 上下文取值
    • Try 的参数用@BusinessActionContextParameter标记,二阶段通过BusinessActionContext.getActionContext(“xxx”)取值;需要全局事务 ID 时用context.getXid()
  • 幂等、空回滚、防悬挂
    • 二阶段方法必须幂等(以xid+couponId做状态机判定);出现空回滚(Cancel 先于 Try)要能识别并直接成功;出现悬挂(Cancel 已执行而 Try 后到)要在 Try 端拒绝执行。
  • 事务边界
    • Try 阶段要在本地事务内完成检查与锁定;二阶段失败由框架/调用方有限重试;不要吞掉异常,否则会被判定为成功。

🔥 关注公众号【云技纵横】,目前正在更新分布式缓存进阶技巧和干货

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 12:53:51

开源治理模式:建立公平透明的项目决策流程

VibeVoice-WEB-UI&#xff1a;当开源治理遇见对话级语音合成 在播客、有声书和虚拟角色交互日益普及的今天&#xff0c;我们对语音合成的需求早已超越了“把文字读出来”的初级阶段。人们期待的是自然轮转的对话节奏、稳定可辨的多角色音色&#xff0c;以及贯穿始终的情感张力—…

作者头像 李华
网站建设 2026/8/16 13:03:56

用JDK17快速构建RESTful API原型

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 使用JDK17快速开发一个RESTful API原型&#xff0c;展示如何利用记录类&#xff08;Record&#xff09;定义数据模型&#xff0c;以及使用新的HTTP客户端进行网络请求。项目应包含…

作者头像 李华
网站建设 2026/8/19 13:35:50

企业短信费用管控实战:从监控到优化的完整方案

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个企业级短信费用管控系统案例演示&#xff0c;包含&#xff1a;1. 模拟电商促销场景的短信发送数据 2. 多维度费用分析仪表盘 3. 异常费用预警功能 4. 批量发送优化工具 5.…

作者头像 李华
网站建设 2026/7/31 21:14:33

比Notepad快10倍的AI代码编辑器开发

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 生成一个专业代码编辑器&#xff0c;要求&#xff1a;1. 支持主流编程语言语法高亮 2. 集成AI代码补全(Kimi-K2模型) 3. 内置终端 4. 代码片段管理 5. Git集成 6. 性能优化确保快速…

作者头像 李华
网站建设 2026/7/31 21:14:37

电商秒杀页面的A2UI实战:从设计到上线仅2小时

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个电商秒杀专题页面&#xff0c;包含&#xff1a;1. 动态倒计时组件 2. 库存实时更新系统 3. 用户行为分析热力图 4. 分布式锁防止超卖 5. 移动端自适应布局。要求使用Vue3T…

作者头像 李华
网站建设 2026/8/9 22:37:45

MiniCPM4.1-8B:80亿参数推理王者,3倍提速超越同类模型

MiniCPM4.1-8B&#xff1a;80亿参数推理王者&#xff0c;3倍提速超越同类模型 【免费下载链接】MiniCPM4.1-8B 项目地址: https://ai.gitcode.com/OpenBMB/MiniCPM4.1-8B 导语 OpenBMB团队最新发布的MiniCPM4.1-8B模型以80亿参数规模实现了推理性能的重大突破&#xf…

作者头像 李华