自己动手实现数据库连接池

数据库连接池

1. 数据库连接池是干什么的

假如我们有个应用程序需要每隔10秒查询一次数据库,我们可以用以下方式

方法1:每次查询的时候都新建一个数据库连接,查询结束关闭数据库连接。

由于数据库连接的建立是一个非常耗费资源的过程,所以这种每次都新建连接的方式非常浪费资源,不可取。

方法2:在最开始的新建一个数据库连接,后续过程中一直使用这个数据库连接进行查询,直到最后关闭

这种方式虽然减少了新建数据库的资源消耗。但是对于一个数据库连接,每10秒才使用一次,也是非常大的浪费。而且假如有一千个应用程序在同时运行,难道就要同时打开一千个数据库连接?

方法3:我们在开始的时候根据需要同时打开多个数据库连接放到一个容器中,每次应用程序进行查询的时候从容器中取出一个数据库连接进行查询,查询完毕放回容器。

方法3即没有重复新建数据库连接,也保证了每个数据库连接的使用率,其中所说的容器就是数据库连接池。

2. 数据库连接池的功能

数据库连接池不仅仅是一个数据库连接的容器,还应具有更加智能的管理数据库连接的功能。我们实现的数据库连接具有以下功能:

  1. 通过getConnection()获取一个数据库连接,如果池中存在连接则直接返回,如果池中没有连接,则新创建一个数据库连接并返回。
  2. 获取的数据库连接进行close()操作的时候,如果连接池中连接数量小于capacity,则该连接自动返回到连接池中,否则直接释放以节省资源。
  3. 即使连接池中的连接数量没有超过capacity,但当其中的部分连接很长时间没有使用时,也要进行释放以节约资源。

3. 数据库连接池中容器的设计

连接池中数据库连接存放的方式可以用队列存放,先放进来的先取出来,也可以用栈来存放,先放进来的后取出来,具体用那种方式,要看需要实现的功能

根据要实现的第三种功能得出,我们需要在存放数据库连接的时候记录连接的上一次使用时间,如果上一次使用时间超过一定时间,则关闭此连接。

如果我们使用队列来存储连接,我们会发现每次新放入的连接都放到了队尾,每次取出来的都是队列前面的最老的数据库连接,所以在不断的存取的过程中,队列里面每一个连接的上一次使用时间都会不断刷新。

一个使用队列的连接池如果有十个连接,每隔10秒取出一个连接使用并放回,则连接池的连接队列会不断从头部取出来,刷新使用时间,放回队列尾部,这样队列每一个连接的上次使用时间都不会超过当前10秒,队列里面所有连接都不能因为超时被释放。

所以我们应当使用栈来存放数据库连接,每次都从上面取出连接,使用完也放回上面。假如使用的频率特别低会导致栈底部的连接长时间未使用,则可以直接释放以节省资源。

连接容器中超时连接的释放有两种方式,1、在往容器中添加或者取出连接的时候释放,2、单独开一个线程不断轮询所有连接释放超时的连接。

我们采用的是第一种方式,在往容器中添加连接的时候释放超时连接,有以下三个原因:

  1. 单独开一个线程需要耗费更多的资源,也更加难以管理
  2. 使用栈来存储连接的话,实际上在不断的存取过程中,栈一直保持着从顶部到底部上次使用时间越来越长的规律,即栈中连接的使用时间是有序的。所以每次释放的时候,只需要从底部向上开始扫描,遇到超时的连接则进行释放,遇上非超时的连接则停止扫描,如果栈中连接均未超时,则只需要扫描最后一个就可以了。
  3. 使用数据库连接的时候,取出来使用应当比放回去的优先级更高,所以释放超时连接的操作应当放在放回连接池中的部分。
  4. 这种方法最坏的情况为:程序开始运行时打开了若干个数据库连接,放置回连接池中,后面则不再进行任何数据库操作(即不再往连接池中取出或存放连接)。这样会导致之前建立的连接一直存放在连接池中,得不到超时释放。但是这种情况出现的几率较少,严格来说这种情况可以通过程序编写避免,所以为了简单和稳定性可以忽略这种情况。

