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

Posted IT-老牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java源码系列-手写数据库连接池相关的知识,希望对你有一定的参考价值。

文章目录

1.目标

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

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);
        以上是关于Java源码系列-手写数据库连接池的主要内容,如果未能解决你的问题,请参考以下文章

java毕业设计项目视频源码,源码+原理+手写框架

追光者系列主流Java数据库连接池比较及前瞻

Go组件学习——手写连接池并没有那么简单

tomcat连接池验证间隔

从零手写一个JAVA连接池组件,窥视架构团队的开发日常

手写数据库连接池实战