AbstractCache.putWithoutLock方法可能导致的外部资源泄露问题
版本
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
问题源码:AbstractCache.putWithoutLock(K key, V object, long timeout)
protected void putWithoutLock(K key, V object, long timeout) {
CacheObj<K, V> co = new CacheObj<>(key, object, timeout);
if (timeout != 0) {
existCustomTimeout = true;
}
final MutableObj<K> mKey = MutableObj.of(key);
// issue#3618 对于替换的键值对,不做满队列检查和清除
if (cacheMap.containsKey(mKey)) {
// 存在相同key,覆盖之
cacheMap.put(mKey, co);
} else {
if (isFull()) {
pruneCache();
}
cacheMap.put(mKey, co);
}
}
存在的问题:如果缓存中存放一些资源连接对象(数据库连接对象,网络socket连接等),可能导致资源泄露。
@Test
public void cachePutMethodTest() throws Exception {
Cache<String, Connection> connectionCache = CacheUtil.newLFUCache(3);
//设置监听器,触发回调释放资源
connectionCache.setListener(new ConnCacheListener());
Class.forName("com.mysql.jdbc.Driver");
Connection connection1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "root");
Connection connection2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "root");
Connection connection3 = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "root");
Connection connection4 = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "root");
connectionCache.put("key1",connection1);
connectionCache.put("key2",connection2);
connectionCache.put("key3",connection3);
//此时缓存满了,缓存中再次put放入key="key1"的对象
connectionCache.put("key1",connection4); //该行代码最终会调用AbstractCache.putWithoutLock(K key, V object, long timeout)方法
}
class ConnCacheListener implements CacheListener<String,Connection>{
@Override
public void onRemove(String key, Connection conn) {
//监听器,触发回调释放资源
try {
if (!conn.isClosed()) {
conn.close(); // 资源手动释放
}
} catch (SQLException e) {
logger.error("关闭连接失败", e);
}
}
}
原因解释:
protected void putWithoutLock(K key, V object, long timeout) {
//省略代码....
if (cacheMap.containsKey(mKey)) { //问题代码出现的地方
// 存在相同key,覆盖
cacheMap.put(mKey, co);
} else {
//省略代码....
}
}
开始创建了size=3的LFUCache,并且给LFUCache设置了监听器,当LFUCache中对象被移除时候,触发自定义的监听器的资源处理工作。当执行 connectionCache.put("key1",connection4)时候会调用AbstractCache.putWithoutLock(K key, V object, long timeout)方法。在putWithoutLock中,cacheMap.containsKey(mKey)当检测到key已存在时,直接覆盖了旧的对象CacheObj(key1,connection1),而旧的缓存对象没有经过任何处理(比如调用onRemove触发监听器处理)就被丢弃了。CacheObj(key1,connection1)旧对象断开了和cacheMap的连接,旧对象会被垃圾回收器gc回收,但是旧对象关联的资源connection1,数据库连接资源永久泄露了,没有被手动关闭。
这可能导致资源泄漏(如果缓存的对象持有外部资源,而监听器负责释放这些资源,那么没有调用监听器就会导致资源泄漏)。准确地说,不是内存泄漏(因为旧对象会被GC回收),而是资源泄漏(没有触发监听器释放外部资源)。
运行结果:
# 查看数据库连接
SHOW PROCESSLIST;
下图展示了上述测试代码执行完之后,数据库中connection1连接没有被释放,如果Cache中有大量的类似的情况,
会导致连接数耗尽、数据库服务器资源泄漏等问题。
修改后的代码,已经提交PR: https://github.com/chinabugotech/hutool/pull/3958
protected void putWithoutLock(K key, V object, long timeout) {
CacheObj<K, V> co = new CacheObj<>(key, object, timeout);
if (timeout != 0) {
existCustomTimeout = true;
}
final MutableObj<K> mKey = MutableObj.of(key);
// issue#3618 对于替换的键值对,不做满队列检查和清除
if (cacheMap.containsKey(mKey)) {
//1. 先处理旧对象,主动触发监听器释放资源
CacheObj<K, V> oldObj = cacheMap.get(mKey);
if (oldObj != null) {
onRemove(oldObj.key, oldObj.obj);
cacheMap.remove(mKey);
}
//2. 触发监听器和删除旧缓存后,put新对象
cacheMap.put(mKey, co);
} else {
if (isFull()) {
pruneCache();
}
cacheMap.put(mKey, co);
}
}