Java源码系列-手写数据库连接池

1.目标

Java源码系列-手写数据库连接池(附源码)

为了理解数据库连接池的底层原理,我们可以自己手写一个类似HikariDruid一样的高性能的数据库连接池!

源码在教学课程的附件中下载:https://download.csdn.net/course/detail/37097

2.数据库连接池原理

2.1.基本原理

在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法。

如外部使用者可通过getConnection方法获取数据库连接,使用完毕后再通过releaseConnection方法将连接返回,注意此时的连接并没有关闭,而是由连接池管理器回收,并为下一次使用做好准备。
在这里插入图片描述

2.2.连接池作用

在这里插入图片描述

资源重用
由于数据库连接得到重用,避免了频繁创建、释放连接引起的大量性能开销。在减少系统消耗的基础上,增进了系统环境的平稳性(减少内存碎片以级数据库临时进程、线程的数量)

更快的系统响应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于池内备用。此时连接池的初始化操作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而缩减了系统整体响应时间。

新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接的配置,实现数据库连接技术。

统一的连接管理,避免数据库连接泄露
在较为完备的数据库连接池实现中,可根据预先的连接占用超时设定,强制收回被占用的连接,从而避免了常规数据库连接操作中可能出现的资源泄露

2.3.市面常见的数据库连接池简介

DBCP (Database Connection Pool)

是一个依赖Jakarta commons-pool对象池机制的数据库连接池,Tomcat的数据源使用的就是DBCP。目前 DBCP 有两个版本分别是 1.3 和 1.4。1.3 版本对应的是 JDK 1.4-1.5 和 JDBC 3,而1.4 版本对应 JDK 1.6 和 JDBC 4。因此在选择版本的时候要看看你用的是什么 JDK 版本了,功能上倒是没有什么区别。

C3P0

是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。

Proxool

Proxool是一种Java数据库连接池技术。是sourceforge下的一个开源项目,这个项目提供一个健壮、易用的连接池,最为关键的是这个连接池提供监控的功能,方便易用,便于发现连接泄漏的情况。

BoneCP

是一个开源的快速的 JDBC 连接池。BoneCP很小,只有四十几K(运行时需要log4j和Google Collections的支持,这二者加起来就不小了),而相比之下 C3P0 要六百多K。另外个人觉得 BoneCP 有个缺点是,JDBC驱动的加载是在连接池之外的,这样在一些应用服务器的配置上就不够灵活。当然,体积小并不是 BoneCP 优秀的原因,BoneCP 到底有什么突出的地方呢,得看看性能测试报告。

Druid简介

Druid是阿里巴巴的一个开源数据库连接池,基于Apache 2.0协议,可以免费自由使用。但它不仅仅是一个数据库连接池,它还包含一个ProxyDriver,一系列内置的JDBC组件库,一个SQL Parser。Druid能够提供强大的监控和扩展功能。但Druid只支持JDK 6以上版本,不支持JDK 1.4和JDK 5.0。

HikariCP

HikariCP 是一个高性能的 JDBC 连接池组件,号称性能最好的后起之秀,是一个基于BoneCP做了不少的改进和优化的高性能JDBC连接池。Spring Boot 2都已经宣布支持了该组件,由之前的Tomcat换成HikariCP。其性能远高于c3p0、tomcat等连接池,以致后来BoneCP作者都放弃了维护,在Github项目主页推荐大家使用HikariCP。

2.4.第一代数据库连接池

在这里插入图片描述
其中C3p0DBCPProxoolBoneCP都已经很久没更新了,TJP(Tomcat JDBC Pool)DruidHikariCP则仍处于活跃的更新中。

第一代数据库连接池性能:
在这里插入图片描述

2.5.站在巨人肩膀上的第二代连接池

功能全面的Druid

  • 提供性能卓越的连接池功能
  • 还集成了sql监控,黑名单拦截等功能,用它自己的话说,druid是“为监控而生”
  • druid另一个比较大的优势,就是中文文档比较全面(毕竟是国人的项目么)在githubwiki页面

