我们的项目是使用Spring JDBC来操作DB的,通常是直接使用SimpleJdbcTemplate,一直以来也没发现什么问题。今天在做performance test的时候,发现内存增长很快,甚至出现了out of memory。把heap dump拉下来查看后,发现是这次新加的一个service出现了问题,但根源是因为NamedParameterJdbcTemplate中的HashMap导致的。
这里要先说明使用的Spring的版本是2.5.6,在3.0.3中已修复该问题。
查看调用的trace是service->SimpleJdbcTemplate->NamedParameterJdbcTemplate。一直没有仔细看过SimpleJdbcTemplate的源码,其实它基本都是通过调用NamedParameterJdbcTemplate来完成操作的。SimpleJdbcTemplate的初始化,其实就是初始化NamedParameterJdbcTemplate。
SimpleJdbcTemplate.java 1
2
3
4
5
6
7
8
9
10
11
public SimpleJdbcTemplate ( DataSource dataSource ) {
this . namedParameterJdbcOperations = new NamedParameterJdbcTemplate ( dataSource );
}
public SimpleJdbcTemplate ( JdbcOperations classicJdbcTemplate ) {
this . namedParameterJdbcOperations = new NamedParameterJdbcTemplate ( classicJdbcTemplate );
}
public SimpleJdbcTemplate ( NamedParameterJdbcOperations namedParameterJdbcTemplate ) {
this . namedParameterJdbcOperations = namedParameterJdbcTemplate ;
}
NamedParameterJdbcTemplate的作用是方便使用命名式的参数,以代替使用SQL中的‘?’。NamedParameterJdbcTemplate会将每次解析过后的SQL放在一个HashMap中,以起到cache的作用,而问题就出现这个Map上。
NamedParameterJdbcTemplate.java 1
2
3
4
5
6
7
8
9
10
11
12
private final Map parsedSqlCache = new HashMap ();
protected ParsedSql getParsedSql ( String sql ) {
synchronized ( this . parsedSqlCache ) {
ParsedSql parsedSql = ( ParsedSql ) this . parsedSqlCache . get ( sql );
if ( parsedSql == null ) {
parsedSql = NamedParameterUtils . parseSqlStatement ( sql );
this . parsedSqlCache . put ( sql , parsedSql );
}
return parsedSql ;
}
}
从上面的代码可以看到,NamedParameterJdbcTemplate不断的将解析过的SQL放入parsedSqlCache中,但并没有任何限制,而我们的service会根据不同的条件产生不同的SQL(条件参数比较多),同时这个SimpleJdbcTemplate也是share的,因此出现了out of memory的问题,当然,测试server的配置比较差也是其中一个原因。
这个问题可以参见Spring Bug 7237 ,该问题已在3.0.3中修复。
由于该阶段已不可能升级Spring的版本,潜在的风险太大,因此可自己实现一个NamedParameterJdbcTemplate,去override掉getParsedSql方法,可取消cache的作用,或参考Spring 3中的实现。
NamedParameterJdbcTemplate.java 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static final int DEFAULT_CACHE_LIMIT = 256 ;
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT ;
private final Map < String , ParsedSql > parsedSqlCache =
new LinkedHashMap < String , ParsedSql >( DEFAULT_CACHE_LIMIT , 0.75f , true ) {
@Override
protected boolean removeEldestEntry ( Map . Entry < String , ParsedSql > eldest ) {
return size () > getCacheLimit ();
}
};
public void setCacheLimit ( int cacheLimit ) {
this . cacheLimit = cacheLimit ;
}
public int getCacheLimit () {
return this . cacheLimit ;
}
protected ParsedSql getParsedSql ( String sql ) {
if ( getCacheLimit () <= 0 ) {
return NamedParameterUtils . parseSqlStatement ( sql );
}
synchronized ( this . parsedSqlCache ) {
ParsedSql parsedSql = this . parsedSqlCache . get ( sql );
if ( parsedSql == null ) {
parsedSql = NamedParameterUtils . parseSqlStatement ( sql );
this . parsedSqlCache . put ( sql , parsedSql );
}
return parsedSql ;
}
}
然后更改一下SimpleJdbcTemplate的实例化方式,先实例化一个改写过的NamedParameterJdbcTemplate,然后作为SimpleJdbcTemplate构造函数的参数来进行实例化。