什么是数据库连接池?

数据库连接池(Connection pooling)是程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地对池中的连接进行申请,使用,释放。
对于我个人理解来说,连接池嘛,就像我们平时挑水喝一样。正常情况下都是先挑满一水缸,等喝的差不多了。根据需要去接着挑水。数据库连接其实也是一种资源,就相当于水一样,我们总不能每次口渴了就走到井边,喝够了再回家。渴了再到井边喝。特别是当井边还有人在喝水时,我们还得等待前面一个人喝完了才到我们。这样子花费的时间太多了。同理,数据库连接也会消耗掉大量的时间。经测试,平均每次创建一个连接需要花费的时间大约为0.1~0.2秒左右。对于并发量小的系统也许感觉不到。但是针对并发量大的系统。无论对于数据库还是程序来说是致命的。严重的话还可能会直接造成数据库雪崩的情况。
数据库连接池恰好是用来解决这个问题的。我们都知道,程序运行过程中,要访问数据库,先是建立一个连接,使用完毕后再关闭这个连接,下一次访问时再获取连接。如果我们想个办法把这些连接保存起来,并且做一个标记是否可用,关闭的时候不真正关闭连接,只是将这个标记设置为可用,依然保持着这些连接,当程序下一次访问的时候。就可以先在池中查询,有可用连接的话就直接拿过来使用。当没有连接可用的时候再去获取连接。这样的话就省去了很多建立连接所花费的时间了,对于数据库来说也是减轻了很多负担。
手写一个数据库连接池
今天,我们就来手写一个数据库连接池。首先我们得先搞清楚,我们的连接池需要实现什么功能。达到什么目的。
首先肯定是数据库配置文件,存放用户名或者密码之类的。这个东西最好不要写在代码里面,因为从实际的生产来说,我们总不能数据库密码被修改了,就得重新编译一遍代码再重新升级嘛,这样太不方便了。
首先在项目下新建一个conf文件夹。新建一名为jdbc.properties的文件。存放数据库的相关配置,如下图所示,其中用户名,密码等是必须的,还可以配置最大连接数量,初始化连接数等。由于之前写过一篇深入理解spring事务底层实现原理 的文章,这里我就直接使用那个项目就行了。方便接下来的测试。有兴趣的可以去看看。包括接下来要写的hibernate泛型增删改查方法封装等也要基于该项目增加的。文章结束我会将该项目上传到我的github上供大家学习使用的。

回到正题,我们都知道,连接池可不只是简简单单的保存了数据库连接而已。我们要做的是除了保存连接,还要管理这些连接。什么时候可以直接使用,而什么时候不能使用。是否需要重新申请数据库连接等等,所以接下来的这个类就是保存连接的。除了有一个连接对象之外,还有一个标记,指示当前这个连接是否可用。
ConnectionManager.java
package spring;
import java.sql.Connection;
public class ConnectionManager{
private Connection conn;
private boolean busy;
public ConnectionManager(Connection conn,boolean busy) {
this.conn = conn;
this.busy = busy;
}
public Connection getConn() {
return conn;
}
public void setConn(Connection conn) {
this.conn = conn;
}
public boolean isBusy() {
return busy;
}
public void setBusy(boolean busy) {
this.busy = busy;
}
}
这个类很简答,接下来的这个类才是我们今天的重点。因为类代码有点长。我这里就放核心代码就行了。稍后我会将源码放在我的github上。有兴趣的同学可以去下载看看
PoolConnect.java。这个类要实现 DataSource这个接口,这个接口我们只要重写getConnectionf方法就行了。

