连接池我们一直再用,比如druid、c3p0、tomcat的连接池等等,包括数据库本身也会有连接池,今天突然想研究一下数据库连接池是怎么实现的。现总结一下:
数据库连接池的目的:
减少频繁的创建/销毁连接,因为一次数据库连接的开销是很大的,要经过一下几个步骤:
1.加载驱动
2.获得一个Connection
3.通过TCP连接数据库
4.发送sql语句
5.执行sql,返回结果
6.关闭TCP连接
7.释放Connection
其中只有4,和5是我们需要动态操作的也是最主要的,其他操作都是重复的。
所以如果每次都进行这一系列的操作,势必会造成不必要的系统开销,增加相应时间,所以我们可以将这些连接复用,在系统初始化的时候就建立好连接,以后直接发送sql执行就行,这样就会大大提高效率。
比较建立一次连接和复用连接的时间:
显然新建一次连接比复用连接更耗时,那到底复用会比新建快多少呢?我来测试一下。
public static void main(String[] args) throws ClassNotFoundException, SQLException {
long start = System.currentTimeMillis();
Class.forName(driver);
Connection connection = DriverManager.getConnection(dbUrl, userName, password);
String sql = "SELECT * FROM tb_user";
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
//省略
}
long end = System.currentTimeMillis();
System.out.println("首次建立连接执行耗时:"+(end - start));
start = System.currentTimeMillis();
statement = connection.prepareStatement(sql);
ResultSet resultSet2 = statement.executeQuery();
while (resultSet2.next()) {
//省略
}
end = System.currentTimeMillis();
System.out.println("复用连接执行耗时:"+(end - start));
}
这段代码里首先建立了一个Connection并统计一个查询操作的耗时,然后继续使用这个连接来执行相同的查询并统计时间,输出如下:
首次建立连接执行耗时:606
复用连接执行耗时:1
可以看到复用连接比新建连接快了600倍左右,显然性能会提高很多。
当然以上程序并不是真正的连接池,只是在同一段代码里使用了同一个连接而已。下面据尝试建立一个连接池。
连接池的主要功能就是实现连接的复用,说白了也就是将一些连接保存在一个集合中,当需要连接时从集合中取,释放连接时并不是真正释放,而是归还给这个集合,达到复用的目的。
首先新建连接池类:ConnectionPool.
为了统一接口我们实现DataSource接口.主要是实现getConnection方法。
/**
* Created by wxg on 2018/8/13 17:42
*/
public class ConnectionPool implements DataSource {
private static final String driver = "com.mysql.jdbc.Driver";
private static final String dbUrl = "jdbc:mysql://localhost:3306/db_blog?useSSL=true";
private static final String userName = "root";
private static final String password= "123456";
private LinkedList<Connection> pool;
private Connection getOneConnection() {
Connection connection = null;
try {
Class.forName(driver);
connection = DriverManager.getConnection(dbUrl,userName,password);
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
return connection;
}
@Override
public Connection getConnection() throws SQLException {
if(pool==null){
pool = new LinkedList<>();
for(int i=0;i<2;i++){
pool.add(getOneConnection());
}
}
if(pool.size()<=0){
pool.add(getOneConnection());
}
return pool.remove();
}
public void close(Connection connection){
pool.add(connection);
}
//省略其他实现...
}
因为是使连接的获取和归还涉及到频繁的删除和添加操作,所以我们用LinkList来作为存储Connection的容器。
这里边封装了我们常用的一次建立连接的过程到方法getOneConnection中。
重写的getConnection中首先判断pool是否为null,若为null则初始化2个连接,然后在判断pool的size是否为0,当pool==0时说明池中没有可用连接了,所有的连接都被用了,此时就需要再调用getOneConnection来建立一个新的连接。最后是移除pool中的第一个连接并返回。
因为这个getConnection会在没有可用连接时创建新的连接,所以这个连接池的大小取决于连接数最大时的大小,所以当系统在繁忙期间进行大量连接时,在不忙的时候就会出现大量空闲连接,我们可以作如下修改:
if(pool.size()<=0){
pool.add(getOneConnection());
}
改为:
if(pool.size()<=0){
return getOneConnection();
}
这样就相当于创建了零时连接,当然更好的做法是设置超时时间,这里就不再讨论了。
对于close()方法很简单,但是也很重要,因为这个close并不是直接关闭掉连接,而是将连接归还给pool。实现复用。
然后一个简单的连接池就实现啦,现在来测试一下:
public static void main(String[] args) throws SQLException {
ConnectionPool pool = new ConnectionPool();
Connection connection1 = pool.getConnection();
pool.close(connection1);
Connection connection2 =pool.getConnection();
pool.close(connection2);
Connection connection3 =pool.getConnection();
pool.close(connection3);
Connection connection4 =pool.getConnection();
pool.close(connection4);
Connection connection5 =pool.getConnection();
pool.close(connection5);
}
然后执行调试,结果如下:
其中红色框是Connection对象,可以看到前几个都是888和889,也就是复用了初始化的两个连接,但是获取最后一个连接的时候因为在它之前已经有两个连接被获取了,池为空了,所以就再新建了一个连接对象,所以是890了。
绿色框是执行时间,可以看到第一次初始化连接时用了625毫秒,而后面复用连接用了0毫秒,最后一次新建连接用了7毫秒,因为省去了加载驱动等过程,所以会比第一次快。
通过这个测试可以发现连接池的作用是多么的重要,它能使数据库连接性能提升几百倍!
然后我们这里存在一个问题就是我们的连接对象是Connection对象,它包含了close()方法来释放连接,我们一般也是调用此方法来释放连接,而不是调用连接池的,因此,我们要保证嗲用的是我们自己的close()方法,因为我们的close方法并不会真的释放掉连接而是让它回归到连接池中,所以就要实现Connection来重写close。
只需要重写close()方法即可:
class MyConnection implements Connection {
private Connection connection;
MyConnection(Connection connection) {
this.connection = connection;
}
@Override
public void close() throws SQLException {
pool.add(connection);
}
//...省略其他方法
}
这里使用装饰器模式将Connection包装成MyConnection,并覆盖了close方法,在close时将Connection归还到pool。
测试如下:
结果与上一步测试类似。(返回的MyConncection不同是因为返回时new了Myconnection进行包装,否则将无法调用我们自己写的lclose()方法)。
结束!