2022-05-12 Druid源码阅读——poolPreparedStatements是如何控制缓存游标的?

在Druid预编译SQL时,会检查是否开启poolPreparedStatements参数缓存预编译SQL,这些预编译的SQL存放在哪里?在什么时候进行存放?

1.如何开启poolPreparedStatements(PSCache)功能

需要注意的是,maxPoolPreparedStatementPerConnectionSize的加载顺序在poolPreparedStatements之后,如果将maxPoolPreparedStatementPerConnectionSize设置为负数,则poolPreparedStatements无法生效。

  • druid.poolPreparedStatements配置项设置为true,此时maxPoolPreparedStatementPerConnectionSize默认为10。
  • druid.maxPoolPreparedStatementPerConnectionSize参数值设置为>0的整数,也会开启此功能

2.预编译的SQL缓存在哪里?

通过分析预编译SQL部分,可以发现这些内容被存放在连接持有者的statementPool属性中。

public final class DruidConnectionHolder {
	protected PreparedStatementPool statementPool;
    /**
     * 获得语句池
     */
    public PreparedStatementPool getStatementPool() {
        //如果语句池为空,则新建语句池,否则返回当前所持有的
        if (statementPool == null) {
            statementPool = new PreparedStatementPool(this);
        }
        return statementPool;
    }
}

也就是说,预编译的SQL,会被存放在当前连接的持有者中,这些预编译的内容不会被其他连接所共享。

3.预编译的SQL是什么时候被添加进缓存的?

由于预编译SQL时,没有将语句缓存到语句池中,推测是在语句关闭时进行处理。

 @Override
 public void close() throws SQLException {
     //如果当前语句已经被关闭(内部状态判断),则不进行处理
     if (isClosed()) {
         return;
     }
     //判断当前连接是否被关闭(同样通过内部状态进行判断,此时如果为true,该连接处于等待回收或正在回收)
     boolean connectionClosed = this.conn.isClosed();
     // Reset the defaults
     //如果当前开启了PSCache,并且连接没有被关闭,重置数值到默认
     if (pooled && !connectionClosed) {
         try {
             if (defaultMaxFieldSize != currentMaxFieldSize) {
                 stmt.setMaxFieldSize(defaultMaxFieldSize);
                 currentMaxFieldSize = defaultMaxFieldSize;
             }
             if (defaultMaxRows != currentMaxRows) {
                 stmt.setMaxRows(defaultMaxRows);
                 currentMaxRows = defaultMaxRows;
             }
             if (defaultQueryTimeout != currentQueryTimeout) {
                 stmt.setQueryTimeout(defaultQueryTimeout);
                 currentQueryTimeout = defaultQueryTimeout;
             }
             if (defaultFetchDirection != currentFetchDirection) {
                 stmt.setFetchDirection(defaultFetchDirection);
                 currentFetchDirection = defaultFetchDirection;
             }
             if (defaultFetchSize != currentFetchSize) {
                 stmt.setFetchSize(defaultFetchSize);
                 currentFetchSize = defaultFetchSize;
             }
         } catch (Exception e) {
             this.conn.handleException(e, null);
         }
     }
     //由连接对象对预编译语句进行处理
     conn.closePoolableStatement(this);
 }
/**
 * 关闭预编译语句
 */
public void closePoolableStatement(DruidPooledPreparedStatement stmt) throws SQLException {
    //获得原始的预编译语句对象
    PreparedStatement rawStatement = stmt.getRawPreparedStatement();
    //获得当前连接的持有者
    final DruidConnectionHolder holder = this.holder;
    //如果当前连接不再被持有,则不处理
    if (holder == null) {
        return;
    }
    //判断是否开启了缓存预编译语句
    if (stmt.isPooled()) {
        try {
            //清空预编译语句中所有的参数
            rawStatement.clearParameters();
        } catch (SQLException ex) {
            //处理异常
            this.handleException(ex, null);
            //判断当前连接是否被放弃,如果放弃不继续处理
            if (rawStatement.getConnection().isClosed()) {
                return;
            }

            LOG.error("clear parameter error", ex);
        }

        try {
            //清除所有的批处理
            rawStatement.clearBatch();
        } catch (SQLException ex) {
            this.handleException(ex, null);
            if (rawStatement.getConnection().isClosed()) {
                return;
            }

            LOG.error("clear batch error", ex);
        }
    }
    //获得当前语句的持有者
    PreparedStatementHolder stmtHolder = stmt.getPreparedStatementHolder();
    //释放当前语句,使其可以被再次获取
    stmtHolder.decrementInUseCount();
    //如果开启了PSCache,并且当前语句没有发生过异常
    if (stmt.isPooled() && holder.isPoolPreparedStatements() && stmt.exceptionCount == 0) {
        //置入语句池
        holder.getStatementPool().put(stmtHolder);
        //清空其返回集合
        stmt.clearResultSet();
        //取消对这个语句的跟踪
        holder.removeTrace(stmt);
        //记录当前语句的的查询峰值(监控用)
        stmtHolder.setFetchRowPeak(stmt.getFetchRowPeak());
        //软关闭当前语句
        stmt.setClosed(true); // soft set close
    } else if (stmt.isPooled() && holder.isPoolPreparedStatements()) {
        // the PreparedStatement threw an exception
        //进入此分支时,则当前语句抛出过异常
        //清除所有的返回集合
        stmt.clearResultSet();
        //删除跟踪
        holder.removeTrace(stmt);
        //从语句池中删除这条语句并关闭,因为这条语句不再健康
        holder.getStatementPool()
                .remove(stmtHolder);
    } else {
        try {
            //Connection behind the statement may be in invalid state, which will throw a SQLException.
            //In this case, the exception is desired to be properly handled to remove the unusable connection from the pool.
            //真正关闭当前预编译过的语句
            stmt.closeInternal();
        } catch (SQLException ex) {
            this.handleException(ex, null);
            throw ex;
        } finally {
            //增加计数(关闭了多少预编译语句)
            holder.getDataSource().incrementClosedPreparedStatementCount();
        }
    }
}

经过查阅代码,发现其确实是在语句关闭时进行处理,对一条被预编译过的语句有以下三种处理方式

  1. 开启PSCache并且这条语句没有抛出过异常时,将其添加进缓存池
  2. 开启PSCache但是这条语句发生过异常,从缓存池中移除并关闭
  3. 没有开启PSCache,直接关闭这条SQL

4.为什么官方文档中不推荐在MySQL中开启PSCache

在查询这方面问题时,关于MySQL不同版本有不同的说法,究其原因是因为在MySQL某一版本之前不支持PSCache。关于poolPreparedStatements问题。 #1256
是否要在线上开启可以参考Druid监控中查看PSCache命中数据来决定。

你可能感兴趣的:(java)