Java源码系列-手写数据库连接池
Posted IT-老牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java源码系列-手写数据库连接池相关的知识,希望对你有一定的参考价值。
文章目录
1.目标
为了理解数据库连接池的底层原理,我们可以自己手写一个类似Hikari
,Druid
一样的高性能的数据库连接池!
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.第一代数据库连接池
其中C3p0
、DBCP
、Proxool
和BoneCP
都已经很久没更新了,TJP(Tomcat JDBC Pool)
,Druid
,HikariCP
则仍处于活跃的更新中。
第一代数据库连接池性能:
2.5.站在巨人肩膀上的第二代连接池
功能全面的Druid
- 提供性能卓越的连接池功能
- 还集成了
sql
监控,黑名单拦截等功能,用它自己的话说,druid
是“为监控而生” druid
另一个比较大的优势,就是中文文档比较全面(毕竟是国人的项目么)在github
的wiki
页面
性能无敌的HikariCP
- 字节码精简:优化代码,直到编译后的字节码最少,这样,
CPU
缓存可以加载更多的程序代码 - 优化代理和拦截器:减少代码,例如
HikariCP
的Statement 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源码系列-手写数据库连接池的主要内容,如果未能解决你的问题,请参考以下文章