在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();
}
}
}
经过查阅代码,发现其确实是在语句关闭时进行处理,对一条被预编译过的语句有以下三种处理方式
- 开启PSCache并且这条语句没有抛出过异常时,将其添加进缓存池
- 开启PSCache但是这条语句发生过异常,从缓存池中移除并关闭
- 没有开启PSCache,直接关闭这条SQL
4.为什么官方文档中不推荐在MySQL中开启PSCache
在查询这方面问题时,关于MySQL不同版本有不同的说法,究其原因是因为在MySQL某一版本之前不支持PSCache。关于poolPreparedStatements问题。 #1256
是否要在线上开启可以参考Druid监控中查看PSCache命中数据来决定。