池化技术在后端开发中应用非常广泛,有数据库连接池,线程池,对象池,常量池等。池化技术的出现是为了提高性能。实际就是对一些使用率较高,且创建销毁比较耗时的资源进行缓存,避免重复地创建和销毁,做到资源回收利用。
传统的数据库连接池有DBCP,C3P0,目前新一代的连接池Druid、HikariaCP等较传统的数据库连接池在性能上有很大的提升。本文将手写一个简单功能的数据库连接池,以理解数据库连接池的基本原理和使用。
一般来说,java应用访问数据库有以下几步操作:
装载数据库驱动,
Class.forName(JDBC_DRIVER);
通过jdbc建立数据库连接,
DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD)
访问数据库,执行sql语句
String sql = "select email from user where id = 5";
PreparedStatement statement = conn.prepareStatement(sql);
ResultSet result = statement.executeQuery();
while (result.next()){
String email = result.getString(1);
System.out.println("【email】:"+email);
}
断开数据库连接
result.close(); state.close(); conn.close();
每一个请求过来都要建立数据库连接,连接数据库需要建立网络连接、系统要分配内存资源、使用完必须断开连接,这些过程可能比实际进行的数据库操作耗时好多倍。并且在高并发量的请求情况下,应用响应速度就会非常慢,甚至造成服务器崩溃。
由此可见,数据库连接是一种非常昂贵的资源。
为解决上述问题,可以采用数据库连接池技术。数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。初始化时在缓冲池中放入指定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定各种参数来控制初始连接数量、最大连接数量、最大空闲连接数量、等待时间等,还可以通过连接池的管理机制来监视数据库连接的数量和使用情况,方便开发测试和性能调优。
有了这些需求下面来实现一个简单的数据库连接池,实现连接池初始化方法、获取连接方法、回收连接方法、销毁池方法,以及几个参数的可配置。
首先将jdbc层面的代码封装成工具类,提供获取连接,关闭连接的方法,以Mysql为例,这部分代码不是重点,所以配置信息也直接写到类里面了:
public class MysqlConnectionUtils {
public static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
public static final String JDBC_URL = "jdbc:mysql://localhost:3306/usercase?useUnicode=true&characterEncoding=utf-8";
public static final String JDBC_USERNAME = "root";
public static final String JDBC_PASSWORD = "123456";
public static Connection getConnection() {
Connection connection=null;
try {
Class.forName(JDBC_DRIVER);
connection = DriverManager.getConnection(JDBC_URL, JDBC_USERNAME, JDBC_PASSWORD);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return connection;
}
public static void closeConnection(Connection conn){
if (conn == null) {return;}
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
定义接口:
public interface ConnectionPool {
/*初始化*/
void init(int initSize,int maxSize,int idleCount,long waitTime);
/*获取连接*/
Connection getConnection() throws TimeoutException;
/*回收*/
void recycle(Connection conn);
/*销毁*/
void destory();
}
属性
这个就是我们要实现的线程池了,首先看该类的一些属性:
public class MyConnectionPool implements ConnectionPool {
//初始化连接数量
int initSize;
//最大连接数量
int maxSize;
//最大空闲连接数量
int idleCount;
//最大等待时间
long waitTime;
//活跃连接数
AtomicInteger activeSize;
//空闲队列
BlockingQueue<Connection> idle;
//已使用队列
BlockingQueue<Connection> busy;
这里定义了两个队列,一个存放空闲连接,一个存放正在使用的连接。活跃连接数就是连接池中未关闭的连接的总数,也就是两个队列的连接总数。
取出连接和回收连接方法模型图:
通过构造方法完成初始化:
public MyConnectionPool(int initSize, int maxSize, int idleCount, long waitTime) {
init(initSize ,maxSize,idleCount,waitTime);
}
public MyConnectionPool() {}
@Override
public void init(int initSize, int maxSize, int idleCount, long waitTime) {
this.initSize = initSize;
this.maxSize = maxSize;
this.idleCount = idleCount;
this.waitTime = waitTime;
this.activeSize = new AtomicInteger();
this.idle = new LinkedBlockingQueue<Connection>();
this.busy = new LinkedBlockingQueue<Connection>();
//初始化连接
initConnection(initSize);
}
private void initConnection(int initSize) {
for (int i = 0; i <initSize ; i++) {
if (activeSize.get()<maxSize){//两重判断是为了保证线程安全
if (activeSize.incrementAndGet()<=maxSize){
//创建连接
Connection conn = MysqlConnectionUtils.getConnection();
//加入空闲队列
idle.offer(conn);
}else{
activeSize.decrementAndGet();
}
}
}
}
构造线程池时,输入四个初始化参数,分别是初始化连接数、最大连接数、最大空闲连接数和超时时间。在构造方法中初始化两个核心队列,使用LinkedBlockingQueue有序阻塞队列。并在构造方法中创建初始化连接数数量的连接,放入空闲队列,注意每成功创建一个连接,活跃连接数activeSize加1。
这里activeSize使用原子类AtomicInteger,是考虑多线程的情况下线程安全问题,防止创建的线程数超出。
初始化方法两重if判断中进行原子操作,可形成锁,保证线程安全。
接下来实现从连接池中获取连接的方法:
@Override
public Connection getConnection() throws TimeoutException {
long startTime = System.currentTimeMillis();
//1.从空闲队列里获取,return
Connection conn = idle.poll();
if (conn != null) {
busy.offer(conn);
System.out.println("["+Thread.currentThread().getName()+"]"+" ************从空闲队列获取连接************");
return conn;
}
//2.空闲队列没有连接了,并且活跃连接数量不超过最大连接数量,创建一个新的连接return
if (activeSize.get()<maxSize){//两重判断是为了保证线程安全
if (activeSize.incrementAndGet()<=maxSize){
conn = MysqlConnectionUtils.getConnection();
System.out.println("["+Thread.currentThread().getName()+"]"+" ************创建一个新的连接************");
busy.offer(conn);
return conn;
}else {
activeSize.decrementAndGet();
}
}
//3.连接全部正在使用,并且活跃连接数超过最大连接数量,则等待正在使用的连接被回收到空闲队列。
//waitTime代表getConnection()方法的等待时间,poll方法阻塞时间其中一部分
long timeout = waitTime - (System.currentTimeMillis()-startTime);
try {
//阻塞方法,等待从idle队列获取连接
conn = idle.poll(timeout, TimeUnit.MILLISECONDS);
if (conn == null) {
throw new TimeoutException("["+Thread.currentThread().getName()+"]"+" ************获取连接超时,等待时间为"+timeout+"ms************");
}
busy.offer(conn);
} catch (InterruptedException e) {
e.printStackTrace();
}
return conn;
}
以上代码核心就是两个队列弹出元素和加入元素的过程。然后插入了一些打印信息,方便测试的时候查看。
获取连接实际上有三种可能的方式以及一种超时抛异常的情况:
然后看另一个关键的方法,回收方法:
@Override
public void recycle(Connection conn) {
if (conn == null) { return;}
//回收连接,就是将连接从使用队列移动到空闲队列
if (busy.remove(conn)){//第一步,移除
//如果实际的空闲连接数量大于用户指定的最大空闲连接数量,则连接多余了,关闭连接
if (idle.size()>=idleCount){
MysqlConnectionUtils.closeConnection(conn);
//关闭连接,则活跃连接减一
activeSize.decrementAndGet();
System.out.println("["+Thread.currentThread().getName()+"]"+" ++++空闲连接数量太多,关闭连接++++");
return;
}
if (!idle.offer(conn)){//第二步,放入,如果放入空闲队列失败,关闭连接
MysqlConnectionUtils.closeConnection(conn);
activeSize.decrementAndGet();
}
System.out.println("["+Thread.currentThread().getName()+"]"+" ++++回收连接成功++++");
}else{//从busy队列移除失败,可能因为该连接不在busy队列中
MysqlConnectionUtils.closeConnection(conn);
activeSize.decrementAndGet();
System.out.println("["+Thread.currentThread().getName()+"]"+" ++++未知连接,直接关闭,不予回收++++");
}
}
回收方法实际就是两步,
注意每关闭一个连接,活跃连接数activeSize减1。
@Override
public void destory() {
while (busy.size()!=0){
recycle(busy.poll());
}
while(idle.size()!=0){
MysqlConnectionUtils.closeConnection(idle.poll());
activeSize.decrementAndGet();
}
}
将连接全部回收,然后关闭所有控线连接。
至此,一个简单的数据库连接池实现完毕。核心方法就是获取连接方法和回收方法。下面通过设置不同的参数来测试一下功能好不好使,以及分析这些参数应当根据那些因素来配置。
测试类代码如下:
public class TestMain {
private static int threadNum = 50;
public static void main(String[] args) {
String sql = "select email from user where id = 5";
CountDownLatch cdl = new CountDownLatch(1);
ConnectionPool pool = new MyConnectionPool(10,20,15,20000L);
for (int i = 0; i <threadNum ; i++) {
new Thread(() ->{
Connection conn = null;
try {
cdl.await(); //阻塞在这,等待所有线程准备好,发令枪响,一起执行
conn = pool.getConnection();
PreparedStatement statement = conn.prepareStatement(sql);
ResultSet result = statement.executeQuery();
while (result.next()){
String email = result.getString(1);
System.out.println("【email】:"+email);
}
//Thread.currentThread().sleep(1000L*new Random().nextInt(5));//模拟线程占用连接的时间
} catch (TimeoutException | InterruptedException | SQLException e) {
e.printStackTrace();
} finally {
pool.recycle(conn);
}
}).start();
}
cdl.countDown(); //发令枪,唤醒所有线程
sleep(3000);//等待所有线程执行完毕
System.out.println("已建立连接数:"+((MyConnectionPool) pool).activeSize.get()+
",空闲连接数:"+((MyConnectionPool) pool).idle.size()+
",正在使用连接数:"+((MyConnectionPool) pool).busy.size());
pool.destory();
System.out.println("detroy MyConnectionPool!!");
System.out.println("已建立连接数:"+((MyConnectionPool) pool).activeSize.get()+
",空闲连接数:"+((MyConnectionPool) pool).idle.size()+
",正在使用连接数:"+((MyConnectionPool) pool).busy.size());
}
public static void sleep(long time){
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
首先设置50个线程并发访问连接池,初始化连接数10、最大连接数20、最大空闲连接数15、超时时间20秒,
ConnectionPool pool = new MyConnectionPool(10,20,15,20000L);
测试结果:
[Thread-1] 从空闲队列获取连接
…
[Thread-17] 创建一个新的连接
…【email】:[email protected]
[Thread-20] ++++回收连接成功++++
…
[Thread-23] ++++空闲连接数量太多,关闭连接++++
…
已建立连接数:15,空闲连接数:15,正在使用连接数:0detroy MyConnectionPool!!
已建立连接数:0,空闲连接数:0,正在使用连接数:0
由于篇幅原因,打印信息省略一部分。
本次测试得到以下信息:
更改参数再次测试,设置超时时间为3秒,模拟线程随机1到5秒的占用连接时间,主线程等待10秒:
ConnectionPool pool = new MyConnectionPool(10,20,15,3000L);
Thread.currentThread().sleep(1000L*new Random().nextInt(5));//模拟线程占用连接的时间
sleep(10000);
[Thread-1] 从空闲队列获取连接
…
[Thread-17] 创建一个新的连接
…【email】:[email protected]
java.util.concurrent.TimeoutException: [Thread-38] 获取连接超时,等待时间为3000ms
at com.youzi.test.concurrent.connPool.MyConnectionPool.getConnection(MyConnectionPool.java:100)
at com.youzi.test.concurrent.connPool.TestMain.lambda$main$0(TestMain.java:23)
at java.lang.Thread.run(Thread.java:745)[Thread-20] ++++回收连接成功++++
…
[Thread-23] ++++空闲连接数量太多,关闭连接++++
…
已建立连接数:15,空闲连接数:15,正在使用连接数:0detroy MyConnectionPool!!
已建立连接数:0,空闲连接数:0,正在使用连接数:0
可以看到由于线程占用连接时间为随机1到5秒,而设置超时时间是3秒,在被占用连接数量达到最大连接数20时,某线程3秒内没有获取到连接,就会抛出超时异常。不影响其他线程。
线程池连接数量始终符合根据配置该有的数量。没有超出配置,也没有连接泄露。
手写连接池测试完毕。