大疆Java面试被问:TCC事务的悬挂、空回滚问题解决方案

一、问题定义与成因分析

1. 悬挂问题(Hanging)

定义 :Try阶段预留的资源迟迟无法释放,导致资源长时间被占用
发生场景

text

复制

下载

复制代码
正常流程:Try → Confirm/Cancel
悬挂流程:Try → 网络超时 → 触发Cancel → Cancel未执行 → 资源锁定

根本原因

  • Try阶段执行成功,但网络超时导致事务管理器未收到响应

  • 事务管理器触发全局回滚,调用Cancel

  • Cancel调用失败或未执行,预留资源无法释放

2. 空回滚问题(Empty Rollback)

定义 :Cancel接口被调用时,对应的Try操作并未执行
发生场景

text

复制

下载

复制代码
正常流程:Try失败 → 调用Cancel
空回滚:Try未执行(网络超时) → 事务管理器超时 → 调用Cancel

根本原因

  • Try阶段因网络问题未到达服务方

  • 事务管理器超时触发全局回滚

  • Cancel接口被调用,但无Try记录可回滚

二、悬挂问题解决方案

方案一:状态标记 + 过期清理

java

复制

下载

复制代码
@Component
public class TccHangingResolver {
    
    @Autowired
    private TransactionLogService logService;
    
    /**
     * 1. Try阶段:记录事务状态
     */
    @Transactional(rollbackFor = Exception.class)
    public void tryPhase(String xid, String bizId) {
        // 检查是否已有未完成的事务
        TransactionLog existing = logService.getUnfinished(xid);
        if (existing != null) {
            // 已有未完成事务,拒绝新请求
            throw new TccHangingException("存在悬挂事务,请等待处理");
        }
        
        // 插入事务记录
        TransactionLog log = new TransactionLog();
        log.setXid(xid);
        log.setBizId(bizId);
        log.setStatus(TccStatus.TRYING);
        log.setExpireTime(LocalDateTime.now().plusMinutes(30)); // 30分钟过期
        log.setCreatedTime(LocalDateTime.now());
        logService.save(log);
        
        // 执行业务Try逻辑
        // ...
    }
    
    /**
     * 2. Confirm阶段:更新状态
     */
    public void confirmPhase(String xid) {
        TransactionLog log = logService.getByXid(xid);
        if (log == null || log.getStatus() != TccStatus.TRYING) {
            throw new IllegalStateException("事务状态异常");
        }
        
        // 更新为完成状态
        log.setStatus(TccStatus.CONFIRMED);
        log.setFinishedTime(LocalDateTime.now());
        logService.update(log);
        
        // 执行业务Confirm逻辑
        // ...
    }
    
    /**
     * 3. 定时任务:清理悬挂事务
     */
    @Scheduled(fixedDelay = 60000) // 每分钟执行一次
    public void cleanHangingTransactions() {
        List<TransactionLog> hangingLogs = logService.findHangingTransactions();
        
        for (TransactionLog log : hangingLogs) {
            if (isExpired(log)) {
                // 执行补偿逻辑
                compensate(log);
                
                // 标记为已处理
                log.setStatus(TccStatus.COMPENSATED);
                logService.update(log);
            }
        }
    }
    
    /**
     * 4. 补偿策略
     */
    private void compensate(TransactionLog log) {
        try {
            // 根据业务类型执行不同的补偿逻辑
            switch (log.getBizType()) {
                case "ORDER":
                    orderService.compensateOrder(log.getBizId());
                    break;
                case "INVENTORY":
                    inventoryService.releaseInventory(log.getBizId());
                    break;
                // ... 其他业务补偿
            }
        } catch (Exception e) {
            // 记录异常,人工干预
            log.error("补偿执行失败: {}", log.getXid(), e);
            alertService.sendAlert("TCC补偿失败", log);
        }
    }
}

方案二:预检查 + 防悬挂锁

java

复制

下载

复制代码
public class HangingPreventionLock {
    
    private final RedissonClient redisson;
    
