您的当前位置:首页正文

Druid数据库连接池源码分析

来源:个人技术集锦
Druid数据库连接池源码分析

  上⼀篇⽂章重点介绍了⼀下Java的Future模式,最后意淫了⼀个数据库连接池的场景。本想通过Future模式来防⽌,当多个线程同时获取数据库连接时各⾃都⽣成⼀个,造成资源浪费。但是忽略了⼀个根本的功能,就是多个线程同时调⽤get⽅法时,得到的是同⼀个数据库连接的多个引⽤,这会导致严重的问题。

  所以,我抽空看了看呼声很⾼的Druid的数据库连接池实现,当然关注点主要是多线程⽅⾯的处理。我觉得,带着问题去看源码是⼀种很好的思考⽅式。

  Druid不仅仅是⼀个数据库连接池,还有很多标签,⽐如统计监控、过滤器、SQL解析等。既然要分析连接池,那先看看DruidDataSource类getConnection⽅法的实现:

@Override

public DruidPooledConnection getConnection() throws SQLException { return getConnection(maxWait); }

public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException { init();

if (filters.size() > 0) {

FilterChainImpl filterChain = new FilterChainImpl(this);

return filterChain.dataSource_connect(this, maxWaitMillis); } else {

return getConnectionDirect(maxWaitMillis); } }

返回的是⼀个DruidPooledConnection,这个类后⾯再说;另外这⾥传⼊了⼀个long类型maxWait,应该是⽤来做超时处理的;init⽅法在getConnection⽅法⾥⾯调⽤,这也是⼀种很好的设计;⾥⾯的过滤器链的处理就不多说了。

