实现一个简单的Java数据库线程池

97 阅读5分钟

前言

最近写golang发现go的sql包中默认实现的一个数据库连接池怪简单的,想着之前也没仔细看看java中的实现。顺带看了一下java的HikariCP初期版本的实现逻辑也差不多一致(后期就比较复杂了之后再看)。大致如下:

  • 初始生产最小数量连接到一个线程安全的容器中idleConnections
  • getConnection从idleConnections获取连接,若有直接返回,没有看是否生成的连接小于最大连接数。若小于直接生成并返回。若等于就是阻塞直到idleConnections有空闲连接。
  • 使用完成后放回idleConnections

实现测试效果

测试出来比HikariCP效果还要好,不知道是不是哪里没配置好?

MySQL8.2 13600KF+32G
HikariCP 5.0.1
JDK 11
Thread NUM 512
QUERY COUNT 512*10000
HikariCP=======================================
size :5120000
total took:72732
min  :0
max  :442
avg  :0.01420546875
5ms  :5118653,percent:99.97369140625
20ms :722,percent:0.0141015625
50ms :113,percent:0.00220703125
100ms :65,percent:0.00126953125
===============================================
SimplePool=====================================
size :5120000
total took:16784
min  :0
max  :541
avg  :0.003278125
5ms  :5119943,percent:99.99888671875
20ms :36,percent:7.03125E-4
50ms :7,percent:1.3671874999999999E-4
100ms :0,percent:0.0
===============================================

SimplePool实现代码

package com.tsq.simple;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 一个简单的数据库连接池
 *
 * @author tangsq
 * @date 2024/09/05
 */
public class SimplePool implements DbPool {


    /**
     * 空闲回收池
     */
    private final LinkedBlockingQueue<Connection> idlePool;

    /**
     * 使用池,这个可以不要但可能不归还,Hirkia是使用的代理连接close时会归还
     */
    private final LinkedBlockingQueue<Connection> busyPool;


    /**
     * 当前new的连接
     */
    private final AtomicInteger activeSize = new AtomicInteger(0);


    /**
     * 配置
     */
    private final SimpleConfig configuration;


    private final DataSource dataSource;
    
    
     SimplePool(SimpleConfig configuration) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, SQLException {
        this.configuration = configuration;
        idlePool = new LinkedBlockingQueue<>();
        busyPool = new LinkedBlockingQueue<>();
        Class<?> clazz = Class.forName(configuration.getDriverClassName());
        this.dataSource = (DataSource) clazz.getDeclaredConstructor().newInstance();
        //设置属性
        setPropertyToDataSource();
        addConnections(this.configuration.getMinPoolSize());
    }


    public void setPropertyToDataSource() throws IllegalAccessException {
        //反射设置属性值,这里标准的dataSource并没有定义,而且反射出的数据源由不同厂家提供可能有定制属性
        //通过getDeclaredFields()方法获取对象类中的所有属性(含私有)
        Field[] configFields = configuration.getClass().getDeclaredFields();
        Map<String, Object> configFieldMap = new HashMap<>();

        for (Field field : configFields) {
            field.setAccessible(true);
            configFieldMap.put(field.getName(), field.get(this.configuration));
        }

        // 获取当前类的父类
        Class<?> parent = this.dataSource.getClass();

        // 递归遍历父类
        while (parent != null) {
            Field[] dataSourceFields = parent.getDeclaredFields();
            //遍历属性
            for (Field field : dataSourceFields) {
                //通过setAccessible()设置为true,允许通过反射访问私有变量
                Object value = configFieldMap.get(field.getName());
                if (value == null) {
                    continue;
                }
                field.setAccessible(true);
                field.set(this.dataSource, value);
            }
            // 获取父类的父类
            parent = parent.getSuperclass();
        }

    }

    @Override
    public void init() {

    }

    public void addConnections(int upper) throws SQLException {
        for (int i = activeSize.get(); i < upper; i++) {
            //可以增加测试联通sql
            addConnection();

        }
    }

