1.8.迄今为止你遇到最大的困难是什么?并且是如何解决的。
好的这个问题是整场面试的胜负手。面试官想听的不是你背出 TCC 的概念而是你如何面对一个混乱的、有资金风险的业务场景一步步推导出 TCC 方案并能对比其他方案优劣的完整思考过程。下面我帮你把这个问题的回答打磨成一个逻辑严密、充满技术含金量的 STAR 故事。面试官问“请你着重讲一下这个放款不一致问题以及为什么最终选了 TCC”1. 背景与最初的“土办法”“我刚接手这个项目时放款流程是一个比较典型的‘传统银行交易’模式。在一个巨大的、上千行的 Java 方法里通过Transactional管理本地事务然后在一个循环里按顺序调用各个外部系统的接口。比如首先在核心系统开立贷款账户然后调用支付系统下拨资金接着登记押品系统最后给监管上报文。这个方法最大的问题就是**‘资金安全靠天吃饭’**。一旦中间某个外部系统调用超时或返回了业务失败整个事务就会回滚。但问题是资金下拨这个动作它往往是个异步的、不可回滚的跨行转账。核心系统这边回滚了但资金可能已经离开了银行。这就造成了巨大的操作风险缺口。当时的应对措施就是写一个定时任务脚本每天去对比核心和支付系统的流水发现单边账就报警然后人工打电话处理非常被动和原始。”2. 深度需求分析与方案推演“在准备重构时我明确了几个核心目标。第一要保证资金绝对安全不能出现钱出了银行我们内部系统还不知道的情况。第二流程要可观测、可干预不能像之前那样失败后不知道卡在哪一步。第三要有兜底即使服务重启也能从断点继续不会丢任务。”“基于这几点我开始调研业界的主流分布式事务方案。对于这种跨越多个异构系统核心、支付、押品、监管的长流程我进行了详细的方案对比和推演。”3. 为什么不用 Seata AT 模式“首先排除了Seata AT 模式。因为它基于数据源代理自动生成回滚日志。而我的场景里很多外部系统是黑盒的只提供 RPC 接口比如资金下拨和监管报文。Seata AT 无法反向生成一个‘资金冲正’或‘报文撤回’的 SQL所以不适用。而且对于资金操作AT 默认的‘读未提交’隔离级别也无法接受可能会在全局事务未完成时让其他服务读到已下拨但未登记押品的中间态风险极高。”4. 为什么不用 Saga 模式“接着我考虑了Saga 模式。Saga 的模型是每一步成功后都提交本地事务下一步失败后逆序调用各服务的补偿接口。这看起来能解决长事务问题但在我们这个具体流程里它有个致命的‘时间窗口风险’。想象一下如果第二步‘资金下拨’成功后提交但在第三步‘押品登记’失败时系统调用资金冲正接口来补偿。万一这个冲正接口调用失败或超时了呢Saga 的标准做法是进行异步重试。但在重试期间这笔钱处于‘已下拨但应冲回而暂时未冲回’的异常挂账状态。对于金融场景这个不确定性窗口越短越好。Saga 的模式是先做再说错了我再补偿补偿可能失败。而我们需要的是先确认所有都OK我再一次性把事情都做了宁可不做不能做错做一半。”5. 为什么最终选择 TCC方案如何落地“所以我最终采用了TCC 模式。我认为它的思想更符合金融操作的安全要求‘先预留再确认最后才动用’。我把整个放款流程设计成了一个包含 TCC 资源的工作流。”具体设计是这样的这是回答核心工作流编排我引入Flowable作为流程引擎把流程的流转、节点的依赖关系、状态持久化和重试机制交给它。这样即使服务重启流程也能从失败节点继续执行。Flowable 在这里是流程的“导演”而 TCC 是每个步骤的“演员”。TCC 资源定义我把整个放款涉及的外部系统都抽象成了 TCC 接口。信贷额度服务:try: 冻结本次放款额度。confirm: 扣减已冻结的额度。cancel: 解冻被冻结的额度。核心账户服务:try: 预开一个贷款账号状态为“预开户”。confirm: 将账号状态改为“正常”。cancel: 删除预开户账号或标记为“已作废”。支付网关服务:try:这是最关键的一步。我要求支付系统提供一个“预下单”接口目的是预约一笔转账指令并冻结收款人账户本次的入金。这个接口只做校验和预约并不真正发出资金。confirm: 将预下单转为正式转账指令真正执行资金划拨。cancel: 撤销预下单解冻收款人账户。工作流 TCC 的执行流程一阶段预留Flowable 驱动流程并发或顺序地调用所有参与方服务的try方法。比如同时调用额度服务的冻结、核心的预开户和支付的预下单。如果所有try都成功了我们才能确信所有前置条件和资源都已就绪。此时资金仍然是安全的只是被“冻住”了。二阶段确认或取消如果所有try成功Flowable 进入下一个节点并发调用所有服务的confirm方法真正执行放款、账户状态变更、资金划拨。由于一阶段资源已全部锁定二阶段的成功几率非常高。如果任一try失败Flowable 立即结束当前节点跳转到错误处理节点并发调用所有已成功try服务的cancel方法将已预占的资源全部释放。这样整个系统回滚到初始状态不会产生任何资金风险。异常处理与兜底空回滚与悬挂严格按照 TCC 规范处理。如果 Cancel 请求先于 Try 到达悬挂我会记录下这个请求等 Try 到来时直接拒绝它。资金安全兜底支付网关的confirm接口必须设计为幂等允许失败后安全重试。此外我保留了最终的对账脚本作为最后的防线监控长期未进入终态的预下单记录并触发告警和人工介入。6. 总结与收获“通过这次重构我们把一个资金风险高、状态不可控的流程变成了一个资源可冻结、步骤可重试、状态可查询的强一致性流程。这让我深刻理解了在金融核心业务中TCC 并不是单纯的技术炫技而是为了保证资金安全这个底线必须做出的业务设计和系统工程。我的收获是技术选型永远是业务特性驱动的没有最好的技术只有最匹配业务安全要求的技术。”好的完全理解你的需求。下面我给你一个可直接运行、精简但完整的 TCC 分布式事务案例。代码模拟了银行放款场景包含三个 TCC 资源额度、账户、支付和一个协调器。同时展示了空回滚、悬挂的处理让你真正掌握 TCC 的核心。一、项目结构tcc-loan-demo/ ├── src/main/java/com/example/tcc/ │ ├── TccLoanApplication.java │ ├── controller/LoanController.java │ ├── service/ │ │ ├── LoanTccCoordinator.java // TCC 协调器核心 │ │ ├── resource/ │ │ │ ├── TccResource.java // TCC 资源接口 │ │ │ ├── CreditLimitResource.java // 额度服务实现 │ │ │ ├── CoreAccountResource.java // 核心账户服务实现 │ │ │ └── PaymentGatewayResource.java // 支付网关服务实现 │ ├── model/ │ │ ├── TccRequest.java │ │ └── TccResponse.java │ └── exception/TccException.java二、核心代码1. TCC 资源接口publicinterfaceTccResource{/** * 一阶段预占资源 * param businessId 业务唯一标识如放款单号用于防悬挂 * param params 预留参数 * return true 成功false 失败 */booleantryReserve(StringbusinessId,MapString,Objectparams);/** * 二阶段确认提交 * param businessId 业务唯一标识 * return true 成功false 失败需重试 */booleanconfirm(StringbusinessId);/** * 二阶段取消释放 * param businessId 业务唯一标识 * return true 成功false 失败需重试 */booleancancel(StringbusinessId);}2. 额度服务实现含防悬挂、幂等ServicepublicclassCreditLimitResourceimplementsTccResource{// 模拟数据库记录冻结额度表// businessId - { status: TRY/CONFIRMED/CANCELLED, frozenAmount: 100000 }privatefinalConcurrentHashMapString,MapString,ObjectdbnewConcurrentHashMap();OverridepublicbooleantryReserve(StringbusinessId,MapString,Objectparams){System.out.println(【额度】try 冻结, bizIdbusinessId);// 防悬挂如果 cancel 先于 try 到达cancel 会插一条占位记录statusCANCELLEDMapString,Objectexistingdb.get(businessId);if(existing!nullCANCELLED.equals(existing.get(status))){System.out.println(【额度】检测到空回滚记录拒绝 try);returnfalse;}// 幂等如果已经 try 过if(existing!null){System.out.println(【额度】幂等处理已 try);returntrue;}// 模拟预占额度MapString,ObjectrecordnewHashMap();record.put(status,TRY);record.put(frozenAmount,params.get(amount));db.put(businessId,record);returntrue;}Overridepublicbooleanconfirm(StringbusinessId){System.out.println(【额度】confirm 确认, bizIdbusinessId);MapString,Objectrecorddb.get(businessId);if(recordnull){// 可能是空回滚后的补偿没有 try 直接 confirm理论上不应该发生记录异常System.err.println(【额度】confirm 异常无 try 记录);returnfalse;}if(CONFIRMED.equals(record.get(status))){returntrue;// 幂等}record.put(status,CONFIRMED);returntrue;}Overridepublicbooleancancel(StringbusinessId){System.out.println(【额度】cancel 释放, bizIdbusinessId);MapString,Objectrecorddb.get(businessId);// 空回滚如果 try 还没有执行先占位一个 CANCELLED 记录阻止后续 tryif(recordnull){System.out.println(【额度】空回滚创建占位 CANCELLED 记录);MapString,ObjectemptynewHashMap();empty.put(status,CANCELLED);db.put(businessId,empty);returntrue;}if(CANCELLED.equals(record.get(status))){returntrue;// 幂等}// 正常回滚释放冻结额度record.put(status,CANCELLED);returntrue;}}3. 支付网关实现最关键的资源ServicepublicclassPaymentGatewayResourceimplementsTccResource{privatefinalConcurrentHashMapString,MapString,ObjectdbnewConcurrentHashMap();OverridepublicbooleantryReserve(StringbusinessId,MapString,Objectparams){System.out.println(【支付】try 预下单, bizIdbusinessId);MapString,Objectexistingdb.get(businessId);if(existing!nullCANCELLED.equals(existing.get(status))){System.out.println(【支付】检测到空回滚拒绝 try);returnfalse;}if(existing!null){returntrue;// 幂等}// 模拟预下单冻结收款账户入金能力MapString,ObjectrecordnewHashMap();record.put(status,TRY);record.put(amount,params.get(amount));db.put(businessId,record);returntrue;}Overridepublicbooleanconfirm(StringbusinessId){System.out.println(【支付】confirm 真正转账, bizIdbusinessId);MapString,Objectrecorddb.get(businessId);if(recordnull){System.err.println(【支付】confirm 异常无 try 记录);returnfalse;}if(CONFIRMED.equals(record.get(status))){returntrue;}// 实际资金划拨动作略record.put(status,CONFIRMED);returntrue;}Overridepublicbooleancancel(StringbusinessId){System.out.println(【支付】cancel 撤销预下单, bizIdbusinessId);MapString,Objectrecorddb.get(businessId);if(recordnull){// 空回滚MapString,ObjectemptynewHashMap();empty.put(status,CANCELLED);db.put(businessId,empty);returntrue;}if(CANCELLED.equals(record.get(status))){returntrue;}record.put(status,CANCELLED);returntrue;}}核心账户服务实现类似不再重复4. TCC 协调器最核心的流程编排ServicepublicclassLoanTccCoordinator{AutowiredprivateCreditLimitResourcecreditLimitResource;AutowiredprivateCoreAccountResourcecoreAccountResource;AutowiredprivatePaymentGatewayResourcepaymentGatewayResource;/** * 执行 TCC 分布式事务 * param businessId 业务唯一标识 * param params 请求参数 * return 是否成功 */publicbooleandoLoanDisburse(StringbusinessId,MapString,Objectparams){// 收集所有资源ListTccResourceresourcesArrays.asList(creditLimitResource,coreAccountResource,paymentGatewayResource);// 第一阶段TRY booleanallTrySuccesstrue;for(TccResourceresource:resources){try{booleanresultresource.tryReserve(businessId,params);if(!result){allTrySuccessfalse;break;}}catch(Exceptione){allTrySuccessfalse;break;}}// 第二阶段 if(allTrySuccess){// 全部 try 成功 → CONFIRMSystem.out.println( 所有 try 成功执行 confirm );for(TccResourceresource:resources){// 生产环境需要捕获异常并重试这里简化resource.confirm(businessId);}returntrue;}else{// 有 try 失败 → CANCEL只取消已 try 成功的资源System.out.println( try 失败执行 cancel );for(TccResourceresource:resources){resource.cancel(businessId);}returnfalse;}}}5. 控制器调用示例RestControllerpublicclassLoanController{AutowiredprivateLoanTccCoordinatorcoordinator;PostMapping(/loan/disburse)publicStringdisburse(RequestParamStringbizId,RequestParamBigDecimalamount){MapString,ObjectparamsnewHashMap();params.put(amount,amount);booleansuccesscoordinator.doLoanDisburse(bizId,params);returnsuccess?放款成功:放款失败;}}三、测试与观察启动后调用接口POST /loan/disburse?bizIdLOAN2024001amount50000控制台输出【额度】try 冻结, bizIdLOAN2024001 【账户】try 预开户, bizIdLOAN2024001 【支付】try 预下单, bizIdLOAN2024001 所有 try 成功执行 confirm 【额度】confirm 确认, bizIdLOAN2024001 【账户】confirm 激活, bizIdLOAN2024001 【支付】confirm 真正转账, bizIdLOAN2024001 放款成功如果人为在额度try时抛异常你会看到cancel被调用并且空回滚占位记录生效。四、关键学习点总结TCC 与 Saga 的本质区别TCC 强调try锁定资源资源在confirm前都是冻结态不实际发生资金或状态变更Saga 直接提交失败再补偿。空回滚cancel调用时可能try尚未到达网络延迟所以要在cancel中插入占位记录后续try检测到后直接拒绝。悬挂try在cancel之后到达通过占位记录防悬挂。幂等所有三个方法都必须支持幂等因为网络重试会导致重复调用。资源抽象TCC 要求每个参与方都提供三接口对银行外部系统可能要求改造但这是保证资金安全的代价。这套代码虽然运行在单机内存中但完整演示了 TCC 的核心思想。你可以把每个Resource替换成真正的 RPC 调用或数据库操作就是一个生产级别的 TCC 框架。