一、目标
用精简的代码实现一个类似于Hikari,Druid一样的高性能数据库连接池。
二、实现思路
1:新建连接池配置类保存连接池配置。
2:实现DataSource接口。
3:新增SmpDbPool类,内部维护一个阻塞队列保存数据库连接,并提供数据库连接的获取回收等方法。
4:大致类图:
三、核心代码
1:数据库连接代理类SmpConnectionProxy
SmpConnectionProxy实现jdbc的Connection接口
重写close方法,确保外部调用close的时候将连接归还到连接池。
public void close() throws SQLException {
if (this.closed) return;
this.closed = true;
//回滚掉未提交的脏statement
if (this.isCommitStateDirty && !this.isAutoCommit){
this.delegate.rollback();//
}
this.clearWarnings();
this.smpDbPool.releaseConnection(this);
}
新增判断连接是否有效方法供连接池类使用。
public boolean isValid() throws SQLException {
if (System.currentTimeMillis() - this.lastActiveTime < this.smpDbPool.getConfig().getKeepaliveTimeInMill()) return true;
long validationTimeout = this.smpDbPool.getConfig().getValidationTimeoutInMill();
this.delegate.setNetworkTimeout(this.smpDbPool.getNetTimeoutExecutor(), (int) validationTimeout);
int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000;
return this.delegate.isValid(validationSeconds);
}
2:数据源类SmpDataSource
SmpDataSource实现DataSource接口
实现getConnection方法,以便外界从连接池获取连接。
@Override
public Connection getConnection() throws SQLException {
try {
return this.dbPool.borrowConnection();
} catch (Exception e) {
LOG.error("getConnection error", e);
throw new SQLException(e);
}
}
3:连接池类SmpDbPool
获取数据库连接方法:
public Connection borrowConnection() throws Exception {
boolean isDebugEnable = LOG.isDebugEnabled();
long sTime = 0;
if (isDebugEnable){//尽量少访问临界资源
sTime = System.currentTimeMillis();
}
try {
SmpConnectionProxy connection = this.getConnectionFromQueue();
if (connection != null) return connection;
//未获取到有效连接,校验是否创建连接
int maxActive = this.smpConfig.getMaxActive(), dbAmount = createdDbConnectionAmount.get();
if (dbAmount >= maxActive){
//尝试等待其他线程释放连接
connection = this.dbConnectQueue.poll(this.smpConfig.getMaxWaitInMill(), TimeUnit.MILLISECONDS);
if (connection != null) return connection.borrowConnection();
LOG.error("get connection error, no connection available,maxActive:{}, activated connection:{}", maxActive, dbAmount);
throw new IllegalStateException("database connection pool too busy");
}
connection = this.createConnectionForPool();
if (connection != null) return connection;
throw new IllegalStateException("database connection pool too busy");
} finally {
if (isDebugEnable){
LOG.debug("get database connection cost {} ms", System.currentTimeMillis() - sTime);
}
}
}
从阻塞队列获取数据库连接方法:
private SmpConnectionProxy getConnectionFromQueue() throws SQLException {
int times = 0;
while (times++ < 1000){
SmpConnectionProxy connection = this.dbConnectQueue.poll();
if (connection == null) return null;
if (connection.isValid()) return connection.borrowConnection();
//destroy invalid connection
connection.destroy();
createdDbConnectionAmount.decrementAndGet();
LOG.info("destroyed invalid jdbc connection");
}
return null;
}
新建数据库连接:
private SmpConnectionProxy createConnectionForPool() throws Exception{
boolean locked = false;
try {
locked = GET_CONNECTION_LOCK.tryLock(this.smpConfig.getMaxWaitInMill() >> 1, TimeUnit.MILLISECONDS);
if (!locked){
LOG.error("get lock to create connection error");
return null;
}
SmpConnectionProxy connection = this.getConnectionFromQueue();
if (connection != null) return connection;
int dbAmount = createdDbConnectionAmount.get(), maxActive = this.smpConfig.getMaxActive();
if (dbAmount >= maxActive){
LOG.error("get connection error, no connection available,maxActive:{}, activated connection:{}", maxActive, dbAmount);
return null;
}
connection = this.createConnection();
createdDbConnectionAmount.addAndGet(1);
return connection.borrowConnection();
} catch (ClassNotFoundException ex){
LOG.error("class {} not found", this.smpConfig.getDriverClassName(), ex);
throw new SQLException(ex);
}finally {
if (locked){
GET_CONNECTION_LOCK.unlock();
}
}
}
private SmpConnectionProxy createConnection() throws ClassNotFoundException, SQLException {
Class.forName( this.smpConfig.getDriverClassName() );
Connection connection = DriverManager.getConnection( this.smpConfig.getUrl(), this.smpConfig.getUsername(), this.smpConfig.getPassword());
LOG.debug("create new database connection with url:{}", this.smpConfig.getUrl());
return new SmpConnectionProxy(connection, this, connection.getAutoCommit());
}
4:构建spring-boot-starter
这里需要说明的是springboot默认会尝试使用com.zaxxer.hikari.HikariDataSource,org.apache.tomcat.jdbc.pool.DataSource,org.apache.commons.dbcp2.BasicDataSource作为连接池,相关代码逻辑在org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration,因此配置spring-boot-starter的时候先要确保应用没有配置连接池。
核心代码:
5:springboot使用
添加依赖后再添加如下配置:
spring.datasource.type=com.lauor.smpdb.SmpDataSource
#最小连接数,默认4
spring.datasource.smpdb.minIdle=4
#最大连接数,默认40
spring.datasource.smpdb.maxActive=20
#数据库连接有效期检查间隔时间ms,默认30分钟,最小1s
spring.datasource.smpdb.keepaliveTimeInMill=1800000
#获取数据库连接最大等待时间ms,默认30s,最小1s
spring.datasource.smpdb.maxWaitInMill=30000
#数据连接合法性校验超时时间,默认5s,最小1s
spring.datasource.smpdb.validationTimeoutInMill=5000
连接池完整代码:https://gitee.com/tandatda/smpdb
连接池使用完整demo:https://gitee.com/tandatda/demo-edr-smpdb