    public Connection addConnection() throws SQLException {
        Connection connection = this.dataSource.getConnection();
        this.idlePool.add(connection);
        activeSize.addAndGet(1);
        return connection;
    }

    @Override
    public Connection getConnection() throws SQLException, InterruptedException {
        Connection conn = idlePool.poll();
        if (conn != null) {
            busyPool.offer(conn);
            return conn;
        }
        if (activeSize.get() < configuration.getMaxPoolSize()) {
            //新增连接
            return addConnection();
        }
        // 如果空闲池中连接数达到maxSize, 则阻塞等待归还连接
        conn = idlePool.poll(10000, TimeUnit.MILLISECONDS);// 阻塞获取连接,如果10秒内有其他连接释放,
        if (conn == null) {
            throw new RuntimeException("等待连接超时");
        }
        busyPool.offer(conn);
        return conn;
    }

    @Override
    public void release(Connection connection) {
        if (connection != null) {
            busyPool.remove(connection);
            idlePool.offer(connection);
        }
    }

    @Override
    public void destroy() {

    }

测试代码


   public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, SQLException, InterruptedException {
        System.out.println("HikariCP=======================================");
        HikariTest();
        System.out.println("===============================================");
        System.out.println("SimplePool=====================================");
        SimpleTest();
        System.out.println("===============================================");

    }


