从零开始手写 mybatis (三)jdbc pool 从零实现数据库连接池
前景回顾
第一节 从零开始手写 mybatis(一)MVP 版本 中我们实现了一个最基本的可以运行的 mybatis。
第二节 从零开始手写 mybatis(二)mybatis interceptor 插件机制详解
本节我们一起来看一下如何实现一个数据库连接池。
为什么需要连接池?
数据库连接的创建是非常耗时的一个操作,在高并发的场景,如果每次对于数据库的访问都重新创建的话,成本太高。
于是就有了“池化”这种解决方案。
这种方案在我们日常生活中也是比比皆是,比如资金池,需求池,乃至人力资源池。
思想都是共通的。
我们本节一起来从零实现一个简易版本的数据库连接池,不过麻雀虽小,五脏俱全。
将从以下几个方面来展开:
(1)普通的数据库连接创建
(2)自动适配 jdbc 驱动
(3)指定大小的连接池创建
(4)获取连接时添加超时检测
(5)添加对于连接有效性的检测
普通的数据库连接创建
这种就是最普通的不适用池化的实现。
实现
mybatis 默认其实也是这种实现,不过我们在这个基础上做了一点优化,那就是可以根据 url 自动适配 driverClass。
public class UnPooledDataSource extends AbstractDataSourceConfig {
@Override
public Connection getConnection() throws SQLException {
DriverClassUtil.loadDriverClass(super.driverClass, super.jdbcUrl);
return DriverManager.getConnection(super.getJdbcUrl(),
super.getUser(), super.getPassword());
}
}
自动适配
这个特性主要是参考阿里的 druid 连接池实现,在用户没有指定驱动类时,自动适配。
核心代码如下:
/**
* 加载驱动类信息
* @param driverClass 驱动类
* @param url 连接信息
* @since 1.2.0
*/
public static void loadDriverClass(String driverClass, final String url) {
ArgUtil.notEmpty(url, url);
if(StringUtil.isEmptyTrim(driverClass)) {
driverClass = getDriverClassByUrl(url);
}
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
throw new JdbcPoolException(e);
}
}
如何根据 url 获取启动类呢?实际上就是一个 map 映射。
/**
* 根据 URL 获取对应的驱动类
*
* 1. 禁止 url 为空
* 2. 如果未找到,则直接报错。
* @param url url
* @return 驱动信息
*/
private static String getDriverClassByUrl(final String url) {
ArgUtil.notEmpty(url, "url");
for(Map.Entry<String, String> entry : DRIVER_CLASS_MAP.entrySet()) {
String urlPrefix = entry.getKey();
if(url.startsWith(urlPrefix)) {
return entry.getValue();
}
}
throw new JdbcPoolException("Can't auto find match driver class for url: " + url);
}
其中 DRIVER_CLASS_MAP 映射如下:
url 前缀 | 驱动类 |
---|---|
jdbc:sqlite | org.sqlite.JDBC |
jdbc:derby | org.apache.derby.jdbc.EmbeddedDriver |
jdbc:edbc | ca.edbc.jdbc.EdbcDriver |
jdbc:ingres | com.ingres.jdbc.IngresDriver |
jdbc:hsqldb | org.hsqldb.jdbcDriver |
jdbc:JSQLConnect | com.jnetdirect.jsql.JSQLDriver |
jdbc:sybase:Tds | com.sybase.jdbc2.jdbc.SybDriver |
jdbc:firebirdsql | org.firebirdsql.jdbc.FBDriver |
jdbc:microsoft | com.microsoft.jdbc.sqlserver.SQLServerDriver |
jdbc:mckoi | com.mckoi.JDBCDriver |
jdbc:oracle | oracle.jdbc.driver.OracleDriver |
jdbc:as400 | com.ibm.as400.access.AS400JDBCDriver |
jdbc:fake | com.alibaba.druid.mock.MockDriver |
jdbc:pointbase | com.pointbase.jdbc.jdbcUniversalDriver |
jdbc:sapdb | com.sap.dbtech.jdbc.DriverSapDB |
jdbc:postgresql | org.postgresql.Driver |
jdbc:cloudscape | COM.cloudscape.core.JDBCDriver |
jdbc:timesten | com.timesten.jdbc.TimesTenDriver |
jdbc:h2 | org.h2.Driver |
jdbc:jtds | net.sourceforge.jtds.jdbc.Driver |
jdbc:odps | com.aliyun.odps.jdbc.OdpsDriver |
jdbc:db2 | COM.ibm.db2.jdbc.app.DB2Driver |
jdbc:mysql | com.mysql.jdbc.Driver |
jdbc:informix-sqli | com.informix.jdbc.IfxDriver |
jdbc:mock | com.alibaba.druid.mock.MockDriver |
jdbc:mimer:multi1 | com.mimer.jdbc.Driver |
jdbc:interbase | interbase.interclient.Driver |
jdbc:JTurbo | com.newatlanta.jturbo.driver.Driver |
池化实现
接下来我们根据指定的大小创建一个初始化的连接池。
定义池化的相关信息
我们首先定义一个接口:
/**
* 池化的连接池
* @since 1.1.0
*/
public interface IPooledConnection extends Connection {
/**
* 是否繁忙
* @since 1.1.0
* @return 状态
*/
boolean isBusy();
/**
* 设置状态
* @param busy 状态
* @since 1.1.0
*/
void setBusy(boolean busy);
/**
* 获取真正的连接
* @return 连接
* @since 1.1.0
*/
Connection getConnection();
/**
* 设置连接信息
* @param connection 连接信息
* @since 1.1.0
*/
void setConnection(Connection connection);
/**
* 设置对应的数据源
* @param dataSource 数据源
* @since 1.5.0
*/
void setDataSource(final IPooledDataSourceConfig dataSource);
/**
* 获取对应的数据源信息
* @return 数据源
* @since 1.5.0
*/
IPooledDataSourceConfig getDataSource();
}
这里我们直接继承了 Connection 接口,实现时全部对 Connection 做一个代理。
内容较多,但是比较简单,此处不再赘述。
连接池初始化
根据配置初始化大小:
/**
* 初始化连接池
* @since 1.1.0
*/
private void initJdbcPool() {
final int minSize = super.minSize;
pool = new ArrayList<>(minSize);
for(int i = 0; i < minSize; i++) {
IPooledConnection pooledConnection = createPooledConnection();
pool.add(pooledConnection);
}
}
createPooledConnection 内容如下:
/**
* 创建一个池化的连接
* @return 连接
* @since 1.1.0
*/
private IPooledConnection createPooledConnection() {
Connection connection = createConnection();
IPooledConnection pooledConnection = new PooledConnection();
pooledConnection.setBusy(false);
pooledConnection.setConnection(connection);
pooledConnection.setDataSource(this);
return pooledConnection;
}
我们使用 busy 属性,来标识当前连接是否可用。
新创建的连接默认都是可用的。
连接的获取
整体流程如下:
(1)池中有连接,直接获取
(2)池中没有连接,且没达到最大的大小,可以创建一个,然后返回
(3)池中没有连接,但是已经达到最大,则进行等待。
@Override
public synchronized Connection getConnection() throws SQLException {
//1. 获取第一个不是 busy 的连接
Optional<IPooledConnection> connectionOptional = getFreeConnectionFromPool();
if(connectionOptional.isPresent()) {
return connectionOptional.get();
}
//2. 考虑是否可以扩容
if(pool.size() >= maxSize) {
//2.1 立刻返回
if(maxWaitMills <= 0) {
throw new JdbcPoolException("Can't get connection from pool!");
}
//2.2 循环等待
final long startWaitMills = System.currentTimeMillis();
final long endWaitMills = startWaitMills + maxWaitMills;
while (System.currentTimeMillis() < endWaitMills) {
Optional<IPooledConnection> optional = getFreeConnectionFromPool();
if(optional.isPresent()) {
return optional.get();
}
DateUtil.sleep(1);
LOG.debug("等待连接池归还,wait for 1 mills");
}
//2.3 等待超时
throw new JdbcPoolException("Can't get connection from pool, wait time out for mills: " + maxWaitMills);
}
//3. 扩容(暂时只扩容一个)
LOG.debug("开始扩容连接池大小,step: 1");
IPooledConnection pooledConnection = createPooledConnection();
pooledConnection.setBusy(true);
this.pool.add(pooledConnection);
LOG.debug("从扩容后的连接池中获取连接");
return pooledConnection;
}
getFreeConnectionFromPool() 核心代码如下:
直接获取一个不是繁忙状态的连接即可。
/**
* 获取空闲的连接
* @return 连接
* @since 1.3.0
*/
private Optional<IPooledConnection> getFreeConnectionFromPool() {
for(IPooledConnection pc : pool) {
if(!pc.isBusy()) {
pc.setBusy(true);
LOG.debug("从连接池中获取连接");
return Optional.of(pc);
}
}
// 空
return Optional.empty();
}
连接的归还
以前 connection 的归还是直接将连接关闭,这里我们做了一个重载。
只是调整下对应的状态即可。
@Override
public void returnConnection(IPooledConnection pooledConnection) {
// 验证状态
if(testOnReturn) {
checkValid(pooledConnection);
}
// 设置为不繁忙
pooledConnection.setBusy(false);
LOG.debug("归还连接,状态设置为不繁忙");
}
连接的有效性
池中的连接存在无效的可能,所以需要我们对其进行定期的检测。
配置讲解
验证的时机是一门学问,我们可以在获取时检测,可以在归还时检测,但是二者都比较消耗性能。
比较好的方式是在空闲的时候进行校验。
配置主要参考 druid 的配置,对应的接口如下:
/**
* 设置验证查询的语句
*
* 如果这个值为空,那么 {@link #setTestOnBorrow(boolean)}
* {@link #setTestOnIdle(boolean)}}
* {@link #setTestOnReturn(boolean)}
* 都将无效
* @param validQuery 验证查询的语句
* @since 1.5.0
*/
void setValidQuery(final String validQuery);
/**
* 验证的超时秒数
* @param validTimeOutSeconds 验证的超时秒数
* @since 1.5.0
*/
void setValidTimeOutSeconds(final int validTimeOutSeconds);
/**
* 获取连接时进行校验
*
* 备注:影响性能
* @param testOnBorrow 是否
* @since 1.5.0
*/
void setTestOnBorrow(final boolean testOnBorrow);
/**
* 归还连接时进行校验
*
* 备注:影响性能
* @param testOnReturn 归还连接时进行校验
* @since 1.5.0
*/
void setTestOnReturn(final boolean testOnReturn);
/**
* 闲暇的时候进行校验
* @param testOnIdle 闲暇的时候进行校验
* @since 1.5.0
*/
void setTestOnIdle(final boolean testOnIdle);
/**
* 闲暇时进行校验的时间间隔
* @param testOnIdleIntervalSeconds 时间间隔
* @since 1.5.0
*/
void setTestOnIdleIntervalSeconds(final long testOnIdleIntervalSeconds);
约定优于配置
所有的属性都支持用户自定义,以满足不同的应用场景。
同时也秉承着默认的配置就是最常用的配置,默认的配置如下:
/**
* 默认验证查询的语句
* @since 1.5.0
*/
public static final String DEFAULT_VALID_QUERY = "select 1 from dual";
/**
* 默认的验证的超时时间
* @since 1.5.0
*/
public static final int DEFAULT_VALID_TIME_OUT_SECONDS = 5;
/**
* 获取连接时,默认不校验
* @since 1.5.0
*/
public static final boolean DEFAULT_TEST_ON_BORROW = false;
/**
* 归还连接时,默认不校验
* @since 1.5.0
*/
public static final boolean DEFAULT_TEST_ON_RETURN = false;
/**
* 默认闲暇的时候,进行校验
*
* @since 1.5.0
*/
public static final boolean DEFAULT_TEST_ON_IDLE = true;
/**
* 1min 自动校验一次
*
* @since 1.5.0
*/
public static final long DEFAULT_TEST_ON_IDLE_INTERVAL_SECONDS = 60;
检测的实现
这里我参考了一篇 statckOverflow 的文章,其实还是使用 Connection#isValid 验证比较简单。
/**
* https://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most
*
* 真正支持标准的,直接使用 {@link Connection#isValid(int)} 验证比较合适
* @param pooledConnection 连接池信息
* @since 1.5.0
*/
private void checkValid(final IPooledConnection pooledConnection) {
if(StringUtil.isNotEmpty(super.validQuery)) {
Connection connection = pooledConnection.getConnection();
try {
// 如果连接无效,重新申请一个新的替代
if(!connection.isValid(super.validTimeOutSeconds)) {
LOG.debug("Old connection is inValid, start create one for it.");
Connection newConnection = createConnection();
pooledConnection.setConnection(newConnection);
LOG.debug("Old connection is inValid, finish create one for it.");
}
} catch (SQLException throwables) {
throw new JdbcPoolException(throwables);
}
} else {
LOG.debug("valid query is empty, ignore valid.");
}
}
闲暇时的线程处理
我们为了不影响性能,单独为闲暇的连接检测开一个线程。
在初始化的创建:
/**
* 初始化空闲时检验
* @since 1.5.0
*/
private void initTestOnIdle() {
if(StringUtil.isNotEmpty(validQuery)) {
ScheduledExecutorService idleExecutor = Executors.newSingleThreadScheduledExecutor();
idleExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
testOnIdleCheck();
}
}, super.testOnIdleIntervalSeconds, testOnIdleIntervalSeconds, TimeUnit.SECONDS);
LOG.debug("Test on idle config with interval seonds: " + testOnIdleIntervalSeconds);
}
}
testOnIdleCheck 实现如下:
/**
* 验证所有的空闲连接是否有效
* @since 1.5.0
*/
private void testOnIdleCheck() {
LOG.debug("start check test on idle");
for(IPooledConnection pc : this.pool) {
if(!pc.isBusy()) {
checkValid(pc);
}
}
LOG.debug("finish check test on idle");
}
开源地址
所有源码均已开源:
使用方式和常见的连接池一样。
maven 引入
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>jdbc-pool</artifactId>
<version>1.5.0</version>
</dependency>
测试代码
PooledDataSource source = new PooledDataSource();
source.setDriverClass("com.mysql.jdbc.Driver");
source.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8");
source.setUser("root");
source.setPassword("123456");
source.setMinSize(1);
// 初始化
source.init();
Connection connection = source.getConnection();
System.out.println(connection.getCatalog());
Connection connection2 = source.getConnection();
System.out.println(connection2.getCatalog());
日志
[DEBUG] [2020-07-18 10:50:54.536] [main] [c.g.h.t.p.d.PooledDataSource.getFreeConnection] - 从连接池中获取连接
test
[DEBUG] [2020-07-18 10:50:54.537] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 开始扩容连接池大小,step: 1
[DEBUG] [2020-07-18 10:50:54.548] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 从扩容后的连接池中获取连接
test
小结
到这里,一个简单版本的连接池就已经实现了。
常见的连接池,比如 dbcp/c3p0/druid/jboss-pool/tomcat-pool 其实都是类似的。
万变不离其宗,实现只是一种思想的差异化表示而已。
但是有哪些不足呢?
性能方面,我们为了简单,都是直接使用 synchronized
保证并发安全,这样性能会相对于乐观锁,或者是无锁差一些。
自定义方面,比如 druid 可以支持用户自定义拦截器,添加注入防止 sql 注入,耗时统计等等。
页面管理,druid 比较优异的一点就是自带页面管理,这一点对于日常维护也比较友好。
从零开始手写 mybatis (三)jdbc pool 从零实现数据库连接池的更多相关文章
- 手写MyBatis ORM框架实践
一.实现手写Mybatis三个难点 1.接口既然不能被实例化?那么我们是怎么实现能够调用的? 2.参数如何和sql绑定 3.返回结果 下面是Mybatis接口 二.Demo实现 1.创建Maven工程 ...
- 要想精通Mybatis?从手写Mybatis框架开始吧!
1.Mybatis组成 动态SQL Config配置 Mapper配置 2.核心源码分析 Configuration源码解析 SqlSessionFactory源码解析 SqlSession源码解析 ...
- 手写mybatis框架笔记
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- 手写MyBatis流程
MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...
- java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端
通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...
- java 从零开始手写 RPC (03) 如何实现客户端调用服务端?
说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...
- java 从零开始手写 RPC (04) -序列化
序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...
- java 从零开始手写 RPC (07)-timeout 超时处理
<过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...
- 《手写Mybatis》第5章:数据源的解析、创建和使用
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 管你吃几碗粉,有流量就行! 现在我们每天所接收的信息量越来越多,但很多的个人却没有多 ...
- 手写mybatis框架-增加缓存&事务功能
前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...
随机推荐
- Java开发者的Golang进修指南:从0->1带你实现协程池
在Java编程中,为了降低开销和优化程序的效率,我们常常使用线程池来管理线程的创建和销毁,并尽量复用已创建的对象.这样做不仅可以提高程序的运行效率,还能减少垃圾回收器对对象的回收次数. 在Golang ...
- 通过宿主机查看K8S或者是容器内的Java程序的简单方法
通过宿主机查看K8S或者是容器内的Java程序的简单方法 背景 最近一个项目的环境出现了 cannot create native process 的错误提示 出现这个错误提示时, docker ex ...
- Redis和Springboot在Windows上面设置开机启动的方法
Redis和Springboot在Windows上面设置开机启动的方法 背景 同事遇到一个问题 Windows 晚上自动更新服务 然后第二天 Springboot开发的程序没有启动起来. 所以基于此想 ...
- [转帖]优化命令之sar——最牛命令
目录 一:sar命令概述 1.1sar概述 1.2sar常用选项 1.3常用参数 二:CPU资源监控 2.1整体CPU使用统计(-u) 2.2各个CPU使用统计(-P) 2.3将CPU使用情况保存到文 ...
- [转帖]vSphere虚拟化平台(vCenter和ESXi)升级注意事项
https://www.dinghui.org/vmware-vsphere-upgrade.html 最近两年做了蛮多vSphere升级项目,几点思路,做一下汇总整理如下供参考: 一.升级必要性 1 ...
- [转帖]sendfile“零拷贝”、mmap内存映射、DMA
https://www.jianshu.com/p/7863667d5fa7 KAFKA推送消息用到了sendfile,落盘技术用到了mmap,DMA贯穿其中. 先说说零拷贝 零拷贝并不是不需要拷贝, ...
- [转帖]NGINX 局限太多,Cloudflare 最终放弃它并用 Rust 自研了全新替代品
https://www.infoq.cn/news/s2fa603MsEENsCmibTYI 长期以来,NGINX 可以说是网站安全和托管服务提供商 Cloudflare 的核心,是其所使用的基础软件 ...
- [转帖]CentOS7完美升级gcc版本方法
https://blog.whsir.com/post-4975.html 在某些应用场景中,需要特定的gcc版本支持,但是轻易不要去编译gcc.不要去编译gcc.不要去编译gcc,我这里推荐使用红帽 ...
- buildkit ctr 与 k3s的简单学习
摘要 前面一部分学习了 buildkit的简单搭建 也学习会了如果build images的简单处理 但是搭建镜像只是万里长征第一步. 如何进行微服务部署,才是关键的第二步. 公司最近使用基于K3S的 ...
- 玩一玩 golang 1.21 的 pgo 编译优化
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 1.下载镜像 暂时不想替换本机的 golang 版本,于是 ...