public void init() throws SQLException { if (inited) { return; }

final ReentrantLock lock = this.lock; // 使⽤lock⽽不是synchronized try {

lock.lockInterruptibly();

} catch (InterruptedException e) {

throw new SQLException(\"interrupt\ }

boolean init = false; try {

if (inited) { return; }

init = true;

connections = new DruidConnectionHolder[maxActive]; // 数组

try {

// init connections

for (int i = 0, size = getInitialSize(); i < size; ++i) {

Connection conn = createPhysicalConnection(); // ⽣成真正的数据库连接 DruidConnectionHolder holder = new DruidConnectionHolder(this, conn); connections[poolingCount] = holder; incrementPoolingCount(); }

if (poolingCount > 0) {

poolingPeak = poolingCount;

poolingPeakTime = System.currentTimeMillis(); }

} catch (SQLException ex) {

LOG.error(\"init datasource error, url: \" + this.getUrl(), ex); connectError = ex; }

createAndLogThread();

createAndStartCreatorThread(); createAndStartDestroyThread(); initedLatch.await(); initedTime = new Date(); registerMbean();

if (connectError != null && poolingCount == 0) { throw connectError; }

} catch (SQLException e) {

LOG.error(\"dataSource init error\ throw e;

} catch (InterruptedException e) {

throw new SQLException(e.getMessage(), e); } finally {

inited = true;

lock.unlock(); // 释放锁

if (init && LOG.isInfoEnabled()) {

LOG.info(\"{dataSource-\" + this.getID() + \ } } }

  我这⾥做了删减,加了⼀些简单的注释。通过这个⽅法,正好复习⼀下之前写的那些知识点,如果感兴趣,可以看看我之前写的⽂章。

  这⾥使⽤了lock,并且保证只会被执⾏⼀次。根据初始容量,先⽣成了⼀批数据库连接,⽤⼀个数组connections存放这些连接的引⽤,⽽且专门定义了⼀个变量poolingCount来保存这些连接的总数量。  看到initedLatch.await有⼀种似曾相识的感觉

private final CountDownLatch initedLatch = new CountDownLatch(2);

  这⾥调⽤了await⽅法,那countDown⽅法在哪些线程⾥⾯被调⽤呢

protected void createAndStartCreatorThread() { if (createScheduler == null) {

String threadName = \"Druid-ConnectionPool-Create-\" + System.identityHashCode(this); createConnectionThread = new CreateConnectionThread(threadName); createConnectionThread.start(); return; }

initedLatch.countDown(); }

  这⾥先判断createScheduler这个调度线程池是否被设置,如果没有设置,直接countDown;否则,就开启⼀个创建数据库连接的线程,当然这个线程的run⽅法还是会调⽤countDown⽅法。但是这⾥我有⼀个疑问:开启创建连接的线程,为什么⼀定要有⼀个调度线程池呢???

  难道是当数据库连接创建失败的时候,需要过了指定时间后,再重试?这么理解好像有点牵强,希望⾼⼈来评论。

  还有就是,当开启destroy线程的时候也会调⽤countDown⽅法。

  接着在看getConnection⽅法,⼀直调⽤到getConnectionInternal⽅法

DruidConnectionHolder holder; try {

lock.lockInterruptibly();

} catch (InterruptedException e) {

connectErrorCount.incrementAndGet(); throw new SQLException(\"interrupt\ }

try {

if (maxWait > 0) {

holder = pollLast(nanos);

} else {

holder = takeLast(); }

} catch (InterruptedException e) {

connectErrorCount.incrementAndGet();

throw new SQLException(e.getMessage(), e); } catch (SQLException e) {

connectErrorCount.incrementAndGet(); throw e; } finally {

lock.unlock(); }

holder.incrementUseCount();

DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder); return poolalbeConnection;

  我这⾥还是做了删减。⼤体逻辑是:先从连接池中取出DruidConnectionHolder,然后再封装

成DruidPooledConnection对象返回。再看看取holder的⽅法:

DruidConnectionHolder takeLast() throws InterruptedException, SQLException { try {

while (poolingCount == 0) {

emptySignal(); // send signal to CreateThread create connection notEmptyWaitThreadCount++;

if (notEmptyWaitThreadCount > notEmptyWaitThreadPeak) { notEmptyWaitThreadPeak = notEmptyWaitThreadCount; } try {

notEmpty.await(); // signal by recycle or creator } finally {

notEmptyWaitThreadCount--; }

notEmptyWaitCount++;

if (!enable) {

connectErrorCount.incrementAndGet(); throw new DataSourceDisableException(); } }

} catch (InterruptedException ie) {

notEmpty.signal(); // propagate to non-interrupted thread notEmptySignalCount++; throw ie; }

decrementPoolingCount();

DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null; return last; }

  这个⽅法⾮常好的诠释了Lock-Condition的使⽤场景,⼏⾏绿⾊的注释解释的很明⽩了,如果对empty和

notEmpty看不太懂,可以去看看我之前写的那篇⽂章。

  这个⽅法的逻辑:先判断池中的连接数,如果到0了,那么本线程就得被挂起,同时释放empty信号,并且等待notEmpty的信号。如果还有连接,就取出数组的最后⼀个,同时更改poolingCount。

  到这⾥,基本理解了Druid数据库连接池获取连接的实现流程。但是,如果不去看看⾥⾯的数据结构,还是会⼀头雾⽔。我们就看看⼏个基本的类,以及它们之间的持有关系。

  1、DruidDataSource持有⼀个DruidConnectionHolder的数组,保存所有的数据库连接

private volatile DruidConnectionHolder[] connections; // 注意这⾥的volatile

  2、DruidConnectionHolder持有数据库连接,还有所在的DataSource等

private final DruidAbstractDataSource dataSource; private final Connection conn;

  3、DruidPooledConnection持有DruidConnectionHolder,所在线程等

protected volatile DruidConnectionHolder holder;

private final Thread ownerThread;

  对于这种设计,我很好奇为什么要添加⼀层holder做封装,数组⾥直接存放Connection好像也未尝不可。  其实,这么设计是有道理的。⽐如说,⼀个Connection对象可以产⽣多个Statement对象,当我们想同时保存Connection和对应的多个Statement的时候,就⽐较纠结。  再看看DruidConnectionHolder的成员变量

private PreparedStatementPool statementPool;

private final List statementTrace = new ArrayList(2);

这样的话,既可以做缓存,也可以做统计。

  最终我们对Connection的操作都是通过DruidPooledConnection来实现,⽐如commit、rollback等,它们⼤都是通过实际的数据库连接完成⼯作。⽽我⽐较关⼼的是close⽅法的实现,close⽅法最核⼼的逻辑是recycle⽅法:

public void recycle() throws SQLException { if (this.disable) { return; }

DruidConnectionHolder holder = this.holder; if (holder == null) {

if (dupCloseLogEnable) { LOG.error(\"dup close\"); }

return; }

if (!this.abandoned) {

DruidAbstractDataSource dataSource = holder.getDataSource(); dataSource.recycle(this); }

this.holder = null; conn = null;

transactionInfo = null; closed = true; }

  通过最后⼏⾏代码,能够看出,并没有调⽤实际数据库连接的close⽅法,⽽只是断开了之前那张图⾥⾯的4号引

⽤。⽤这种⽅式,来实现数据库连接的复⽤。

因篇幅问题不能全部显示,请点此查看更多更全内容