4.数据库连接池的实现

数据库连接池中栈容器的实现是基于Java自带的双向链表来实现的。

public class ConnectionStack{
    private final int timeout;//单位:秒
    private final int capacity;
    private LinkedList<ConnWithTime> list=new LinkedList<>();
    //内部类,用来记录Connection的上次使用时间
    private class ConnWithTime{
        private final long time=new Date().getTime();//实例化的时间即为上次归还的时间
        private Connection conn;
        public ConnWithTime(Connection conn){
            this.conn=conn;
        }
        //当前连接是否已经超时
        boolean isTimeout(){
            return new Date().getTime()-time>timeout*1000;
        }
    }
    public ConnectionStack(int capacity,int timeout){
        this.capacity=capacity;
        this.timeout=timeout;
    }
    public synchronized void push(Connection conn){
        clearTimeoutConnection();
        if(list.size()<capacity){
            list.addFirst(new ConnWithTime(conn));
        }else{
            connection.close();
        }
    }
    private void clearTimeoutConnection(){
        //接下来从栈底部开始遍历,把所有超时的连接关闭并且丢弃,直到未超时的连接为止
        //ps:在正常情况下,栈底部最后一个也是活跃的,所以直接break了,平常调用应该是常数时间
        Iterator<ConnWithTime> iterator = list.descendingIterator();
        while (iterator.hasNext()) {
            ConnWithTime connWithTime = iterator.next();
            if (connWithTime.isTimeout()) {
                connWithTime.conn.close();
                iterator.remove();
            } else {
                break;
            }
        }
    }
    public synchronized Connection pop(){
        if(list.isEmpty())return null;
        return list.removeFirst().conn;
    }

    public synchronized void close(){
        Iterator<ConnWithTime> iterator = list.descendingIterator();
        while (iterator.hasNext()) {
            ConnWithTime connWithTime = iterator.next();
            connWithTime.conn.close();
            iterator.remove();
        }
}

数据库连接池的主体部分实现应当如下:

public class DatabasePool{
    private ConnectionStack stack;
    private final String url,username,password;
    private volatile boolean isRun=true;

    public DatabasePool(String url, String username, String password, int capacity, int timeout) {
       this.url = url;
       this.username = username;
       this.password = password;
       stack = new ConnectionStack(capacity, timeout);
    }

    public Connection getConnection() throws SQLException{
        if(!isRun)throw new SQLException("pool closed");
        Connection conn=stack.pop();
        if(conn==null)conn= DriverManager.getConnection(url, username, password);
        final myConn=conn;
        //使用动态代理,当调用Connection.close()的时候自动将连接放回连接池中
        return (Connection) Proxy.newProxyInstance(
                    DatabasePool.class.getClassLoader(),
                    conn.getClass().getInterfaces(),
                    (proxy, method, args) -> {
                        if (method.getName().equals("close") && isRun) {//当为调用close函数并且连接池正在运行,则将连接放入stack中,否则直接调用相关函数
                            stack.push(myConn);//
                        } else {
                            return method.invoke(myConn, args);
                        }
                        return null;
                    });
    }
    public void close(){
        isRun=false;
        stack.close();
    }
}

5. 数据库连接池的使用

数据库连接池是线程安全的,所以即使在多线程条件下可以直接使用

DatabasePool pool=new DatabasePool(url,username,password,10,60*60);
//多个线程下可以同时如下使用
Connection conn=null;
try{
    conn=pool.getConnection();
    ...//执行数据库操作    
}catch(SQLException e){
    logger.exception(e);
}finally{
    if(conn!=null)conn.close();
}
展开阅读全文

没有更多推荐了,返回首页