在生产环境中,我们经常会遇到数据库连接池打满、锁等待超时甚至服务雪崩的问题。排查时往往发现,罪魁祸首并不是某条具体的“慢SQL”,而是“长事务”(Long Transaction)。
长事务是指持有数据库事务时间过长的操作。它可能包含多条执行很快的 SQL,但由于混合了 RPC 调用、复杂的业务逻辑计算或 IO 操作,导致事务长时间不提交。这会引发锁资源无法释放、MVCC 回滚段膨胀等严重后果。
本文将分享一种在 Java Spring Boot 体系下,完全不侵入业务代码,基于基础设施层实现的长事务与死锁监控方案。
核心设计思路
不同于简单的 AOP 耗时统计,生产级的事务监控需要解决以下几个痛点:
- 准确性:必须精准识别事务的物理提交/回滚时间,而不仅仅是方法执行时间。
- 去噪:Spring 事务支持嵌套(Propagation),我们只关心最外层事务,避免日志爆炸。
- 现场保留:发现长事务或死锁时,需要保留 TraceID、线程堆栈、当前 SQL 及用户上下文。
- 隔离性:监控数据的落库不能影响主业务事务,必须异步且独立。
整体架构图
我们利用 Spring 强大的事务同步机制(Transaction Synchronization)来实现这一目标。
技术实现拆解
1. 切入点:AOP 与 TransactionSynchronizationManager
最直观的做法是用 AOP 环绕通知(@Around)拦截 @Transactional 注解。但单纯的 AOP 无法感知事务是否真正提交(例如事务可能在 finally 块中才由 TransactionManager 完成 commit)。
更“资深”的做法是:在 AOP 中仅做入口拦截,利用 TransactionSynchronizationManager 注册一个回调钩子,监听事务的生命周期。
@Aspect
@Component
public class TransactionMonitorAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorTransaction(ProceedingJoinPoint pjp) throws Throwable {
// 1. 检查当前是否是新事务(最外层事务)
// TransactionAspectSupport.currentTransactionStatus().isNewTransaction() 在此处可能尚不可用
// 通常结合 ThreadLocal 标记或 TransactionSynchronizationManager.isActualTransactionActive() 判断
boolean isOuterTransaction = !TransactionSynchronizationManager.isSynchronizationActive();
if (isOuterTransaction) {
// 2. 初始化事务同步管理器,开启同步
TransactionSynchronizationManager.initSynchronization();
// 3. 注册我们的监控钩子
TransactionSynchronizationManager.registerSynchronization(
new TransactionMonitorSynchronization(System.currentTimeMillis(), TraceContext.getTraceId())
);
}
try {
return pjp.proceed();
} finally {
// Spring 的 TransactionInterceptor 会处理清理工作,这里主要是防守
}
}
}
2. 核心逻辑:TransactionMonitorSynchronization
这是实现监控的核心类,继承 TransactionSynchronizationAdapter。我们重点关注 afterCompletion 方法,它会在事务 commit 或 rollback 完成后被回调。
public class TransactionMonitorSynchronization extends TransactionSynchronizationAdapter {
private final long startTime;
private final String traceId;
private static final long SLOW_THRESHOLD = 5000L; // 5秒视为长事务
public TransactionMonitorSynchronization(long startTime, String traceId) {
this.startTime = startTime;
this.traceId = traceId;
}
@Override
public void afterCompletion(int status) {
long duration = System.currentTimeMillis() - startTime;
// 场景一:耗时超过阈值,标记为长事务
if (duration > SLOW_THRESHOLD) {
handleLongTransaction(duration, status);
}
// 场景二:虽然耗时短,但发生了死锁异常(需要结合 Exception 捕获机制)
// 注意:afterCompletion 只能拿到 status,具体的 Exception 需要在 AOP 层或 ExceptionHandler 中传递进来
}
private void handleLongTransaction(long duration, int status) {
// 采集当前线程信息、Request信息等
TransactionInfo info = new TransactionInfo();
info.setTraceId(traceId);
info.setDuration(duration);
info.setThreadName(Thread.currentThread().getName());
info.setStatus(status == STATUS_COMMITTED ? "COMMIT" : "ROLLBACK");
// 异步落库
AsyncLogService.save(info);
}
}
3. 精准捕获死锁与锁等待
Spring 的 DataAccessException 封装了底层的 JDBC 异常。为了区分是普通报错还是数据库死锁,我们需要解包异常链,检查 MySQL 的 Error Code。
- 1213: Deadlock found (死锁)
- 1205: Lock wait timeout exceeded (锁等待超时)
这一步通常可以在全局异常处理器或 AOP 的 catch 块中增强:
public boolean isLockError(Throwable t) {
Throwable root = ExceptionUtils.getRootCause(t); // 使用 Apache Commons Lang
if (root instanceof SQLException) {
int errorCode = ((SQLException) root).getErrorCode();
return errorCode == 1213 || errorCode == 1205;
}
return false;
}
4. 丰富的上下文采集
光知道“慢”是不够的,我们需要知道“为什么慢”。在 handleLongTransaction 中,我们可以采集以下关键信息:
- Java 线程信息:
Thread.currentThread().getName(),定位是哪个业务线程。 - Web 上下文:通过
RequestContextHolder获取请求 URL、User ID、客户端 IP。 - 全链路 TraceID:从 MDC (Mapped Diagnostic Context) 中提取
traceId,方便去日志系统(如 ELK、SkyWalking)查询完整的调用链路。 - 数据库现场(高级):如果权限允许,甚至可以在检测到异常时,查询 MySQL
performance_schema,记录当前的THREAD_ID和锁持有情况。
5. 异步落库与新事务
监控日志的保存绝对不能影响主业务事务,也不能复用当前已经结束(或回滚)的事务连接。
因此,落库操作必须:
- 异步执行:使用线程池或
@Async。 - 独立事务:使用
Propagation.REQUIRES_NEW。
@Service
public class AsyncLogService {
@Async("monitorThreadPool") // 专用线程池
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(TransactionInfo info) {
// 保存到独立的监控表或发送到 Kafka/ES
transactionHistoryRepository.save(info);
}
}
6、 业务开发中的长事务治理建议
监控只是手段,解决问题才是目的。在日常开发中,为了避免触发上述报警,建议大家遵循以下原则:
1. 事务颗粒度“最小化”
错误示范:
@Transactional
public void buyTicket() {
// 1. 远程调用查询库存 -> 耗时不可控!
inventoryService.check();
// 2. 数据库操作
db.update();
// 3. 发送邮件通知 (IO操作) -> 耗时!
emailService.send();
}
正确做法:
禁止将远程调用、IO 操作放入事务中,可做异步化处理,将非 DB 操作移出 @Transactional 方法。
2. 批量操作分批次
大批量数据(如 10 万条)的 Update/Delete,不要在一个事务中做完。建议分批提交(如每 1000 条 commit 一次),避免长时间占用 undo log 和锁。
3. 警惕死锁
严格规范表资源的访问顺序。如果业务 A 先锁表 1 再锁表 2,业务 B 必须保持同样的顺序,严禁反向操作。
总结
这套方案通过组合 Spring 的 AOP 和 TransactionSynchronizationManager,实现了对业务代码零侵入的监控。其核心技术价值在于:
- 生命周期对齐:使用
afterCompletion确保统计的是事务持有的完整物理时间(包括 commit 的 IO 时间),比单纯的方法耗时统计更准确。 - 最外层识别:利用 Spring 事务状态判断,避免了嵌套事务导致的监控数据冗余。
- 异常归因:通过底层 SQL Error Code 解析,精准区分“业务慢”和“数据库锁冲突”。
- 全链路串联:集成 TraceID,打通了应用监控与数据库监控的壁垒。