    public static void SimpleTest() throws SQLException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, InterruptedException {
        LinkedBlockingQueue<Long> time = new LinkedBlockingQueue<>();
        SimpleConfig simpleConfig = new SimpleConfig();
        simpleConfig.setIdleTimeout(1000);
        simpleConfig.setConnectionTimeout(1000);
        simpleConfig.setJdbcUrl("jdbc:mysql://localhost:3306?useUnicode=true&characterEncoding=UTF-8");
        simpleConfig.setMinPoolSize(10);
        simpleConfig.setMaxPoolSize(100);
        simpleConfig.setDatabaseName("rank");
        simpleConfig.setDriverClassName("com.mysql.cj.jdbc.MysqlDataSource");
        simpleConfig.setUser("root");
        simpleConfig.setPassword("root");
        SimplePool simplePool = new SimplePool(simpleConfig);

        int THREAD_COUNT = 512;
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        for (int k = 0; k < 10000; k++) {
            CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_COUNT);
            for (int i = 0; i < THREAD_COUNT; i++) {
                executor.submit(() -> {
                    Connection connection = null;
                    PreparedStatement preparedStatement = null;
                    ResultSet resultSet = null;
                    try {
                        long start1 = System.currentTimeMillis();
                        connection = simplePool.getConnection();
                        long start2 = System.currentTimeMillis();
                        time.add(start2 - start1);
                        preparedStatement = connection.prepareStatement("select * from post1 limit 1");
                        resultSet = preparedStatement.executeQuery();
                        while (resultSet.next()) {
                            String id = resultSet.getString(1);
                        }
                    } catch (Exception e) {
                        System.out.println(e);
                    } finally {
                        try {
                            if (resultSet != null) {
                                resultSet.close();
                            }
                            if (preparedStatement != null) {
                                preparedStatement.close();
                            }
                        } catch (SQLException e) {
                            System.out.println(e);
                        }
                        simplePool.release(connection);
                        countDownLatch1.countDown();
                    }
                });
            }
            countDownLatch1.await();
        }
        int size = time.size();
        long max = 0;
        long min = 0;
        long total = 0;
        long ms5 = 0;
        long ms20 = 0;
        long ms50 = 0;
        long ms100 = 0;
        for (long t : time) {
            if (t > max) {
                max = t;
            }
            if (t < min) {
                min = t;
            }
            if (t <= 5) {
                ms5++;
            }
            if (5 < t && t <= 20) {
                ms20++;
            }
            if (20 < t && t <= 50) {
                ms50++;
            }
            if (50 < t && t <= 100) {
                ms100++;
            }
            total += t;
        }
        System.out.println("size :" + size);
        System.out.println("total took:" + total);
        System.out.println("min  :" + min);
        System.out.println("max  :" + max);
        System.out.println("avg  :" + total * 1.0 / size);
        System.out.println("5ms  :" + ms5 + ",percent:" + ms5 * 1.0 / size * 100);
        System.out.println("20ms :" + ms20 + ",percent:" + ms20 * 1.0 / size * 100);
        System.out.println("50ms :" + ms50 + ",percent:" + ms50 * 1.0 / size * 100);
        System.out.println("100ms :" + ms100 + ",percent:" + ms100 * 1.0 / size * 100);
    }

    public static void HikariTest() throws SQLException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, InterruptedException {
        LinkedBlockingQueue<Long> time = new LinkedBlockingQueue<>();
        HikariConfig config = new HikariConfig();
        // jdbc properties
        config.setDriverClassName("com.mysql.cj.jdbc.MysqlDataSource");
        config.setJdbcUrl("jdbc:mysql://localhost:3306/rank?useUnicode=true&characterEncoding=UTF-8");
        config.setUsername("root");
        config.setPassword("root");
        config.setMinimumIdle(10);
        config.setMaximumPoolSize(100);
        config.setConnectionTimeout(30000);
        config.setMaxLifetime(0);
        config.setIdleTimeout(0);
        HikariDataSource hikariDataSource = new HikariDataSource(config);


        int THREAD_COUNT = 512;
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
        for (int k = 0; k < 10000; k++) {
            CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_COUNT);
            for (int i = 0; i < THREAD_COUNT; i++) {
                executor.submit(() -> {
                    Connection connection = null;
                    PreparedStatement preparedStatement = null;
                    ResultSet resultSet = null;
                    try {
                        long start1 = System.currentTimeMillis();
                        connection = hikariDataSource.getConnection();
                        long start2 = System.currentTimeMillis();
                        time.add(start2 - start1);
                        preparedStatement = connection.prepareStatement("select * from post1 limit 1");
                        resultSet = preparedStatement.executeQuery();
                        while (resultSet.next()) {
                            String id = resultSet.getString(1);
                        }
                    } catch (Exception e) {
                        System.out.println(e);
                    } finally {
                        try {
                            if (resultSet != null) {
                                resultSet.close();
                            }
                            if (preparedStatement != null) {
                                preparedStatement.close();
                            }
                        } catch (SQLException e) {
                            System.out.println(e);
                        }
                        try {
                            assert connection != null;
                            connection.close();
                        } catch (SQLException e) {
                            System.out.println(e);
                        }
                        countDownLatch1.countDown();
                    }
                });
            }
            countDownLatch1.await();
        }
        int size = time.size();
        long max = 0;
        long min = 0;
        long total = 0;
        long ms5 = 0;
        long ms20 = 0;
        long ms50 = 0;
        long ms100 = 0;
        for (long t : time) {
            if (t > max) {
                max = t;
            }
            if (t < min) {
                min = t;
            }
            if (t <= 5) {
                ms5++;
            }
            if (5 < t && t <= 20) {
                ms20++;
            }
            if (20 < t && t <= 50) {
                ms50++;
            }
            if (50 < t && t <= 100) {
                ms100++;
            }
            total += t;
        }
        System.out.println("size :" + size);
        System.out.println("total took:" + total);
        System.out.println("min  :" + min);
        System.out.println("max  :" + max);
        System.out.println("avg  :" + total * 1.0 / size);
        System.out.println("5ms  :" + ms5 + ",percent:" + ms5 * 1.0 / size * 100);
        System.out.println("20ms :" + ms20 + ",percent:" + ms20 * 1.0 / size * 100);
        System.out.println("50ms :" + ms50 + ",percent:" + ms50 * 1.0 / size * 100);
        System.out.println("100ms :" + ms100 + ",percent:" + ms100 * 1.0 / size * 100);
    }