PreparedStatementCache的意义
Oracle支持游标,一个PreparedStatement对应服务器一个游标,如果PreparedStatement被缓存起来重复执 行,PreparedStatement没有被关闭,服务器端的游标就不会被关闭,性能提高非常显著。在类似SELECT * FROM T WHERE ID = ?这样的场景,性能可能是一个数量级的提升。
由于PreparedStatementCache性能提升明显,DruidDataSource、DBCP、JBossDataSource、WeblogicDataSource都实现了PreparedStatementCache。
PreparedStatementCache带来的问题
阿里巴巴在使用jboss连接池做PreparedStatementCache时,遇到了full gc频繁的问题。通过mat来分析jmap dump的结果,发现T4CPreparedStatement占内存很多,出问题的几个项目,有的300M,有的500M,最夸张的900M。这些应用 都是用jboss连接池访问Oracle数据库,T4CPreparedStatement是Oracle JDBC Driver的PreparedStatement一种实现。
oracle driver不是开源,通过逆向工程以及mat分析,发现其中占内存的是字段char[] defineChars,defineChars大小的计算公式是这样的:
defineChars大小 = rowSize * rowPrefetchCount
|
rowPrefetchCount在Oracle中,缺省值为10。
其中rowSize是执行查询设计的每一列的大小的和。计算公式是:
rowSize = col_1_size + col_2_size + ... + col_n_size
|
很悲剧,有些列数据类型是varchar2(4000),于是rowSize巨大,很多个表关联的SQL,rowSize可能高达数十K,再乘以 rowPrefetchCount,defineChars大小接近1M。可以想想,maxPoolSize设置为 30,PreparedStatementCacheSize设置为50的场景下,是可能导致PreparedStatementCache占据上G的内 存。
实际测试得到的结果如下:
varchar2(4000) | col_size 4000 chars |
clob -> col_size | col_size 4000 bytes |
实际占据内存的公式:
占据内存大小峰值 = defineChars大小 * PreparedStatementCacheSize * MaxPoolSize
|
我们实际分析,一个应用运行的SQL大约数百条,PreparedStatementCacheSize为 50,PreparedStatementCache的算法为LRU,很多的SQL执行之后,在Cache中HitCount为0就被淘汰了,淘汰的过 程,其位置从第1移到第50,这个漫长的过程导致了defineChars不能够被young gc回收。
Druid的解决方案
命中率低的SQL,不持有defineChars缓存。
OracleStatement提供了clearDefines方法,用于清理defineChars。DruidDataSource在 PreparedStatement加入Cache时,如果发现hitCount < 1,就调用clearDefines方法。通过这种方式,避免了命中率为0的PreparedStatement占用内存。测试表明,200个 SQL,PreparedStatementCacheSize为50的场景,能够减少一半的young gc。(生产环境还没做测试)
根据PreparedStatement执行的结果,计算RowPrefetch大小
添加PreparedStatementCache计数器
包括:
- PreparedStatementCacheCurrentSize
- PreparedStatementCacheDeleteCount 缓存删除次数
- PreparedStatementCacheHitCount 缓存命中次数
- PreparedStatementCacheMissCount 缓存不命中次数
- PreparedStatementCacheAccessCount 缓存访问次数
通过这五个计数器,我们清晰了解PreparedStatementCache的工作情况,然后根据实际情况调整。