连接池是常见的一种资源复用的技术。利用连接池,可以将那些创建开销较大的资源汇聚到一个池子里缓存起来,需要使用的时候只需要从连接池里取出来就可以了。中间省去了频繁的创建和销毁的过程。数据库连接池就是其中的典型应用。
Tomcat-JDBC
是Spring Boot
中自动配置优先级最高的连接池方案,它的出现是用来替代Apache
早期的连接池产品——DBCP 1.x
。总得来说,各款连接池的原理大同小异,具体还得看细节,比如某些早期连接池对于并发和利用CPU多核考虑得就不够到位。
在介绍Tomcat-JDBC
之前,我们可以简单的思考一下,假设让我们来实现一个数据库连接池,会有哪些问题需要解决?
现在,我们可以带着上面的两个问题来看看Tomcat-JDBC
的实现细节
数据库连接池初始化的核心就是:
TIPS:在【初始化JdbcInterceptors】环节,会调用每个拦截器的poolStarted方法。但是这里的JdbcInterceptor实例只是临时创建,不会在后续使用,所以在自己实现JdbcInterceptor重写poolStarted方法的时候,不要里面操作类的成员变量,只有操作静态变量才是有意义的
其中有几点可能从上图看不是特别清楚的再说明下:
testWhileIdle
哪些参数控制着清洁工任务的启动?
1是必要条件,2|3|4|5 只要有一个满足,那么就会启动清洁工任务
还记得刚才的清洁工任务中有一个是专门来检查空闲连接的有效性的,下面就来介绍如何判断一个连接的有效性:
其中,所有类型的验证都在这里维护,包括
上面这四种类型,在【需要验证】这个步骤里会有体现,分别对应上面的配置项。
另外,对于非testOnConnect
这种类型的验证,可以用到validationInterval
来避免频繁的连接验证
下面再给出和有效性验证相关的参数:
select 1
关于【获取连接】,有两点需要注意一下:
testOnBorrow
的校验时,如果第一次校验失败,还会给予一次reconnect
的机会去重连数据库,然后继续校验(这次校验不通过那就报错了)。其他的有效性校验只要不通过就报错
返回连接的核心是将取出的连接放回连接池中,但是在放回池中之前会做一系列的校验:比如是否超过maxAge,有效性验证是否通过等。如果前置校验通不过,那么会将该连接直接释放掉,而不返回到池中。
下面这些参数在上述流程中基本都有提到,可以结合起来再回顾一下:
也就是JdbcInterceptor,继承它的拦截器可以拦截connection所有方法的调用。
下面列几个个人觉得非常有用的拦截器:
可配置Sql超时时间x,超过该时间的Sql将被Kill掉,客户端报错。简单原理就是执行Sql之前会延迟x毫秒启动一个定时任务,该定时任务就是发送Kill Query命令到数据库,如果在x毫秒之内执行完,那么该定时任务会在执行前就被cancel掉,如果到了x毫秒还没执行完,那么定时任务启动,Kill Query,导致客户端连接报错。这个可以对系统起到一定的保护和监控作用
可配置慢查的阈值,主要就是记录慢查,但是这个东西有点不完善的地方,就是打慢查日志只会打PrepareStatement,打不了里面的参数,这样会造成无法拿Sql去DB里找对应的查询,后面在优化之路里会给出一个增强的慢查监控拦截器代码
用来缓存autoCommit, readOnly, transactionIsolation和catalog这几个属性,将它们缓存在本地,避免各种和数据库之间的roundtrip消耗。比如:
想要搞清楚这个问题,首先需要了解执行一个事务,我们到底会和数据库(MySQL)产生几次交互?
我们以一个简单的事务为例,比如说订单到店这个事务,涉及到订单表的更新,以及订单操作记录表的更新,我们简化成两条语句:updateorder
和 updateorder_operate_record
,在执行该事务的过程中,我们会和MySQL交互几次呢?
1. 先从连接池取出连接,其中可能会碰到连接池中没有空闲连接的情况,这个时候假设还没超出最大活跃连接的话,连接池会发起创建连接,这时就会产生一次交互,并且建立连接的消耗相对于执行Sql更大
2. 根据不同的连接池参数配置,可能还需要对取出的连接做有效性校验,MySQL中一般都是用SELECT 1
来充当校验语句的
3. 下面需要开启事务,SET AUTOCOMMIT = 0
;
4. 然后再发送两条update语句
5. 最后需要告诉MySQL我们的事务结束了,COMMIT,当然也有可能中间碰到一些问题,ROLLBACK掉
6. 还记得第三步开启事务的时候,执行的SET AUTOCOMMIT = 0
吗?所以做为完整的事务操作,最后还有一步SET AUTOCOMMIT = 1
7. 结束了么?最后还要把连接放回连接池。貌似不用和MySQL交互?放回去之前,根据不同的连接池参数配置,可能还需要对放回去的连接做有效性校验。等等,除了有效性校验之外,可能还会有maxAge/maxIdle之类的校验?不过这个不用和MySQL交互。好吧,这里先打住,不然内容太多了。
看看上面的内容,你大概知道一个事务的执行,不仅仅只有事务中的两行更新语句和数据库有交互吧,所以,也不难理解为什么慢查抓不到,但是实际请求处理得很慢了。但是就这样结束了么?既然对于慢查可以监控,为什么不把所有和MySQL有交互的点都监控起来呢?好,有想法是好事,那我们来看看如何把所有的节点都监控起来?
Tomcat-JDBC连接池似乎不提供这个功能。更换连接池?貌似有点牵强。我们何不转向与MySQL更加紧密的MySQL驱动呢?翻阅了MySQL驱动的官方文档,发现其中是有性能监控的开关——profileSQL
通过这个开关,我们可以观察到应用与MySQL交互的每一条语句,包括建立连接时做了哪些初始化操作?什么时候开启事务,什么时候校验连接有效性,什么时候提交事务等,都会有日志打印,包括耗时。
但是,通过上述的配置项,还是无法监控到我们上面第一点的建立连接的耗时,这块通过查询驱动源码发现也是可以实现的,于是自己动手,一点点代码量就完成了这个小功能。至此,上述每个节点都有迹可循,目测可以轻松的找到耗时所在。
当然是可以的。
首先最容易想到的就是连接有效性校验那一块儿,比如上面提到的第2点和第7点里,也就是取出连接之后(testOnBorrow/testOnConnect)和把连接放回连接池(testOnReturn)之前,可能需要做的校验操作,我们可以省去。那么有效性怎么保证呢?理论上来说,大多数情况下都是有效的,除非数据库挂了之类,所以单独线程来做就好了(testWhileIdle)
第5点的COMMIT貌似也可以省去,看了官网,应该是只要再SET AUTOCOMMIT = 1
的时候会自动COMMIT,不过这个目前还没有验证过
这个通过连接池似乎不太好做,我们可以通过MySQL Connector
提供的ConnectionLifecycleInterceptor
来实现:
/**
* Created by Zhu on 2017/9/19.
*/
public class ConnectionLifeInteceptor implements ConnectionLifecycleInterceptor{
private ConnectionImpl connection;
public static final Logger LOGGER = LoggerFactory.getLogger(ConnectionLifeInteceptor.class);
// 这里只关注创建连接耗时
@Override public void init(Connection conn, Properties props) throws SQLException {
this.connection = (ConnectionImpl) conn;
Field field = ReflectionUtils.findField(conn.getClass(), "connectionCreationTimeMillis", Long.TYPE);
ReflectionUtils.makeAccessible(field);
Long connectionCreationTimeMillis = (Long) ReflectionUtils.getField(field, conn);
LOGGER.info("connection:{} cost:{}", connection.getId(), System.currentTimeMillis() - connectionCreationTimeMillis);
}
}
超时设置一直都是系统优化中很重要的节点,所以这里自然而然就想到了是否可以通过timeout来避免潜在的创建连接hang住的风险。答案自然是肯定的,但是翻遍
Tomcat-JDBC
连接池配置也并找不到类似的配置。想必聪明的你已经想到了MySQL Connector
。没错,还是MySQL Connector
,它提供了一个参数connectTimeout
用来设置创建连接的超时时间。
通过监控创建连接耗时帮助我们最后定位到偶尔慢的现象是因为取出的连接衰老而死(超过maxAge),触发了reconnect,导致重新与MySQL建立连接,并且建立连接耗时1s左右。那怎么办呢?我们可以扫描idle队列里即将要超过maxAge(比如60s内)的连接,比如发现快要过期了,那么我们就拿该连接reconnect一下,重新激活,该任务我们可以直接置于
PoolCleaner
中,附上部分代码:
protected static class PoolCleaner extends TimerTask {
// 省略部分代码
@Override
public void run() {
// 省略部分代码
if (pool.getPoolProperties().isTestWhileIdle()){
pool.testAllIdle();
pool.recoverNearDeath();
}
// 省略部分代码
}
// 省略部分代码
}
public void recoverNearDeath(){
try {
if (idle.size()==0) return;
Iterator unlocked = idle.iterator();
while (unlocked.hasNext()) {
PooledConnection con = unlocked.next();
try {
con.lock();
// 连接被取出,不做处理
if (busy.contains(con))
continue;
if (con.isNearDeath()){
log.info("Connection ["+con+"] is near death, last connected:" + con.getLastConnected());
con.reconnect();
}
} finally {
con.unlock();
}
} //while
} catch (Exception e) {
log.error("recoverNearDeath failed",e);
}
}
public boolean isNearDeath(){
// 这里暂时不做配置,定义离maxAge还差1min以内的为濒临死亡的连接
final long val = 60000;
return (System.currentTimeMillis() - getLastConnected()) > (getPoolProperties().getMaxAge() - val);
}
/**
* @author Zhu
* @date 2017年3月22日 下午11:00:11
* @description
*/
public class MonitorSlowQueryReport extends SlowQueryReport {
private String systemCode;
// logger
private static final Logger LOGGER = LoggerFactory.getLogger(MonitorSlowQueryReport.class);
class RecordParamStatementProxy extends StatementProxy {
/**
* @param parent
* @param query
*/
public RecordParamStatementProxy(Object parent, String query) {
super(parent, query);
}
/*
* (non-Javadoc)
*
* @see org.apache.tomcat.jdbc.pool.interceptor.AbstractQueryReport.
* StatementProxy#invoke(java.lang.Object, java.lang.reflect.Method,
* java.lang.Object[])
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith("set") && args != null && args.length >= 2) {
ParamHolder.params.get().add(args[1]);
}
Object result = null;
try {
result = super.invoke(proxy, method, args);
} finally {
if (isExecute(method, false)) {
ParamHolder.params.remove();
}
}
return result;
}
}
@Override
public void setProperties(Map properties) {
super.setProperties(properties);
final String systemCode = "systemCode";
InterceptorProperty p1 = properties.get(systemCode);
if (p1 != null) {
setSystemCode(p1.getValue());
}
}
/*
* (non-Javadoc)
*
* @see
* org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport#reportSlowQuery(
* java.lang.String, java.lang.Object[], java.lang.String, long, long)
*/
@Override
protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) {
// extract the query string
String sql = (query == null && args != null && args.length > 0) ? (String) args[0] : query;
// if we do batch execution, then we name the query 'batch'
if (sql == null && compare(EXECUTE_BATCH, name)) {
sql = "batch";
}
if (isLogSlow() && sql != null) {
String beautifulSql = sql.replace("\n", "").replaceAll("[' ']+", " ");
LOGGER.warn("Slow Query Report SQL={}; param:[{}], consume={};", beautifulSql,
StringUtils.join(ParamHolder.params.get(), ','), delta);
}
return sql;
}
public String getLocalHostAddress() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
LOGGER.error("获取本地ip异常", e);
}
return "";
}
/**
* 为了打印全貌sql,重写一下
*/
@Override
public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
try {
Object result = null;
String name = method.getName();
String sql = null;
Constructor> constructor = null;
if (compare(CREATE_STATEMENT, name)) {
// createStatement
constructor = getConstructor(CREATE_STATEMENT_IDX, Statement.class);
} else if (compare(PREPARE_STATEMENT, name)) {
// prepareStatement
sql = (String) args[0];
constructor = getConstructor(PREPARE_STATEMENT_IDX, PreparedStatement.class);
if (sql != null) {
prepareStatement(sql, time);
}
} else if (compare(PREPARE_CALL, name)) {
// prepareCall
sql = (String) args[0];
constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class);
prepareCall(sql, time);
} else {
// do nothing, might be a future unsupported method
// so we better bail out and let the system continue
return statement;
}
result = constructor.newInstance(new Object[] { new RecordParamStatementProxy(statement, sql) });
return result;
} catch (Exception x) {
LOGGER.warn("Unable to create statement proxy for slow query report.", x);
}
return statement;
}
/**
* @return the systemCode
*/
public String getSystemCode() {
return systemCode;
}
/**
* @param systemCode
* the systemCode to set
*/
public void setSystemCode(String systemCode) {
this.systemCode = systemCode;
}
}
后续再更新吧