池化技术原理,手写数据库连接池

池化技术在后端开发中应用非常广泛,有数据库连接池,线程池,对象池,常量池等。池化技术的出现是为了提高性能。实际就是对一些使用率较高,且创建销毁比较耗时的资源进行缓存,避免重复地创建和销毁,做到资源回收利用。

传统的数据库连接池有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();

每一个请求过来都要建立数据库连接,连接数据库需要建立网络连接、系统要分配内存资源、使用完必须断开连接,这些过程可能比实际进行的数据库操作耗时好多倍。并且在高并发量的请求情况下,应用响应速度就会非常慢,甚至造成服务器崩溃。

由此可见,数据库连接是一种非常昂贵的资源。

二、数据库连接池技术

为解决上述问题,可以采用数据库连接池技术。数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。初始化时在缓冲池中放入指定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定各种参数来控制初始连接数量、最大连接数量、最大空闲连接数量、等待时间等,还可以通过连接池的管理机制来监视数据库连接的数量和使用情况,方便开发测试和性能调优。

有了这些需求下面来实现一个简单的数据库连接池,实现连接池初始化方法、获取连接方法、回收连接方法、销毁池方法,以及几个参数的可配置。

1、工具类:MysqlConnectionUtils

首先将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();
        }
    }
}
2、定义接口:ConnectionPool

定义接口:

public interface ConnectionPool {
    /*初始化*/
    void init(int initSize,int maxSize,int idleCount,long waitTime);
    /*获取连接*/
    Connection getConnection() throws TimeoutException;
    /*回收*/
    void recycle(Connection conn);
    /*销毁*/
    void destory();
}
3、实现类:MyConnectionPool

属性

这个就是我们要实现的线程池了,首先看该类的一些属性:

public class MyConnectionPool implements ConnectionPool {

    //初始化连接数量
    int initSize;
    //最大连接数量
    int maxSize;
    //最大空闲连接数量
    int idleCount;
    //最大等待时间
    long waitTime;
    //活跃连接数
    AtomicInteger activeSize;
    //空闲队列
    BlockingQueue<Connection> idle;
    //已使用队列
    BlockingQueue<Connection> busy;

这里定义了两个队列,一个存放空闲连接,一个存放正在使用的连接。活跃连接数就是连接池中未关闭的连接的总数,也就是两个队列的连接总数。

取出连接和回收连接方法模型图:

池化技术原理,手写数据库连接池_第1张图片

构造器

通过构造方法完成初始化:

	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判断中进行原子操作,可形成锁,保证线程安全。

接下来实现从连接池中获取连接的方法:

获取连接方法:getConnection()
    @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;
    }

以上代码核心就是两个队列弹出元素和加入元素的过程。然后插入了一些打印信息,方便测试的时候查看。

获取连接实际上有三种可能的方式以及一种超时抛异常的情况:

  • 空闲队列中有可用连接,直接从空闲队列弹出
  • 空闲队列没有连接了,并且活跃连接数没有超过配置的最大连接数,此时自己创建连接给到调用者,注意每成功创建一个连接,活跃连接数activeSize加1。(从这里可以看出,如果最大空闲连接数设置得过小,最大连接数远大于最大空闲连接数的话,线程池会大量创建连接,性能就会比较差。)
  • 空闲队列没有连接,并且活跃连接数达到配置的最大连接数,此时不能在创建连接,只能阻塞等待,当超时时间内有连接被回收进空闲队列,阻塞结束,获得连接。
  • 当达到超时时间没有连接被回收,抛出超时异常。(这里超时时间的配置应当根据多数线程占用连接的时间决定

然后看另一个关键的方法,回收方法:

回收方法:recycle()
    @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()+"]"+" ++++未知连接,直接关闭,不予回收++++");
        }
    }

回收方法实际就是两步,

  • 将指定的连接从busy队列中弹出,如果该连接不是busy队列中的连接,也就是弹出失败,就直接关闭它。
  • 检查空闲队列内的连接数,如果达到配置的最大空闲连接数,则直接关闭连接。如果没有达到,就将该连接放入空闲队列。

注意每关闭一个连接,活跃连接数activeSize减1。

连接池销毁方法:destroy()
	@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,正在使用连接数:0

detroy MyConnectionPool!!
已建立连接数:0,空闲连接数:0,正在使用连接数:0

由于篇幅原因,打印信息省略一部分。

本次测试得到以下信息:

  • 由于设置的超时时间是20秒,远大于各线程占用时间,所以没有出现超时异常的情况。
  • 初始化了10个连接,“创建一个连接”输出了10行,“空闲连接数量太多,关闭连接”输出了5行,所有线程执行完毕后,已建立连接数:15,空闲连接数:15,正在使用连接数: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,正在使用连接数:0

detroy MyConnectionPool!!
已建立连接数:0,空闲连接数:0,正在使用连接数:0

可以看到由于线程占用连接时间为随机1到5秒,而设置超时时间是3秒,在被占用连接数量达到最大连接数20时,某线程3秒内没有获取到连接,就会抛出超时异常。不影响其他线程。

线程池连接数量始终符合根据配置该有的数量。没有超出配置,也没有连接泄露。


手写连接池测试完毕。

你可能感兴趣的:(java并发)