Druid source code reading 6-PreparedStatementPool source code and usage scenario analysis

This article has participated in the "Newcomer Creation Ceremony" activity, and started the road of Nuggets creation together

In the process of reading the source code of DruidDataSource, I found that DruidConnectionHolder has a special property PreparedStatementPool statementPool. According to experience, this is the cache for DruidPreparedStatement to cache. In the process of using PreparedStatement, since PreparedStatement parses SQL statements and injects parameters separately, after adding cache, requests for the same SQL and different parameters can be reused on the same connection.

1. Turn on the parameters

If you want to use psCache, you need to configure druid.maxPoolPreparedStatementPerConnectionSize greater than 0. In the configFromPropety method of the DruidDataSource source code:

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);
    }
}
复制代码

Through the setMaxPoolPreparedStatementPerConnectionSize method, when the configured parameter is greater than 0, modify the poolPreparedStatements to true.

public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
//maxPoolPreparedStatementPerConnectionSize 大于0,则设置poolPreparedStatements为true
    if (maxPoolPreparedStatementPerConnectionSize > 0) {
        this.poolPreparedStatements = true;
    } else {
        this.poolPreparedStatements = false;
    }

    this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}
复制代码

Then determine whether to create a cache by judging the state of this variable.

   public boolean isPoolPreparedStatements() {
        return poolPreparedStatements;
    }
复制代码

2.cache creation

After the opening parameter is turned on, the cache is created in the process of using prepareStatement. There is the following code in the prepareStatement method of DruidPooledConnection:

boolean poolPreparedStatements = holder.isPoolPreparedStatements();
//如果开启了psCache
if (poolPreparedStatements) {
    stmtHolder = holder.getStatementPool().get(key);
}
复制代码

And the getStatementPool method is as follows:

public PreparedStatementPool getStatementPool() {
    if (statementPool == null) {
        statementPool = new PreparedStatementPool(this);
    }
    return statementPool;
}
复制代码

When calling the getStatementPool method, if the statementPool is enabled, the cache will be initialized at this time. The initialization method is as follows:

public PreparedStatementPool(DruidConnectionHolder holder){
    this.dataSource = holder.getDataSource();
    int initCapacity = holder.getDataSource().getMaxPoolPreparedStatementPerConnectionSize();
    if (initCapacity <= 0) {
        initCapacity = 16;
    }
    map = new LRUCache(initCapacity);
}
复制代码

At this point, it can be found that the configuration of maxPoolPreparedStatementPerConnectionSize is the initial initCapacity of LRUCache. If this parameter is not configured, the default value is 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<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的回收还存在于多个场景中。

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不支持游标。 在分库分表的场景下,会导致大量的内存占用,也不建议使用。

Guess you like

Origin juejin.im/post/7078496075228643341