在阅读DruidDataSource源码的过程中,发现DruidConnectionHolder有个特别的属性PreparedStatementPool statementPool。
根据经验可知,这是DruidPreparedStatement进行缓存的cache。我们在使用PreparedStatement的过程中,由于PreparedStatement对sql语句的解析和参数的注入是分开的,
因此,加入cache之后,可以在同一个连接上,对相同sql,不同参数的请求进行复用。
1.开启参数
如果要使用psCache,那么需要配置druid.maxPoolPreparedStatementPerConnectionSize大于0。
在DruidDataSource源码的configFromPropety方法中:
String property = properties.getProperty("druid.maxPoolPreparedStatementPerConnectionSize");
if (property != null && property.length() > 0) {
try {
int value = Integer.parseInt(property);
//set 配置的参数
this.setMaxPoolPreparedStatementPerConnectionSize(value);
} catch (NumberFormatException e) {
LOG.error("illegal property 'druid.maxPoolPreparedStatementPerConnectionSize'", e);
}
}
通过setMaxPoolPreparedStatementPerConnectionSize方法,当配置的参数大于0的时候,修改poolPreparedStatements为true。
public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
//maxPoolPreparedStatementPerConnectionSize 大于0,则设置poolPreparedStatements为true
if (maxPoolPreparedStatementPerConnectionSize > 0) {
this.poolPreparedStatements = true;
} else {
this.poolPreparedStatements = false;
}
this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}
之后通过判断这个变量的状态来确定是否创建缓存。
public boolean isPoolPreparedStatements() {
return poolPreparedStatements;
}
2.cache创建
在开启参数打开之后,使用prepareStatement的过程中,创建cache。
在DruidPooledConnection的prepareStatement方法中有如下代码:
boolean poolPreparedStatements = holder.isPoolPreparedStatements();
//如果开启了psCache
if (poolPreparedStatements) {
stmtHolder = holder.getStatementPool().get(key);
}
而getStatementPool方法如下:
public PreparedStatementPool getStatementPool() {
if (statementPool == null) {
statementPool = new PreparedStatementPool(this);
}
return statementPool;
}
调用getStatementPool方法的时候,如果开启了statementPool,此时就会对这个cache进行初始化。
初始化方法如下:
public PreparedStatementPool(DruidConnectionHolder holder){
this.dataSource = holder.getDataSource();
int initCapacity = holder.getDataSource().getMaxPoolPreparedStatementPerConnectionSize();
if (initCapacity <= 0) {
initCapacity = 16;
}
map = new LRUCache(initCapacity);
}
此时可以发现,maxPoolPreparedStatementPerConnectionSize的配置就是LRUCache初始的initCapacity。
如果该参数不配置,默认的值为10:
protected volatile int maxPoolPreparedStatementPerConnectionSize = 10;
也就是说,如果不配置druid.maxPoolPreparedStatementPerConnectionSize,那么系统将默认开启psCache。默认的长度为10。
3.psCache结构
psCache的构成非常简单,其内部就一个LRUCache的map。
public class PreparedStatementPool {
private final static Log LOG = LogFactory.getLog(PreparedStatementPool.class);
//cache结构
private final LRUCache map;
//指向dataSource的指针
private final DruidAbstractDataSource dataSource;
}
LRUCache的结构:
LRUCache本质上是一个LinkedHashMap,学习过LinkedHashMap源码就会知道,实际上这是一个非常适合LRU缓存的数据结构。可以参考LinkedHashMap源码分析.
public class LRUCache extends LinkedHashMap {
private static final long serialVersionUID = 1L;
public LRUCache(int maxSize){
super(maxSize, 0.75f, true);
}
//重写了removeEldestEntry方法
protected boolean removeEldestEntry(Entry eldest) {
//确认remove状态
boolean remove = (size() > dataSource.getMaxPoolPreparedStatementPerConnectionSize());
//关闭statement
if (remove) {
closeRemovedStatement(eldest.getValue());
}
return remove;
}
}
重写removeEldestEntry方法的目的是在LinkedHashMap中调用remove移除Entry的时候,对缓存的statement进行关闭。这样就能完成对statement的回收。
需要注意的是,在使用LRUCache的时候,并没有加锁,也就意味着LRUCache是非线程安全的。实际上由于cache对连接生效,一个connection就会创建一个LRUCache。
而连接又是单线程操作,因此不会存在线程安全问题。
当然,对于CRUCache中PreparedStatement的回收还存在于多个场景中。
4.Entry中的PreparedStatementKey
在Entry中,key的类型PreparedStatementKey,value的类型为PreparedStatementHolder。
public static class PreparedStatementKey {
//sql语句
protected final String sql;
//catlog name
protected final String catalog;
//method 参见MethodType枚举类
protected final MethodType methodType;
//返回值类型
public final int resultSetType;
public final int resultSetConcurrency;
public final int resultSetHoldability;
public final int autoGeneratedKeys;
private final int[] columnIndexes;
private final String[] columnNames;
... ...
}
需要注意的是,PreparedStatementKey主要是来标识两个要执行的sql语句是否为同一个PreparedStatement。
这个生成hashcode的方法也非常特别:
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((sql == null) ? 0 : sql.hashCode());
result = prime * result + ((catalog == null) ? 0 : catalog.hashCode());
result = prime * result + ((methodType == null) ? 0 : methodType.hashCode());
result = prime * result + resultSetConcurrency;
result = prime * result + resultSetHoldability;
result = prime * result + resultSetType;
result = prime * result + autoGeneratedKeys;
result = prime * result + Arrays.hashCode(columnIndexes);
result = prime * result + Arrays.hashCode(columnNames);
return result;
}
如果要确认两个语句是否可以为同一个statement,那么需要PreparedStatementKey中的全部字段都相同。
5.Entry中的PreparedStatementHolder
PreparedStatementHolder是一个对PreparedStatement的扩展类。
其属性如下:
public final PreparedStatementKey key;
public final PreparedStatement statement;
private int hitCount = 0;
//fetch峰值
private int fetchRowPeak = -1;
private int defaultRowPrefetch = -1;
private int rowPrefetch = -1;
private boolean enterOracleImplicitCache = false;
private int inUseCount = 0;
private boolean pooling = false;
这个类主要扩展了部分统计参数。当调用PreparedStatement的时候,会调用这些参数对应的统计方法。
通过源码可以发现,作者特别喜欢通过Holder来对java sql包提供的对象进行扩展。当然这也与druid连接池的定位是分不开的,druid最大的有点就是其监控功能非常完善。这些监控中统计的数据就是通过这些Holder来实现的。
如果我们在业务系统的开发过程中需要增加一些监控的参数,也可以参考Druid的实现。
6.总结
关于PreparedStatementCache的使用,在Druid中实际上cache是Connection级的。每个连接一个Cache。
一般在mysql中不建议使用这个Cache。mysql不支持游标。
在分库分表的场景下,会导致大量的内存占用,也不建议使用。