性能无敌的HikariCP
在这里插入图片描述

  • 字节码精简:优化代码,直到编译后的字节码最少,这样,CPU缓存可以加载更多的程序代码
  • 优化代理和拦截器:减少代码,例如HikariCPStatement proxy只有100行代码,只有BoneCP的十分之一
  • 自定义数组类型(FastStatementList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描
  • 自定义集合类型(ConcurrentBag):提高并发读写的效率
  • 其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究

3.手写连接池步骤

3.1.读取外部配置信息

db.properties

#文件名:db.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/singerdb
jdbc.username=root
jdbc.password=123456
# 初始化连接数
jdbc.initSize=3
# 最大连接数
jdbc.maxSize=6
#是否启动检查
jdbc.health=true

#检查延迟时间
jdbc.delay=2000
#间隔时间,重复获得连接的频率
jdbc.period=2000

# 连接超时时间,10S
jdbc.timeout=100000

# 重复获得连接的频率
jdbc.waittime=1000

配置类DataSourceConfig读取配置文件:

package com.bruce.pool;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Properties;

public class DataSourceConfig {

    private String driver;
    private String url;
    private String username;
    private String password;
    private String initSize;
    private String maxSize;
    private String health;
    private String delay;
    private String period;

    //连接超时时间,10S
    private String timeout;

    //重复获得连接的频率,1S
    private String waittime;// 重复获得连接的频率

    //省略set和get方法// 编写构造器,在构造器中对属性进行初始化
    public DataSourceConfig() {
        Properties prop = new Properties();
        // maven项目中读取文件好像只有这中方式
        InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties");
        try {
            prop.load(stream);
            // 在构造器中调用setter方法,这里属性比较多,我们肯定不是一步一步的调用,建议使用反射机制
            for (Object obj : prop.keySet()) {
                // 获取形参,怎么获取呢?这不就是配置文件的key去掉,去掉什么呢?去掉"jdbc."
                String fieldName = obj.toString().replace("jdbc.", "");
                Field field = this.getClass().getDeclaredField(fieldName);
                Method method = this.getClass().getMethod(toUpper(fieldName), field.getType());
                method.invoke(this, prop.get(obj));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 读取配置文件中的key,并把他转成正确的set方法
    public String toUpper(String fieldName) {
        char[] chars = fieldName.toCharArray();
        chars[0] -= 32;     // 如何把一个字符串的首字母变成大写
        return "set" + new String(chars);
    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getInitSize() {
        return initSize;
    }

    public void setInitSize(String initSize) {
        this.initSize = initSize;
    }

    public String getMaxSize() {
        return maxSize;
    }

    public void setMaxSize(String maxSize) {
        this.maxSize = maxSize;
    }

    public String getHealth() {
        return health;
    }

    public void setHealth(String health) {
        this.health = health;
    }

    public String getDelay() {
        return delay;
    }

    public void setDelay(String delay) {
        this.delay = delay;
    }

    public String getPeriod() {
        return period;
    }

    public void setPeriod(String period) {
        this.period = period;
    }

    public String getTimeout() {
        return timeout;
    }

    public void setTimeout(String timeout) {
        this.timeout = timeout;
    }


    public String getWaittime() {
        return waittime;
    }

    public void setWaittime(String waittime) {
        this.waittime = waittime;
    }

    @Override
    public String toString() {
        return "DataSourceConfig{" +
                "driver='" + driver + '\'' +
                ", url='" + url + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", initSize='" + initSize + '\'' +
                ", maxSize='" + maxSize + '\'' +
                ", health='" + health + '\'' +
                ", delay='" + delay + '\'' +
                ", period='" + period + '\'' +
                ", timeout='" + timeout + '\'' +
                ", waittime=" + waittime +
                '}';
    }
}

3.2.连接池类

接口IConnectionPool

package com.bruce.pool;

import java.sql.Connection;

public interface IConnectionPool {

    /**
     * 获取Connection 复用机制
     */
    Connection getConn();

    /**
     *释放连接(可回收机制)
     */
    void release(Connection conn);
}

接口IConnectionPool实现类ConnectionPool

package com.bruce.pool;

import javafx.concurrent.Worker;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;

public class ConnectionPool implements IConnectionPool {

    // 加载配置类
    DataSourceConfig config;

    // 写一个参数,用来标记当前有多少个活跃的连接(总的连接数)
    private AtomicInteger currentActive = new AtomicInteger(0);

    // 创建一个集合,干嘛的呢?用来存放连接,毕竟我们刚刚初始化的时候就需要创建initSize个连接
    // 并且,当我们释放连接的时候,我们就把连接放到这里面
    Vector<Connection> freePools = new Vector<Connection>();

    // 正在使用的连接池
    Vector<PoolEntry> usePools = new Vector<PoolEntry>();

    public ConnectionPool(DataSourceConfig config) {
        this.config = config;
        init();
    }

    // 初始化方法
    private void init() {
        try {
            // 我们的jdbc是不是每次都要加载呢?肯定不是的,只要加载一次就够了
            Class.forName(config.getDriver());
            for (int i = 0; i < Integer.valueOf(config.getInitSize()); i++) {
                Connection conn = createConn();
                freePools.add(conn);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //开启任务检查
        check();
    }

    // 定时检查占用时间超长的连接,并关闭
    private void check() {
        if (Boolean.valueOf(config.getHealth())) {
            Worker worker = new Worker();
            /**
             * #检查延迟时间
             * jdbc.delay=2000
             * #间隔时间,重复获得连接的频率
             * jdbc.period=2000
             */
            new Timer().schedule(worker, Long.valueOf(config.getDelay()), Long.valueOf(config.getPeriod()));
        }
    }


    class Worker extends TimerTask {
        public void run() {
            System.out.println("例行检查...");
            for (int i = 0; i < usePools.size(); i++) {
                PoolEntry entry = usePools.get(i);
                long startTime = entry.getUseStartTime();
                long currentTime = System.currentTimeMillis();
                try {
                    // # 连接超时时间,10S
                    if ((currentTime - startTime) > Long.valueOf(config.getTimeout())) {
                        Connection conn = entry.getConn();
                        if (conn != null && !conn.isClosed()) {
                            conn.close();
                            usePools.remove(i);
                            currentActive.decrementAndGet();
                            System.out.println("发现有超时连接强行关闭," + conn + ",空闲连接数:" + freePools.size() + "," + "在使用连接数:" + usePools.size() + ",总的连接数:" + currentActive.get());
                        }
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                } finally {
                }
            }
        }
    }

    public synchronized Connection createConn() {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection(config.getUrl(), config.getUsername(), config.getPassword());
            currentActive.incrementAndGet();
            System.out.println("new一个新的连接:" + conn);
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }

    /**
     * 创建连接有了,是不是也应该获取连接呢?
     * @return
     */
    public synchronized Connection getConn() {
        Connection conn = null;
        //如果空闲连接池中不为空,获取一个连接出来
        if (!freePools.isEmpty()) {
            conn = freePools.get(0);
            freePools.remove(0);
        } else {
            if (currentActive.get() < Integer.valueOf(config.getMaxSize())) {
                //如果空闲连接为空,
                conn = createConn();
            } else {
                try {
                    //如果总连接数超过了连接总数,需要等待
                    System.out.println(Thread.currentThread().getName() + ",连接池最大连接数为:" + config.getMaxSize() + "已经满了,需要等待...");
                    wait(Integer.valueOf(config.getWaittime()));
                    return getConn();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                }
            }
        }
        PoolEntry poolEntry = new PoolEntry(conn, System.currentTimeMillis());
        // 获取连接干嘛的?不就是使用的吗?所以,每获取一个,就放入正在使用池中
        usePools.add(poolEntry);
        System.out.println(Thread.currentThread().getName() + ",获取并使用连接:" + conn + ",空闲连接数:" + freePools.size() + "," + "再使用连接数:" + usePools.size() + ",总的连接数:" + currentActive.get());
        return poolEntry.getConn();
    }

    /**
     * 创建连接,获取连接都已经有了,接下来就是该释放连接了
     *
     * @param conn
     */
    public synchronized void release(Connection conn) {
        try {
            if (!conn.isClosed() && conn != null) {
                freePools.add(conn);
            }
            for (int i = 0; i < usePools.size(); i++) {
                if (usePools.get(i).getConn() == conn) {
                    usePools.remove(i);
                }
            }
            System.out.println("回收了一个连接:" + conn + ",空闲连接数为:" + freePools.size() + ",在用连接数为:" + usePools.size());
            notifyAll();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
        }
    }
}

PoolEntry

package com.bruce.pool;

import java.sql.Connection;

public class PoolEntry {

    private Connection conn;
    private long useStartTime; //开始使用时间

    public Connection getConn() {
        return conn;
    }

    public void setConn(Connection conn) {
        this.conn = conn;
    }

    public long getUseStartTime() {
        return useStartTime;
    }

    public void setUseStartTime(long useStartTime) {
        this.useStartTime = useStartTime;
    }

    public PoolEntry(Connection conn, long useStartTime) {
        super();
        this.conn = conn;
        this.useStartTime = useStartTime;
    }
}

3.3.连接池管理类

package com.bruce.pool;

import java.sql.Connection;

public class ConnectionPoolManager {

    private static DataSourceConfig config = new DataSourceConfig();
    private static ConnectionPool connectionPool = new ConnectionPool(config);

    // 获取连接(重复利用机制)
    public static Connection getConnection() {
        return connectionPool.getConn();
    }

    // 释放连接(可回收机制)
    public static void releaseConnection(Connection connection) {
        connectionPool.release(connection);
    }

}

4. 测试

package com.bruce.test;

import com.bruce.pool.ConnectionPoolManager;
import com.bruce.pool.DataSourceConfig;

import java.sql.Connection;

public class TestDataSource {

    public static void main(String[] args) {
        ThreadConnection threadConnection = new ThreadConnection();
        for (int i = 1; i <=8; i++) {
            Thread thread = new Thread(threadConnection, "线程:" + i);
            thread.start();
        }
    }

}

class ThreadConnection implements Runnable {

    public void run() {
        Connection connection = ConnectionPoolManager.getConnection();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
        ConnectionPoolManager.releaseConnection(connection);
    }

}

5. 运行结果

C:\JDK8\bin\java.exe "-javaagent:G:\softDevelopment\IDEA201932\IntelliJ IDEA 2019.3.2\lib\idea_rt.jar=4999:G:\softDevelopment\IDEA201932\IntelliJ IDEA 2019.3.2\bin" -Dfile.encoding=UTF-8 -classpath C:\JDK8\jre\lib\charsets.jar;C:\JDK8\jre\lib\deploy.jar;C:\JDK8\jre\lib\ext\access-bridge-64.jar;C:\JDK8\jre\lib\ext\cldrdata.jar;C:\JDK8\jre\lib\ext\dnsns.jar;C:\JDK8\jre\lib\ext\jaccess.jar;C:\JDK8\jre\lib\ext\jfxrt.jar;C:\JDK8\jre\lib\ext\localedata.jar;C:\JDK8\jre\lib\ext\nashorn.jar;C:\JDK8\jre\lib\ext\sunec.jar;C:\JDK8\jre\lib\ext\sunjce_provider.jar;C:\JDK8\jre\lib\ext\sunmscapi.jar;C:\JDK8\jre\lib\ext\sunpkcs11.jar;C:\JDK8\jre\lib\ext\zipfs.jar;C:\JDK8\jre\lib\javaws.jar;C:\JDK8\jre\lib\jce.jar;C:\JDK8\jre\lib\jfr.jar;C:\JDK8\jre\lib\jfxswt.jar;C:\JDK8\jre\lib\jsse.jar;C:\JDK8\jre\lib\management-agent.jar;C:\JDK8\jre\lib\plugin.jar;C:\JDK8\jre\lib\resources.jar;C:\JDK8\jre\lib\rt.jar;E:\Server\DataSourceDemo02\target\test-classes;E:\Server\DataSourceDemo02\target\classes;D:\maven2022\repository\mysql\mysql-connector-java\5.1.25\mysql-connector-java-5.1.25.jar;D:\maven2022\repository\junit\junit\4.12\junit-4.12.jar;D:\maven2022\repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar com.bruce.test.TestDataSource
new一个新的连接:com.mysql.jdbc.JDBC4Connection@609c2543
new一个新的连接:com.mysql.jdbc.JDBC4Connection@1b12761b
new一个新的连接:com.mysql.jdbc.JDBC4Connection@14fbd532
线程:1,获取并使用连接:com.mysql.jdbc.JDBC4Connection@609c2543,空闲连接数:2,再使用连接数:1,总的连接数:3
线程:7,获取并使用连接:com.mysql.jdbc.JDBC4Connection@1b12761b,空闲连接数:1,再使用连接数:2,总的连接数:3
线程:8,获取并使用连接:com.mysql.jdbc.JDBC4Connection@14fbd532,空闲连接数:0,再使用连接数:3,总的连接数:3
new一个新的连接:com.mysql.jdbc.JDBC4Connection@142b2903
线程:5,获取并使用连接:com.mysql.jdbc.JDBC4Connection@142b2903,空闲连接数:0,再使用连接数:4,总的连接数:4
new一个新的连接:com.mysql.jdbc.JDBC4Connection@281a7bc3
线程:6,获取并使用连接:com.mysql.jdbc.JDBC4Connection@281a7bc3,空闲连接数:0,再使用连接数:5,总的连接数:5
new一个新的连接:com.mysql.jdbc.JDBC4Connection@6f771e7f
线程:4,获取并使用连接:com.mysql.jdbc.JDBC4Connection@6f771e7f,空闲连接数:0,再使用连接数:6,总的连接数:6
线程:3,连接池最大连接数为:6已经满了,需要等待...
线程:2,连接池最大连接数为:6已经满了,需要等待...
回收了一个连接:com.mysql.jdbc.JDBC4Connection@1b12761b,空闲连接数为:1,在用连接数为:5
线程:2,获取并使用连接:com.mysql.jdbc.JDBC4Connection@1b12761b,空闲连接数:0,再使用连接数:6,总的连接数:6
线程:3,连接池最大连接数为:6已经满了,需要等待...
回收了一个连接:com.mysql.jdbc.JDBC4Connection@609c2543,空闲连接数为:1,在用连接数为:5
回收了一个连接:com.mysql.jdbc.JDBC4Connection@14fbd532,空闲连接数为:2,在用连接数为:4
线程:3,获取并使用连接:com.mysql.jdbc.JDBC4Connection@609c2543,空闲连接数:1,再使用连接数:5,总的连接数:6
回收了一个连接:com.mysql.jdbc.JDBC4Connection@281a7bc3,空闲连接数为:2,在用连接数为:4
回收了一个连接:com.mysql.jdbc.JDBC4Connection@142b2903,空闲连接数为:3,在用连接数为:3
回收了一个连接:com.mysql.jdbc.JDBC4Connection@6f771e7f,空闲连接数为:4,在用连接数为:2
例行检查...
回收了一个连接:com.mysql.jdbc.JDBC4Connection@609c2543,空闲连接数为:5,在用连接数为:1
回收了一个连接:com.mysql.jdbc.JDBC4Connection@1b12761b,空闲连接数为:6,在用连接数为:0
例行检查...
例行检查...

源码在教学课程的附件中下载:https://download.csdn.net/course/detail/37097

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

熊猫-IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值