去年在金融级分库分表项目中使用ShardingSphere时,我们团队曾遭遇一个隐蔽的数据库连接问题:系统在高峰时段频繁出现连接泄漏,最终导致连接池耗尽。经过72小时的问题追踪,发现根源在于ShardingSphere数据源Connection对象的元数据误用。
典型症状表现为:
ShardingSphere采用装饰器模式管理数据库连接,其核心类关系如下:
java复制ShardingConnection (装饰器)
|- MasterSlaveConnection
|- AbstractConnectionAdapter
|- PhysicalConnection (实际物理连接)
关键点在于:
问题常发生在以下两种场景:
场景一:连接关闭顺序错误
java复制try(Connection conn = dataSource.getConnection()) {
DatabaseMetaData meta = conn.getMetaData(); // 获取元数据
// 业务代码...
} // 自动关闭时,元数据持有连接引用导致关闭失败
场景二:跨方法传递元数据
java复制public DatabaseMetaData getSchemaMeta(Connection conn) {
return conn.getMetaData(); // 危险操作!
}
// 调用方
Connection conn = dataSource.getConnection();
DatabaseMetaData meta = getSchemaMeta(conn);
conn.close(); // 实际连接未释放
通过字节码增强+内存dump分析,我们发现:
关键引用链:
code复制ThreadLocal
└── DatabaseMetaData
└── ShardingConnection
└── PhysicalConnection (无法回收)
方案一:主动清理元数据引用
java复制try(Connection conn = dataSource.getConnection()) {
DatabaseMetaData meta = conn.getMetaData();
// 使用meta...
meta = null; // 显式断开引用
}
方案二:使用元数据代理
java复制public class SafeMetaDataProxy implements DatabaseMetaData {
private final DatabaseMetaData delegate;
@Override
public Connection getConnection() {
return null; // 切断反向引用
}
}
java复制@Override
public void close() {
clearMetaDataReferences(); // 新增方法
super.close();
}
java复制public class ConnectionLeakDetector {
public static void check(Connection conn) {
// 反射检查元数据引用
}
}
| 测试场景 | 连接泄漏率 | QPS下降幅度 |
|---|---|---|
| 原始版本 | 3.2% | 15% |
| 修复版本 | 0.05% | <1% |
建议增加以下监控项:
shardingsphere_connection_metadata_ref_countjdbc_connection_hold_time_seconds{type="with_metadata"}connection_leak_suspect_countjava复制public class SafeConnectionWrapper implements Connection {
private final Connection delegate;
@Override
public DatabaseMetaData getMetaData() {
return new WeakReferenceMetaData(delegate.getMetaData());
}
}
除DatabaseMetaData外,还需警惕以下JDBC对象:
通用检测方法:
java复制Field[] fields = jdbcObj.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.getType().equals(Connection.class)) {
// 存在连接引用风险
}
}
java复制private static final Cache<String, DatabaseMetaData> META_CACHE =
Caffeine.newBuilder()
.weakValues() // 使用弱引用
.build();
properties复制# Druid配置
druid.removeAbandoned=true
druid.removeAbandonedTimeoutMillis=300000
现象:连接数持续增长不释放
排查步骤:
jstack <pid> > thread.logbash复制grep -A 20 "ShardingConnection" thread.log
java复制jmap -histo:live <pid> | grep DatabaseMetaData
应急方案:
java复制ShardingSphereConfig.setMetaDataEnabled(false);
这个案例给我们的启示:
改进后的连接生命周期管理模型:
code复制[获取连接] -> [使用元数据] -> [显式解引用]
-> [关闭连接] -> [框架级清理]