文章目录
1.目标
Java源码系列-手写数据库连接池(附源码)
为了理解数据库连接池的底层原理,我们可以自己手写一个类似Hikari
,Druid
一样的高性能的数据库连接池!
源码在教学课程的附件中下载: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.第一代数据库连接池
其中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);
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