Druid源码解析(六):PreparedStatementPool源码及使用场景分析
在阅读DruidDataSource源码的过程中,发现DruidConnectionHolder有个特别的属性PreparedStatementPool statementPool。 根据经验可知,这是DruidPreparedStatement进行缓存的cache。我们在使用PreparedStatement的过程中,由于PreparedStatement对sql语句的解析和参数的注入是分开的, 因此,加入cache之后,可以在同一个连接上,对相同sql,不同参数的请求进行复用。
一、开启参数
如果要使用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; }
二、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。
三、psCache结构
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缓存的数据结构。
public class LRUCache extends LinkedHashMap<PreparedStatementKey, PreparedStatementHolder> { private static final long serialVersionUID = 1L; public LRUCache(int maxSize){ super(maxSize, 0.75f, true); } //重写了removeEldestEntry方法 protected boolean removeEldestEntry(Entry<PreparedStatementKey, PreparedStatementHolder> 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的回收还存在于多个场景中。
四、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中的全部字段都相同。
五、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的实现。
六、总结
关于PreparedStatementCache的使用,在Druid中实际上cache是Connection级的。每个连接一个Cache。 一般在mysql中不建议使用这个Cache。mysql不支持游标。 在分库分表的场景下,会导致大量的内存占用,也不建议使用。
-----------------------------------------------------------
---------------------------------------------
朦胧的夜 留笔~~