    /**
     * 获取防悬挂锁
     */
    public boolean tryAcquireHangingLock(String xid, long timeoutSeconds) {
        String lockKey = "tcc:hanging:lock:" + xid;
        RLock lock = redisson.getLock(lockKey);
        
        try {
            return lock.tryLock(timeoutSeconds, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
    
    /**
     * Try阶段前置检查
     */
    public boolean preCheck(String xid) {
        // 1. 检查是否已有Confirm/Cancel记录
        if (hasFinished(xid)) {
            return false; // 已结束,拒绝Try
        }
        
        // 2. 检查是否在取消中
        if (isCancelling(xid)) {
            return false; // 正在取消,拒绝Try
        }
        
        // 3. 获取锁,防止并发问题
        if (!tryAcquireHangingLock(xid, 10)) {
            throw new TccLockException("获取防悬挂锁失败");
        }
        
        return true;
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

三、空回滚问题解决方案

方案一:Try状态检查 + 幂等处理

java

复制

下载

复制代码
@Component
public class EmptyRollbackPreventer {
    
    @Autowired
    private TransactionStateStorage stateStorage;
    
    /**
     * Cancel接口实现
     */
    @Transactional
    public void cancel(String xid, String bizId) {
        // 1. 检查Try是否执行过
        TccState state = stateStorage.getState(xid);
        
        if (state == null || state.getStatus() == TccStatus.INIT) {
            // Try未执行,属于空回滚
            log.warn("检测到空回滚: xid={}", xid);
            
            // 记录空回滚日志
            recordEmptyRollback(xid);
            
            // 返回成功,保证幂等性
            stateStorage.saveState(xid, TccStatus.CANCELLED_EMPTY);
            return;
        }
        
        if (state.getStatus() == TccStatus.CANCELLED 
            || state.getStatus() == TccStatus.CANCELLED_EMPTY) {
            // 已取消过,幂等返回
            log.info("Cancel已执行过,幂等返回: xid={}", xid);
            return;
        }
        
        if (state.getStatus() != TccStatus.TRY_SUCCESS) {
            throw new IllegalStateException("事务状态异常,无法Cancel");
        }
        
        // 2. 执行实际的Cancel逻辑
        try {
            businessCancel(bizId);
            
            // 3. 更新状态
            stateStorage.saveState(xid, TccStatus.CANCELLED);
            
        } catch (Exception e) {
            // 记录失败,需要重试
            stateStorage.saveState(xid, TccStatus.CANCEL_FAILED);
            throw e;
        }
    }
    
    /**
     * Try接口:必须记录状态
     */
    @Transactional
    public void tryBusiness(String xid, String bizId) {
        // 1. 先记录状态
        stateStorage.saveState(xid, TccStatus.TRYING);
        
        try {
            // 2. 执行业务逻辑
            doTryBusiness(bizId);
            
            // 3. 标记Try成功
            stateStorage.saveState(xid, TccStatus.TRY_SUCCESS);
            
        } catch (Exception e) {
            // 标记Try失败
            stateStorage.saveState(xid, TccStatus.TRY_FAILED);
            throw e;
        }
    }
}

方案二:预写日志 + 状态确认

java

复制

下载

复制代码
public class PreWriteLogSolution {
    
    private final TransactionLogDAO logDAO;
    
    /**
     * 方案:两阶段状态确认
     */
    public void processTransaction(String xid) {
        // 第一阶段:预写状态日志
        preWriteLog(xid);
        
        try {
            // 第二阶段:执行业务
            executeBusiness(xid);
            
            // 第三阶段:确认日志
            confirmLog(xid);
            
        } catch (Exception e) {
            // 第四阶段:回滚检查
            rollbackWithCheck(xid);
        }
    }
    
    private void preWriteLog(String xid) {
        TransactionLog log = new TransactionLog();
        log.setXid(xid);
        log.setStatus(TccStatus.PREPARED);
        log.setCreateTime(System.currentTimeMillis());
        log.setExpireTime(System.currentTimeMillis() + 300000); // 5分钟过期
        
        // 插入预写日志
        logDAO.insert(log);
    }
    
    private void rollbackWithCheck(String xid) {
        // 检查预写日志是否存在
        TransactionLog preLog = logDAO.selectByXid(xid);
        
        if (preLog == null || preLog.getStatus() != TccStatus.PREPARED) {
            // 没有预写日志,属于空回滚
            logDAO.insertEmptyRollbackLog(xid);
            return;
        }
        
        // 执行实际的回滚逻辑
        doRollback(xid);
        
        // 更新日志状态
        preLog.setStatus(TccStatus.ROLLBACKED);
        logDAO.update(preLog);
    }
}

四、综合解决方案:防悬挂空回滚框架

完整架构设计

java

复制

下载

复制代码
@Slf4j
@Component
public class TccProblemSolver {
    
    @Autowired
    private TransactionStateService stateService;
    @Autowired
    private Compensator compensator;
    @Autowired
    private ScheduleExecutor scheduler;
    
    /**
     * TCC Try阶段 - 增强版
     */
    public void enhancedTry(String xid, String bizId, TccAction tryAction) {
        // 1. 防悬挂检查
        if (stateService.hasUnfinishedTransaction(bizId)) {
            throw new TccHangingRiskException("业务ID存在未完成事务");
        }
        
        // 2. 获取分布式锁
        DistributedLock lock = lockManager.acquireLock(bizId);
        try {
            // 3. 记录开始状态
            stateService.recordTryStart(xid, bizId);
            
            // 4. 执行业务Try
            tryAction.execute();
            
            // 5. 记录Try成功
            stateService.recordTrySuccess(xid);
            
        } catch (Exception e) {
            // 6. 记录Try失败
            stateService.recordTryFailure(xid, e.getMessage());
            throw e;
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * TCC Cancel阶段 - 增强版
     */
    public void enhancedCancel(String xid, String bizId, TccAction cancelAction) {
        // 1. 检查是否为空回滚
        TransactionState state = stateService.getState(xid);
        
        if (state == null || state.getStatus() == Status.INITIAL) {
            // 空回滚处理
            logEmptyRollback(xid);
            stateService.markAsEmptyRollback(xid);
            return;
        }
        
        // 2. 幂等检查
        if (state.getStatus() == Status.CANCELLED 
            || state.getStatus() == Status.CANCELLED_EMPTY) {
            log.info("Cancel已执行,幂等返回: xid={}", xid);
            return;
        }
        
        // 3. 状态验证
        if (state.getStatus() != Status.TRY_SUCCESS) {
            throw new IllegalStateException("事务状态不允许Cancel: " + state.getStatus());
        }
        
        // 4. 执行业务Cancel
        try {
            cancelAction.execute();
            stateService.markAsCancelled(xid);
        } catch (Exception e) {
            stateService.markAsCancelFailed(xid);
            throw e;
        }
    }
    
    /**
     * 悬挂事务检测与处理
     */
    @Scheduled(fixedDelay = 30000)
    public void detectAndHandleHanging() {
        // 1. 查询过期未完成的事务
        List<Transaction> hangingTransactions = 
            stateService.findHangingTransactions(5); // 超过5分钟
        
        for (Transaction tx : hangingTransactions) {
            try {
                // 2. 根据业务类型执行补偿
                CompensationResult result = compensator.compensate(tx);
                
                if (result.isSuccess()) {
                    // 3. 标记为已补偿
                    stateService.markAsCompensated(tx.getXid());
                    log.info("悬挂事务补偿成功: {}", tx.getXid());
                } else {
                    // 4. 补偿失败,升级处理
                    handleCompensationFailure(tx, result);
                }
            } catch (Exception e) {
                log.error("悬挂事务处理异常: {}", tx.getXid(), e);
            }
        }
    }
    
    /**
     * 状态机设计
     */
    public enum Status {
        INITIAL,          // 初始状态
        TRY_STARTED,      // Try开始
        TRY_SUCCESS,      // Try成功
        TRY_FAILED,       // Try失败
        CONFIRMING,       // Confirm中
        CONFIRMED,        // Confirm成功
        CANCELLING,       // Cancel中
        CANCELLED,        // Cancel成功
        CANCELLED_EMPTY,  // 空回滚
        HANGING,          // 悬挂中
        COMPENSATED       // 已补偿
    }
}

五、数据库表设计

事务状态表

sql

复制

下载

复制代码
CREATE TABLE tcc_transaction (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    xid VARCHAR(64) NOT NULL COMMENT '全局事务ID',
    biz_id VARCHAR(64) NOT NULL COMMENT '业务ID',
    biz_type VARCHAR(32) NOT NULL COMMENT '业务类型',
    status VARCHAR(32) NOT NULL COMMENT '事务状态',
    
    -- 时间字段
    create_time DATETIME NOT NULL COMMENT '创建时间',
    update_time DATETIME NOT NULL COMMENT '更新时间',
    expire_time DATETIME NOT NULL COMMENT '过期时间',
    finish_time DATETIME COMMENT '完成时间',
    
    -- 控制字段
    retry_count INT DEFAULT 0 COMMENT '重试次数',
    max_retry INT DEFAULT 3 COMMENT '最大重试次数',
    version INT DEFAULT 1 COMMENT '版本号',
    
    -- 索引
    UNIQUE KEY uk_xid (xid),
    KEY idx_biz_id (biz_id),
    KEY idx_status_expire (status, expire_time),
    KEY idx_create_time (create_time)
) COMMENT 'TCC事务状态表';

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】

防悬挂锁表

sql

复制

下载

复制代码
CREATE TABLE tcc_hanging_lock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    lock_key VARCHAR(128) NOT NULL COMMENT '锁Key',
    lock_holder VARCHAR(64) NOT NULL COMMENT '锁持有者',
    expire_time DATETIME NOT NULL COMMENT '锁过期时间',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE KEY uk_lock_key (lock_key),
    KEY idx_expire_time (expire_time)
) COMMENT '防悬挂锁表';

六、最佳实践总结

1. 悬挂问题防护要点

  • ✅ Try前检查是否存在未完成事务

  • ✅ 设置合理的资源预留过期时间

  • ✅ 实现补偿任务定期清理悬挂资源

  • ✅ 使用分布式锁防止并发悬挂

2. 空回滚问题防护要点

  • ✅ Try阶段必须记录状态日志

  • ✅ Cancel前检查Try是否执行成功

  • ✅ 实现幂等Cancel接口

  • ✅ 记录空回滚日志用于监控

3. 监控与告警

java

复制

下载

复制代码
// 关键监控指标
public class TccMetrics {
    // 悬挂事务数量
    @Meter("tcc.hanging.count")
    private Meter hangingCount;
    
    // 空回滚比例
    @Gauge("tcc.empty_rollback.ratio")
    private Double emptyRollbackRatio;
    
    // 补偿成功率
    @Gauge("tcc.compensation.success_rate")
    private Double compensationSuccessRate;
    
    // 事务平均耗时
    @Timer("tcc.transaction.duration")
    private Timer transactionDuration;
}

4. 配置建议

yaml

复制

下载

复制代码
tcc:
  hanging:
    check-interval: 30000    # 悬挂检查间隔30秒
    expire-time: 300000      # 资源过期时间5分钟
    max-retry: 3             # 最大重试次数
    
  empty-rollback:
    enabled: true            # 启用空回滚防护
    log-path: /logs/tcc/empty-rollback.log
    
  compensation:
    enabled: true            # 启用自动补偿
    thread-pool-size: 10     # 补偿线程池大小
    batch-size: 100          # 批量处理大小

通过以上综合解决方案,可以有效预防和解决TCC事务中的悬挂和空回滚问题,确保分布式事务的可靠性和数据一致性。

相关推荐
diving deep35 分钟前
脚本速览-python
开发语言·python
workflower39 分钟前
使用大语言模型处理用户需求
大数据·人工智能·设计模式·重构·动态规划
一生了无挂1 小时前
Java处理JSON技巧教学(从基础到高阶实战全覆盖)
java·开发语言·json
李白的天不白1 小时前
使用 SmartAdmin 进行前后端开发
java·前端
swordbob1 小时前
Spring 单例 Bean 是线程安全的吗?
java·开发语言
CodePlayer竟然被占用了2 小时前
没有生态的大模型不算前沿
人工智能
米小虾2 小时前
AI Agent 开发实战:2026年主流框架与MCP协议深度解析
人工智能·agent
米小虾2 小时前
2026年AI大模型半年报:从"参数军备"到"生态为王",谁在领跑下半场?
人工智能
m0_571186602 小时前
第五十周周报
人工智能