以下内容是对这两周源码查看的一个梳理,以前没有看过Druid
的源码,算是从零开始,研究的不那么深入。希望我的整个研究思路能对你有所帮助。
DruidDataSource
Druid
的数据源封装类,通过设置需要的参数之后实例化然后就可以愉快的使用Druid
了。
DruidDataSource
里最核心的方法是init()
,这个方法里面有很多校验和一些设置参数的初始化,驱动加载,filter的加载等。然后init()
这个方法调用不是必须的,因为在调用getConnect()
时会自动调用init()
.
我认为里面比较核心的几块逻辑是:
初始连接的创建
初始连接数通过initialSize
设定,分同步和异步两种情况:
同步就是一个while循环依次创建,通过底层的Driver.connect(url, info)
拿到实际连接,在其前后进行各种各样的监控计数和校验逻辑以及前置的filter逻辑。
异步是根据传进来的createScheduler
周期性的执行创建任务,创建逻辑都是调用的createPhysicalConnection
这个方法。
public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
、、、、、、、、、、、、、、、、、、、、、、、、、、
String password = getPassword();
PasswordCallback passwordCallback = getPasswordCallback();
if (passwordCallback != null) {
if (passwordCallback instanceof DruidPasswordCallback) {
DruidPasswordCallback druidPasswordCallback = (DruidPasswordCallback) passwordCallback;
druidPasswordCallback.setUrl(url);
druidPasswordCallback.setProperties(connectProperties);
}
char[] chars = passwordCallback.getPassword();
if (chars != null) {
password = new String(chars);
}
}
Properties physicalConnectProperties = new Properties();
if (connectProperties != null) {
physicalConnectProperties.putAll(connectProperties);
}
if (user != null && user.length() != 0) {
physicalConnectProperties.put("user", user);
}
if (password != null && password.length() != 0) {
physicalConnectProperties.put("password", password);
}
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
try {
conn = createPhysicalConnection(url, physicalConnectProperties);
connectedNanos = System.nanoTime();
if (conn == null) {
throw new SQLException("connect error, url " + url + ", driverClass " + this.driverClass);
}
initPhysicalConnection(conn, variables, globalVariables);
、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、
return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
}
从代码里可以看出,数据库密码的加密解析也是在这块做的。
创建连接守护线程的创建
创建连接的线程,如果有设置createScheduler
则直接采用设置的定时器,否则会创建一个守护线程执行创建的工作。创建的核心逻辑上面已经列了,就不赘述。
createScheduler
主要是为了优化一个数据源创立两个线程分别用于创建连接和销毁或检测连接,当分库分表情况下,可能有成百上千个数据库,因此创立大量链接。利用createScheduler
可以让多个数据源共享同一个连接池。destroyScheduler
同理。
摧毁检测连接守护线程的创建
根据设定的timeBetweenEvictionRunsMillis
摧毁连接,摧毁和检测线程的核心逻辑有两个方法shrink()
和removeAbandoned()
。
shrink()
会做以下几件事:
- 将连接池的连接限制为最小连接数,多余的关闭
- 对于一些发生过异常的连接由于不知道是否可用,拿去做保活检测
- 超过设定的物理超时时间关闭该连接,主要用于跳过
mysql
8小时自动断开连接 - 关闭大于最大存活时间的连接
- 开启保活且存活时间超过保活检测周期的拿去做保活检测
- 开启保活且连接池连接加活跃连接数小于最小连接填充连接至最小连接
- 保活连接不是一直存在连接池里的,而是一开始和丢弃连接一样从连接池拿掉,当检测有效之后,再将连接放进连接池尾部,如果连接池已经有了对应的holder会将连接丢弃。
removeAbandoned()
只有开启了removeAbandoned
之后会调用,它的作用是遍历活跃连接,跳过正在执行的,拿到里面存活时间大于removeAbandonedTimeoutMillis
的连接,直接关闭并丢掉。
连接获取
获取连接的时候如果有设置filter
,会先递归的调用filter逻辑之后(主要是监控和计数),才会调用对应的getConnectionDirect()
从连接池尾部拿到连接,里面会有一些很复杂的逻辑判断,这块应该和各种状态值有关,就没细究了。
连接关闭
关闭连接入口在DruidPooledConnection
的recycle()
方法里,对于在removeAbandoned()
里已经被丢弃的连接是不需要回收的,主要是针对没有被丢弃的连接,具体实现是DruidDataSource
的recycle(DruidPooledConnection pooledConnection)
方法里,recycle()
主要做了一下几件事:
removeAbandoned
相关调试,获取连接时当开启removeAbandoned
之后,在拿到连接的时候这个状态设置为true
,关闭时会进一步检测。- 对于需要回滚的连接进行回滚,如果回滚异常可能导致连接关闭,刷新相应数值。(
activeCount
,closeCount
等) - 当连接使用次数达到
phyMaxUseCount
这个设置的连接最大使用次数时,关闭连接,这个主要是针对分布式数据库优化的,通过达到一定使用次数后断开重连,使得多个服务器间负载更均衡。 - 测试
testOnReturn
逻辑,如果设置会验证连接是否可用,不可用会关闭连接。 - 数据源可用性验证,不可用关闭连接。
- 超过设定的物理超时时间关闭该连接。
- 所有检验通过后将连接放进连接池,更新相应数值。
PreparedStatementPool
当一个PreparedStatementPool
会多次用到,频繁的创建其实引来不必要的消耗,这时候可以引入缓存,即PC Cache
。PS Cache
的实现其实很简单,底层就是一个LRU Cache
。当设置maxPoolPreparedStatementPerConnectionSize
>0则会开启。
当DruidPooledPreparedStatement
调用close
方法时会调用conn.closePoolableStatement(this)
方法,如果开启了缓存会调用holder.getStatementPool().put(stmtHolder);
将stmtHolder
放进缓存里。
执行prepareStatement
方法时如果开启缓存会通过stmtHolder = holder.getStatementPool().get(key);
拿到对应的PreparedStatement
对象。