上周排查一个线上问题时,发现日志中频繁出现No operations allowed after connection closed异常。这个报错发生在使用Druid连接池的Spring事务环境中,特别出现在事务提交后的扩展逻辑里。异常堆栈显示连接在事务提交阶段已经被关闭,但后续的TransactionSynchronizationManager回调中仍然尝试使用该连接。
这种问题在分布式事务、审计日志等场景特别常见——我们经常需要在事务成功提交后执行一些额外操作,比如:
Druid作为生产级连接池,其连接管理有几个关键特性:
connection.close()调用时会将连接返回到池中removeAbandoned等机制检测长时间未释放的连接isClosed()等状态java复制// DruidDataSource.getConnection()核心逻辑
public DruidPooledConnection getConnection() throws SQLException {
// 会校验连接有效性
if (connection.isClosed()) {
throw new SQLException("No operations allowed after connection closed");
}
return connection;
}
TransactionSynchronizationManager通过线程绑定的方式管理事务资源:
DataSource和ConnectionHolderregisterSynchronization注册回调beforeCommitbeforeCompletionafterCommitafterCompletionjava复制// 典型的事务同步使用示例
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 这里如果使用原连接会出问题!
jdbcTemplate.update("INSERT INTO audit_log...");
}
}
);
通过DEBUG跟踪发现,Druid连接在beforeCompletion阶段就被关闭了。这是因为:
DataSourceTransactionManager在doCleanupAfterCompletion中调用con.close()DruidPooledConnection.close()会立即将连接标记为closedafterCommit仍在当前线程执行java复制// 事务管理器清理资源的典型流程
protected void doCleanupAfterCompletion(Object transaction) {
// 释放连接
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(dataSource);
conHolder.getConnection().close(); // 连接在此关闭
// 但TransactionSynchronizationManager的同步器队列还未处理完
}
Spring事务的完整生命周期如下(关键阶段):
| 阶段 | 操作 | 连接状态 |
|---|---|---|
| beforeCommit | 同步器的beforeCommit() | 活跃 |
| beforeCompletion | 同步器的beforeCompletion() | 活跃 |
| 连接关闭 | DataSourceTransactionManager清理连接 | 已关闭 |
| afterCommit | 同步器的afterCommit() | 已关闭 → 报错 |
| afterCompletion | 同步器的afterCompletion() | 已关闭 |
对于必须在事务成功后执行的数据操作,最佳实践是使用独立的数据源:
java复制@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource mainDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
public DataSource auditDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
// 使用时
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
// 使用独立数据源
new JdbcTemplate(auditDataSource()).update(...);
}
}
);
通过自定义TransactionManager延迟连接关闭:
java复制public class DelayedCloseTransactionManager extends DataSourceTransactionManager {
@Override
protected void doCleanupAfterCompletion(Object transaction) {
// 先处理同步器
TransactionSynchronizationManager.invokeAfterCommit();
TransactionSynchronizationManager.invokeAfterCompletion(TransactionSynchronization.STATUS_COMMITTED);
// 最后关闭连接
super.doCleanupAfterCompletion(transaction);
}
}
警告:此方案需要严格测试,可能影响连接池性能
对于非强一致性的场景,可以采用异步处理:
java复制@Transactional
public void businessMethod() {
// 主事务操作...
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
eventPublisher.publishEvent(new AfterCommitEvent(data));
}
}
);
}
@EventListener
@Async
public void handleEvent(AfterCommitEvent event) {
// 异步处理
}
建议在预发布环境验证时重点关注:
sql复制-- Druid监控SQL
SELECT * FROM druid_datasource WHERE active_count > 0;
java复制@Test
public void testConcurrentAfterCommit() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
businessService.methodWithAfterCommit();
latch.countDown();
}).start();
}
latch.await();
}
在Spring Boot中配置Druid监控:
yaml复制spring:
datasource:
druid:
stat-view-servlet:
enabled: true
web-stat-filter:
enabled: true
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
关键监控项:
connection_hold_time_distributionactive_countwait_thread_count结合事务同步器可以实现本地事务与外部通知的一致性:
java复制public class ReliableEventPublisher {
public void publishAfterCommit(String event) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
realPublish(event);
}
}
);
} else {
realPublish(event);
}
}
}
在Saga模式中可以作为本地事务与协调器的桥梁:
java复制@Transactional
public void sagaStep() {
// 本地事务操作...
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
sagaCoordinator.report(status);
}
}
);
}
java复制// 优化后的注册方式
private static final Set<Object> registeredKeys = Collections.newSetFromMap(new ConcurrentHashMap<>());
public void registerIfAbsent(String key, Runnable task) {
if (registeredKeys.add(key)) {
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
try {
task.run();
} finally {
registeredKeys.remove(key);
}
}
}
);
}
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 偶尔出现连接关闭异常 | 同步器中使用原连接 | 改用独立数据源 |
| 回调方法未执行 | 事务未真正开启 | 检查@Transactional生效条件 |
| 回调执行两次 | 重复注册同步器 | 使用注册去重机制 |
| 线程阻塞 | 同步器中有同步IO | 改为异步处理 |
DEBUG断点:
DataSourceTransactionManager.doCleanupAfterCompletionTransactionSynchronization.invokeAfterCommit日志配置:
xml复制<logger name="org.springframework.transaction" level="DEBUG"/>
<logger name="com.alibaba.druid.pool" level="INFO"/>
bash复制watch org.springframework.transaction.support.TransactionSynchronizationManager invokeAfterCommit '{params,returnObj}' -x 3
TransactionSynchronizationManager本质是观察者模式的实现:
plantuml复制@startuml
class TransactionSynchronizationManager {
+ registerSynchronization()
+ getSynchronizations()
}
interface TransactionSynchronization {
+ afterCommit()
+ afterCompletion()
}
class MySynchronization {
+ afterCommit()
}
TransactionSynchronizationManager o-- TransactionSynchronization
TransactionSynchronization <|.. MySynchronization
@enduml
推荐采用ResourceHolder模式管理辅助连接:
java复制public class AuditResourceHolder implements ResourceHolder {
private Connection connection;
@Override
public void close() {
DataSourceUtils.releaseConnection(connection, auditDataSource);
}
}
// 使用示例
TransactionSynchronizationManager.bindResource(
auditDataSource, new AuditResourceHolder()
);
| 维度 | TransactionSynchronization | @TransactionalEventListener |
|---|---|---|
| 执行时机 | 严格的事务生命周期阶段 | 依赖事件总线时序 |
| 事务上下文 | 与原事务同线程 | 可配置线程池 |
| 异常处理 | 会传播回事务 | 独立错误处理 |
| 适用场景 | 强一致性需求 | 最终一致性场景 |
对于事务同步场景,不同连接池的表现:
| 特性 | Druid | HikariCP | Tomcat JDBC |
|---|---|---|---|
| 关闭行为 | 立即关闭 | 延迟关闭 | 可配置 |
| 同步器支持 | 需额外处理 | 兼容性好 | 中等 |
| 监控能力 | 完善 | 基础 | 有限 |
| 推荐场景 | 需要监控 | 高性能需求 | 简单场景 |
在实际项目中,我们最终采用了方案一(独立数据源)配合HikariCP作为审计日志的数据源。这个方案经过三个月线上验证,在日均百万级事务量的系统中保持零故障。关键配置如下:
yaml复制audit:
datasource:
jdbc-url: jdbc:mysql://audit-db:3306/audit
hikari:
maximum-pool-size: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
对于需要在事务成功后执行的逻辑,最重要的是理解事务资源的生命周期。不同连接池的实现差异、Spring事务的阶段划分,这些底层细节往往决定了方案的可靠性。建议在架构设计初期就考虑好事务扩展点的资源管理策略,避免在后期出现难以排查的边界条件问题。