一、问题定义与成因分析
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事务中的悬挂和空回滚问题,确保分布式事务的可靠性和数据一致性。