hutool icon indicating copy to clipboard operation
hutool copied to clipboard

AbstractCache.putWithoutLock方法可能导致的外部资源泄露问题

Open IcoreE opened this issue 7 months ago • 0 comments

版本

<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中有大量的类似的情况, 会导致连接数耗尽、数据库服务器资源泄漏等问题。 Image

修改后的代码,已经提交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);
		}
	}

IcoreE avatar Jun 04 '25 07:06 IcoreE