【系列目录】
Mybatis源码阅读之一——工厂模式与SqlSessionFactory
Mybatis源码阅读之二——模板方法模式与Executor
Mybatis源码阅读之三——JDBC解析与Mybatis封装
Mybatis源码阅读之四——装饰器模式与Mybatis中的各种Cache
Mybatis源码阅读之六——数据库连接池实现与hikariCP简析
【本文目录】
前文 Mybatis源码阅读之五——Java的XML解析我们介绍了Mybatis如何解析XML,本文来看一下它自身的数据源连接池实现,以及与其他热门连接池的比较。
数据库连接池
池化技术在JAVA中有很多应用,比如典型的线程池,ThreadPoolExecutor是必知必会。
数据库连接池也是一样的,Mybatis每次发起一个请求都需要与数据库服务器建立一个新的网络连接,这样的消耗显然不合理,于是数据库连接池应运而生。
- 数据库连接池当需要使用连接时,从连接池中获取,当使用完连接后,连接不会直接销毁,而是放回连接池。
- 优势:
- 连接复用
- 连接总数量可控
如何实现一个数据库连接池
在之前Mybatis源码阅读之三——JDBC解析与Mybatis封装中我们已经介绍了JDBC以及数据库连接Connection。我们要做的,就是将Connection封装成一个池。
- 第一步,定义PoolConnectionFactory,一个连接的获取/归还方法,用以替代java.sql.DriveManager。
import java.sql.Connection;
public interface PoolConnectionFactory{
// 获取连接
Connection getConnection() throws SQLException;
// 归还连接
void reserveConnection(Connection conn);
}
- 第二步,定义池结构,简单一些,直接用一个ArrayList
- 一个空闲连接池用于存放没有被使用的连接
- 一个活跃连接池用于存放正在被使用的连接
- 一个用于控制连接池大小的size
- 额外的,还可以定义连接池满的拒绝策略,排队阻塞队列等。
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
public class PoolConnectionFactoryImpl implements PoolConnectionFactory {
final private List<Connection> idleList = new ArrayList<>();
final private List<Connection> activeList = new ArrayList<>();
private int MAX_POOL_SIZE = 0;
public PoolConnectionFactoryImpl(int MAX_POOL_SIZE) {
this.MAX_POOL_SIZE = MAX_POOL_SIZE;
}
@Override
public Connection getConnection() {
// TODO
return null;
}
@Override
public void reserveConnection(Connection conn) {
// TODO
return null;
}
}
- 第三步,实现连接的获取方法getConnection,归还连接的reserveConnection。简单展开下getCnnection。
- 上锁,连接的获取/归还是并发操作需要上锁保证逻辑正确,
这里有更好的实现方法,见下文hikariCP
- 如果空闲连接池还有连接,直接使用,否则下一步。
- 如果活跃连接池+空闲连接池的size没有达到最大限度,那么生成新的连接。
- 如果已经达到最大限度,那么拒绝(或设计其他拒绝策略,如kill掉耗时最久的连接,将连接给本次请求使用)
- 上锁,连接的获取/归还是并发操作需要上锁保证逻辑正确,
@Override
public Connection getConnection() throws SQLException {
Connection conn = null;
synchronized (this){
if(!idleList.isEmpty()){
conn = idleList.get(0);
idleList.remove(conn);
activeList.add(conn);
}else{
if(activeList.size()+idleList.size()<MAX_POOL_SIZE){
conn = DriverManager.getConnection("mock", "mock", "mock");
activeList.add(conn);
} else{
// TODO 可以设计拒绝策略
throw new RejectedExecutionException("连接池繁忙,无连接可使用");
}
}
}
return conn;
}
@Override
public void reserveConnection(Connection conn) {
synchronized (this){
idleList.add(conn);
activeList.remove(conn);
}
}
如上只是一个demo级别的实现,不建议生产使用,那么让我们看一下,Mybatis官方以及其他优秀的数据库连接池框架是怎么实现的。
Mybatis连接池实现
非池化与池化
- 包结构,我们关注pooled(池化连接管理),unpooled(非池化连接管理)
- DataSourceFactory用于定义获取数据源的接口
工厂的具体实现类只是通过配置获取到url等信息。
- DataSource接口是jdbc包下。
- unpooled (主要逻辑在UnpooledDataSource.getConnection)
最终,也是通过DriverManager获取连接的,没有额外封装处理。
- pooled,重点来了——PooledDataSource变量定义,值得关注的一个是UnpooledDataSource,即池化的数据源只是在非池化上面做了封装。另外一点,PoolState实际上就是连接池,囊括了基本的空闲池与活跃池。
- PooledDataSource.getConnection()
- 第一步,死循环获取连接。对池上锁,保证并发正确性。
- 第二步,若空闲池有存量则使用,否则第三步。
- 第三步,若活跃池未超过
活跃池最大限制
,则新建一个连接,否则第四步。 - 第四步,
拒绝策略
,如果活跃池中耗时最久的连接超过了最大任务执行时间
,则会被停止回滚,并将连接拿过来使用。 - 第五步,时间最长的连接也没有超时,那么新任务只能等待。
扩展
Mybatis虽然自己实现了一套连接池,但是秉持着开源社区的理念,它提供了数据源连接池的扩展方式,具体执行的地方正是在我们前文Mybatis源码阅读之五——Java的XML解析提到的mybatis-config解析过程中。
也即,如果我们想要让mybatis使用我们自定义的连接池,那么就需要实现一个DataSourceFactory/DataSource,同时设置一下mybatis的datasource.type配置。
其他连接池比较
Mybatis实现的连接池,最火热的有HikariCP连接池,Durid连接池,这两个连接池,一个是以快为闻名,另一个则是以监控出名。
HikariCP 的hikari,取自日语中“光”的翻译,一个字,快。
Durid的官方描述则是:阿里云计算平台DataWorks团队出品,为监控而生的数据库连接池。
监控的功能实际上其他开源组建也可以实现,所以对比而言,我个人还是对HikariCP更感兴趣。
HikariCP
为什么说HikariCP快?它比一般的数据库连接池强在哪里?
- 字节码级别优化,很多方法通过JavaAssist生成。(JavaAssist字节码生成技术相较于JDK和CGLIB,没有去继承/包装一些额外的类,精简了许多)
- 使用FastList代替ArrayList,以及使用CopyOnWriteArrayList等大量并发方面等优化
- 使用ConcurrentBag的设计
ConcurrentBag
其中,第三点与连接池的设计有关,我们简单了解下它的实现。
在一个完善的数据库连接池中,我们需要在四个方法考虑并发问题:
- add,添加一个连接。
- remove,移除一个连接。
- get/borrow,获取一个连接。
- return/reserve,归还一个连接。
在我们之前的设计中,会存在一个活跃池和一个空闲池,用于区分这个连接是否被使用,这样就导致当get/return一个连接时,需要对这两个池同时上锁才能完成,而get/return又是高频方法,如果能够避免在此时进行锁操作,性能就一定能得到提升。
hikariCP就是这么做的,它使用了无锁集合ConcurrentBag。
ConcurrentBag没有区分活跃池和空闲池,而是一个统一的集合,区分是否被使用则是通过一个标识,有同学可能有疑问了,并发情况下,不还是可能会同时被多个线程拿到一个连接吗?
怎么解决?CAS操作标识位。
篇幅有限,后面抽时间再来专门分析一下hikariCP的源码。
欢迎关注微信公众号 【JAVA技术分享官】,公众号首发,持续输出原创高质量JAVA开发者知识点