Sharding-JDBC内存泄漏点分析

1,711 阅读2分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

前言

上篇文章,我们说了如何使用Sharding-JDBC处理分表的业务需求。但是在在实际应用中,发现在执行批量插入方法时,会有内存泄漏的问题。
事情是这样的,既然我们用到了分表,那说明数据的体量肯定不小。那对于大体量的数据库,肯定插入的次数很频繁。插入很频繁的需求,自然会想到用批量插入的方法去写数据,从而减少对数据库建立链接的次数。
当项目运行一段时间后,会发现内存持续上涨,dump堆栈日志来分析,发现问题了。 image.png 可以看出org.apache.shardingsphere.sql.parser的一个实例中org.springframework.boot.loader.LaunchedURLClassLoader占用805,016,520(89.93%)字节。内存在com.google.common.cache.LocalCache的一个实例中积累。我们再看看详细的报告。 image.png 发现用到了一个cache叫org.apache.shardingsphere.sql.parser.cache.SQLParseResultCache。那我们就接着看看这个SQLParseResultCache的源码。

/**
 * SQL parse result cache.
 */
public final class SQLParseResultCache {
    
    private final Cache<String, SQLStatement> cache = CacheBuilder.newBuilder().softValues().initialCapacity(2000).maximumSize(65535).build();
    
    /**
     * Put SQL and parse result into cache.
     * 
     * @param sql SQL
     * @param sqlStatement SQL statement
     */
    public void put(final String sql, final SQLStatement sqlStatement) {
        cache.put(sql, sqlStatement);
    }
    
    /**
     * Get SQL statement.
     *
     * @param sql SQL
     * @return SQL statement
     */
    public Optional<SQLStatement> getSQLStatement(final String sql) {
        return Optional.ofNullable(cache.getIfPresent(sql));
    }
    
    /**
     * Clear cache.
     */
    public synchronized void clear() {
        cache.invalidateAll();
    }
}

通过这个类的注释说明这是个SQL解析结果缓存。举个例子。 比如我们的插入sql是这样的。批量插入两条数据
INSERT INTO consume_record ( id , content ) VALUES ( ? , ? ) , ( ?, ? )
就会对这sql解析出来的SQLStatement,二者进行缓存。这样下次再插入数据时,只需要从缓存里拿对应解析结果就可以了。极大的减少了sql解析次数,大大提高效率。

可是一个很好的设计,为什么会导致内存泄漏呢?

可以试想一下,如果我们每次批量插入的条数不一样呢?这次是插入两条,下次是5条,再下次是1条。是不是每次的sql里的问号都不一样。这就会导致缓存不断增加。而通过上面的代码可以看到,缓存的最大size是65535。可想而知,批量插入如果不去控制批量插入的条数,就会导致这个SQL解析结果缓存爆满。

结论

所以我们在使用Sharding-JDBC时,如果用到了batchInsert,就需要固定一个batchSize,目的是为了让SQL解析结果缓存可以持续命中,既提高性能又避免了内存泄漏问题。