前景回顾

第一节 从零开始手写 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");
}

开源地址

所有源码均已开源:

jdbc-pool

使用方式和常见的连接池一样。

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 从零实现数据库连接池的更多相关文章

  1. 手写MyBatis ORM框架实践

    一.实现手写Mybatis三个难点 1.接口既然不能被实例化?那么我们是怎么实现能够调用的? 2.参数如何和sql绑定 3.返回结果 下面是Mybatis接口 二.Demo实现 1.创建Maven工程 ...

  2. 要想精通Mybatis?从手写Mybatis框架开始吧!

    1.Mybatis组成 动态SQL Config配置 Mapper配置 2.核心源码分析 Configuration源码解析 SqlSessionFactory源码解析 SqlSession源码解析 ...

  3. 手写mybatis框架笔记

    MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...

  4. 手写MyBatis流程

    MyBatis 手写MyBatis流程 架构流程图 封装数据 封装到Configuration中 1.封装全局配置文件,包含数据库连接信息和mappers信息 2.封装*mapper.xml映射文件 ...

  5. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  6. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  7. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  8. java 从零开始手写 RPC (07)-timeout 超时处理

    <过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...

  9. 《手写Mybatis》第5章:数据源的解析、创建和使用

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 管你吃几碗粉,有流量就行! 现在我们每天所接收的信息量越来越多,但很多的个人却没有多 ...

  10. 手写mybatis框架-增加缓存&事务功能

    前言 在学习mybatis源码之余,自己完成了一个简单的ORM框架.已完成基本SQL的执行和对象关系映射.本周在此基础上,又加入了缓存和事务功能.所有代码都没有copy,如果也对此感兴趣,请赏个Sta ...

随机推荐

  1. Java开发者的Golang进修指南:从0->1带你实现协程池

    在Java编程中,为了降低开销和优化程序的效率,我们常常使用线程池来管理线程的创建和销毁,并尽量复用已创建的对象.这样做不仅可以提高程序的运行效率,还能减少垃圾回收器对对象的回收次数. 在Golang ...

  2. 通过宿主机查看K8S或者是容器内的Java程序的简单方法

    通过宿主机查看K8S或者是容器内的Java程序的简单方法 背景 最近一个项目的环境出现了 cannot create native process 的错误提示 出现这个错误提示时, docker ex ...

  3. Redis和Springboot在Windows上面设置开机启动的方法

    Redis和Springboot在Windows上面设置开机启动的方法 背景 同事遇到一个问题 Windows 晚上自动更新服务 然后第二天 Springboot开发的程序没有启动起来. 所以基于此想 ...

  4. [转帖]优化命令之sar——最牛命令

    目录 一:sar命令概述 1.1sar概述 1.2sar常用选项 1.3常用参数 二:CPU资源监控 2.1整体CPU使用统计(-u) 2.2各个CPU使用统计(-P) 2.3将CPU使用情况保存到文 ...

  5. [转帖]vSphere虚拟化平台(vCenter和ESXi)升级注意事项

    https://www.dinghui.org/vmware-vsphere-upgrade.html 最近两年做了蛮多vSphere升级项目,几点思路,做一下汇总整理如下供参考: 一.升级必要性 1 ...

  6. [转帖]sendfile“零拷贝”、mmap内存映射、DMA

    https://www.jianshu.com/p/7863667d5fa7 KAFKA推送消息用到了sendfile,落盘技术用到了mmap,DMA贯穿其中. 先说说零拷贝 零拷贝并不是不需要拷贝, ...

  7. [转帖]NGINX 局限太多,Cloudflare 最终放弃它并用 Rust 自研了全新替代品

    https://www.infoq.cn/news/s2fa603MsEENsCmibTYI 长期以来,NGINX 可以说是网站安全和托管服务提供商 Cloudflare 的核心,是其所使用的基础软件 ...

  8. [转帖]CentOS7完美升级gcc版本方法

    https://blog.whsir.com/post-4975.html 在某些应用场景中,需要特定的gcc版本支持,但是轻易不要去编译gcc.不要去编译gcc.不要去编译gcc,我这里推荐使用红帽 ...

  9. buildkit ctr 与 k3s的简单学习

    摘要 前面一部分学习了 buildkit的简单搭建 也学习会了如果build images的简单处理 但是搭建镜像只是万里长征第一步. 如何进行微服务部署,才是关键的第二步. 公司最近使用基于K3S的 ...

  10. 玩一玩 golang 1.21 的 pgo 编译优化

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 1.下载镜像 暂时不想替换本机的 golang 版本,于是 ...