从上到下,我们先初始化一下数据库连接池
//最大连接数
private static int maxAlive = 5;
//初始化连接数
private static int initAlive = 3;
//存放连接池对象,使用线程安全的Vector
private static List<ConnectionManager> conns = new Vector<ConnectionManager>(maxAlive);
static {
try {
//读取初始化配置文件
InputStream inStream = new FileInputStream("./conf/jdbc.properties");
Properties pro = new Properties();
pro.load(inStream);
driverClassName = pro.getProperty("driverClassName");
password = pro.getProperty("password");
username = pro.getProperty("username");
url = pro.getProperty("url");
initAlive = Util.toInt(pro.getProperty("initAlive")) == 0 ? initAlive:Util.toInt(pro.getProperty("initAlive"));
maxAlive = Util.toInt(pro.getProperty("maxAlive")) == 0 ? initAlive:Util.toInt(pro.getProperty("maxAlive"));
//初始化数据库连接池
Class.forName(driverClassName);
for(int i=0;i<initAlive;i++) {
Connection conn = DriverManager.getConnection(url, username, password);
ConnectionManager myConn = new ConnectionManager(conn, false);
conns.add(myConn);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
因为涉及到多线程并发,所以这里需要使用线程安全的Vector。用来保存数据库连接。然后就是读取配置文件的内容,直接使用FileInputStream去读就可以。然后再用Properties类把内容获取到。初始化配置完成之后就开始进行创建数据库连接了。这里我们创建连接的个数为配置的初始化大小。有人可能会问,那个最大连接数maxAlive是做什么的。这个参数是当连接池中初始化的连接数量用完了,再次获取连接的时候就需要去打开连接了。然后把连接再放到池里面,等用完了就要归还,下一次有请求就可以直接用了。
细心的朋友可能发现了,这一块初始化的过程我是直接下载静态代码块里面的。因为这里是最先执行的。其实也可以下载构造函数里。看个人爱好了,总之要做的就是保证在类创建完成后我们的池中要有连接。不然获取的时候会报错的。
上面有说到实现了接口之外,还得重写一下接口中的Connection getConnection()这个方法
@Override
public Connection getConnection() throws SQLException {
final ConnectionManager arg3 = getFreeConnection();//自定义获取连接
final Connection proxyConnection = arg3.getConn();
return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { Connection.class }, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("close".equals(method.getName())) {
conns.get(conns.indexOf(arg3)).setBusy(false);//修改连接为可用当执行关闭时并不是真正关闭
return null;
}
return method.invoke(proxyConnection, args);
}
});
}
这里我使用了动态代理机制,返回的被代理对象其实为数据库连接Connection实例。不清楚动态代理的可以看我的这一篇文章。讲解了静态代理和动态代理机制。
这个方法重点关注动态代理里面的invoke方法。可以看到在里面拦截了一个close方法。也就是当我们执行了close方法时,并不是关闭这个连接,而是把连接标记为可用。这样子下一个线程获取连接的时候就能够使用这个连接了。
以上就是数据库连接池的基本实现原理。
上面有一行代码是获取我们自定义的ConnectionManager。这个方法原理是每次外部现场获取连接的时候都要去连接池也就是我们的Vector容器里面查找,如果找到可用连接,则直接返回这个连接如果没找的话再判断当前连接数是否达到了连接池中配置最大的数量。没有达到的话再新建一个连接。若已经达到最大连接数了,那么只能等待其他线程将连接释放掉了。相关代码如下:
//循环连接池查询是否有可用连接,没有的话则等待其他线程把连接释放后获取。
private synchronized ConnectionManager getFreeConnection() throws SQLException {
while (true) {
if(conns.size()>0) {//先检查池中是否还有可用连接
for(ConnectionManager arg0:conns) {
if(!arg0.isBusy()) {
arg0.setBusy(true);
return arg0;
}
}
}
if(conns.size()<maxAlive) {
try {
Class.forName(driverClassName);
Connection arg1 = DriverManager.getConnection(url, username, password);
ConnectionManager arg2 = new ConnectionManager(arg1, false);
conns.add(arg2);
return arg2;
} catch (ClassNotFoundException e) {
throw new SQLException(e.getMessage());
}
} else {
try {
Thread.sleep(100);
System.out.println("池中连接数为:"+conns.size()+",无可用连接,等待中...");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
注意这个方法是加了synchronized同步锁关键字的。也就是说每次只能有一个线程访问。若不加同步锁的话,会导致创建的连接数大于指定的连接数量。
代码写完后,我们开始进行测试。
首先先写一个DBUtil.java工具类来或者我们的数据库连接池实例。
package spring;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
public class DBUtil {
private static DataSource dataSource;
public static DataSource getDataSourceInstance() throws SQLException{
if(dataSource == null)
dataSource = new PoolConnect();
return dataSource;
}
public static Connection getConnection() throws SQLException {
if(dataSource == null)
dataSource = new PoolConnect();
return dataSource.getConnection();
}
}
为防止过多的线程池被创建,我们这里使用了单例模式来获取连接池实例。
接下里是测试类,Test.java
package spring;
import java.sql.SQLException;
import java.util.function.DoubleUnaryOperator;
import javax.sql.DataSource;
public class Test {
public static void main(String[] args) throws SQLException, InterruptedException {
DataSource b = DBUtil.getDataSourceInstance();
for(int i=0;i<300;i++) {
new Thread(new Runnable() {
@Override
public void run() {
UserService u = new UserDao(b);
TransactionManager t = new TransactionManager(b);
try {
t.start();
u.buy();
u.addShops();
t.commit();
t.close();
} catch (SQLException e) {
e.printStackTrace();
try {
t.rollBack();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
e.printStackTrace();
}
}
}).start();
}
}
}
可以看到,我在主类里面新建了300个线程。模拟300个用户同时对数据库读写操作。如果没有使用连接池的话,那么至少要创建300个以上的连接。我们使用池化技术,观测一波

可以看到,连接池中最大连接数一直为3。至此,一个数据库连接池就完成了。
github地址:
https://github.com/luofuxian/spring




