一、SpringBoot2.x 默认连接池 HikariCP简单配置
现在国内用的最多的数据库连接池无疑是druid,因为它的监控功能实在太好用了,另外性能、稳定性、社区活跃度等各方面几乎没啥大的缺点。我们公司自然也是用的druid,这导致我一直没意识到springboot默认的连接池的存在。直到今天,我新建了一个springboot项目,导入jpa和web依赖包,配置好mysql地址,发现数据库连接竟然失败了。
我发现日志里有打印
MyHikariCP - Starting...
然后嘛,就是在启动这个连接池的过程中报错了。带着好奇心百度了下这个是啥玩意儿,一查才知道,这个是springboot2.x默认的数据库连接池,而springboot1.x默认的是tomcat连接池。
于是我在网上找了份HikariCP的标准配置文件,只需改下数据库名和账号密码就能正常使用了。
## 数据库配置 spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.driverClassName = com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username = root spring.datasource.password = root ## Hikari 连接池配置 ------ 详细配置请访问:https://github.com/brettwooldridge/HikariCP ## 最小空闲连接数量 spring.datasource.hikari.minimum-idle=5 ## 空闲连接存活最大时间,默认600000(10分钟) spring.datasource.hikari.idle-timeout=180000 ## 连接池最大连接数,默认是10 spring.datasource.hikari.maximum-pool-size=10 ## 此属性控制从池返回的连接的默认自动提交行为,默认值:true spring.datasource.hikari.auto-commit=true ## 连接池母子 spring.datasource.hikari.pool-name=MyHikariCP ## 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 spring.datasource.hikari.max-lifetime=1800000 ## 数据库连接超时时间,默认30秒,即30000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=SELECT 1
二、HikariCP源码分析
1. HikariPool和PoolEntry
HikariPool有三个内部类,其中一个是自定义的运行时异常,暂且不谈。另两个分别是HouseKeeper和PoolEntryCreater。HouseKeeper是一个Runnable线程,PoolEntryCreator是一个Callable执行单元。Hikari在自己的构造方法里使用了这两个线程类。
public HikariPool(){ this.poolEntryCreator = new HikariPool.PoolEntryCreator((String)null); this.houseKeepingExecutorService = this.initializeHouseKeepingExecutorService(); this.houseKeeperTask = this.houseKeepingExecutorService.scheduleWithFixedDelay(new HikariPool.HouseKeeper(), 100L, this.housekeepingPeriodMs, TimeUnit.MILLISECONDS); }
先看下HikariPool的getConnection方法,里面调用了poolEntry.createProxyConnection(),poolEntry是poolEntryCreator创造出来放到ConcurrentBag<PoolEntry>里的,这里取出来。
再来看createProxyConnection
Connection createProxyConnection(ProxyLeakTask leakTask, long now) { return ProxyFactory.getProxyConnection(this, this.connection, this.openStatements, leakTask, now, this.isReadOnly, this.isAutoCommit);
}
终于走到ProxyFactory这里,具体看下面有关hikari工厂和代理的讲解。
2. ProxyFactory
代码简化下大致是这样:
public final class ProxyFactory{ static ProxyConnection getProxyConnection(){return new HikariProxyConnection()};
static Statement getProxyStatement(){return new HikariProxyStatement()};
static CallableStatement getProxyCallableStatement(){return new HikariProxyCallableStatement()};
static PreparedStatement getProxyPreparedStatement(){return new HikariProxyPreparedStatement()};
static ResultSet getProxyResultSet(){return new HikariProxyResultSet()};
static DatabaseMetaData getProxyDatabaseMetaData(){return new HikariProxyDatabaseMetaData()};
}
因为只有一个具体工厂类,里面的工厂方法也都是静态的,所以这是一个典型的简单工厂模式。
ProxyFactory生产的是Hikari有关数据库操作相关的代理类,被代理的类是java.sql下面数据库操作的原生类。
3. hikari中的动态代理
我们用ProxyConnection这个代理类来举例,这里只贴出部分代码
public abstract class ProxyConnection implements Connection { protected ProxyConnection(Connection connection) { this.delegate = connection; } }
可以看到ProxyConnection实现了共用主题接口Connection,并且通过构造方法持有另一个Connection实际对象的引用。这里另一个Connection实际对方就是被代理的类。
这里使用了JavassistProxyFaxctory动态代理,是预先加载字节码的,涉及到类加载机制,我还没看懂,以后慢慢看。
4. fastlist对比arraylist
总结:因为fastlist仅供内部使用,所以减少了很多安全校验。另外fastlist因为确认马上就会使用,直接初始化长度32的数组,扩容也改成2倍再扩容,减少了扩容次数。
这位大佬讲的很全面,从构造函数、扩容、插入、删除、取值、迭代器各方面做了对比。
读HikariCP源码学Java(二)—— 因地制宜的改装版ArrayList:FastList - 缪若尘 - 博客园 (cnblogs.com)
5. ConcurrentBag
这个是借鉴了c#中的设计,主要有如下特点
- A lock-free design
- ThreadLocal caching
- Queue-stealing
- Direct hand-off optimizations
最后,推荐下这个大佬写的追光者系列Hikari三连,感觉写得很好,我自己也还没看完,mark一下有空再看。
【追光者系列】HikariCP源码分析之字节码修改类库Javassist委托实现动态代理 - 云+社区 - 腾讯云 (tencent.com)
三、简单看下druid源码
大概了看了下druid没有类似hikari里那么明显的pool类,它存放连接的容器叫DruidConnectionHolder。druid核心代码主要在DuidDataSource,这个类是简单工厂DruidDataSourceFactory的产品。那么着重看下DruidDataSource,它的父类实现了java.sql.DataSource接口,所以它本质也是DataSource的一个实现类。
结合druid使用时的配置来看,会更好理解一点
initialSize: 5
minIdle: 5 # 初始化大小,最小,最大 maxActive: 20 maxWait: 60000 # 配置获取连接等待超时的时间 timeBetweenEvictionRunsMillis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 maxPoolPreparedStatementPerConnectionSiz: 20 filters: stat,wall,log4j connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
1. 先看DruidDataSource的构造方法
默认是非公平锁(效率高),也可以使用有参构造函数传入false设置为公平锁。构造函数中创建计算器值为2的全局CountDownLatch。
2. 然后看DruidDataSource对外提供的入口函数getConnection(long maxWaitMillis)
这个参数表示连接不使用后最大等待时间。函数内部首先调用init()函数进行了DruidDataSource的初始化
1)异步初始化
init中判断如果asyncInit为true,也就是异步初始化,则根据使用for循环去创建initialSize数量的CreateConnectionTask线程,并将线程提交到Future中去运行。
2)同步初始化
如果asyncInit为false,则使用while语句判断poolingCount是否小于initialSize,如果小于,则直接调用父类的createPhysicalConnection()创建一个连接,并放入holder容器。
3)创建守护线程
紧接着init()函数会创建LogStatsThread、CreateConnectionThread、DestroyConnectionThread这三个守护线程,其中LogStatsThread不会使用CountdownLatch。这时候调用全局CountDownLatch的await函数,等待上面几个守护进程创建的核心连接创建完成。
3. DruidDataSource有5个内部类
LogStatsThread 继承Thread类,设置为守护线程,功能是打印日志。
CreateConnectionThread 继承Thread类,设置为守护线程。在run()方法的第一步会调用countDown(),只会执行一次。下面会在while死循环中判断是否需要创建新连接,如果满足条件会调用createPhysicalConnection()。
DestroyConnectionThread 继承Thread类,设置为守护线程。在run()方法的第一步会调用countDown(),只会执行一次。下面会在while死循环中判断是否需要销毁连接,如果满足条件,调用destoryTask的run()方法。
CreateConnectionTask 实现Runnable接口,核心方法runInternal() ,如果没有等待使用的闲置连接(这个判断过程用ReetrantLock保证同步安全),则创建连接。创建连接的方法createPhysicalConnection()是DruidDataSource的父类中定义的方法。另外这里面有一个fastfail快速失败机制以后有空再研究。
DestroyTask 实现Runnable接口,run方法中移除超时的连接。
4. DruidConnectionHolder
5. druid中的过滤器链
用来实现监控、日志、防sql注入,分别对应参数 filters: stat,log4j,wall
6. DruidDataSourceC3P0Adapter
这个适配器用来适配c3p0的连接,具体还没细看。hikari中貌似没看到这种适配设计。
7. ConnectionProxyImpl
这个是druid中的代理类,被代理的是connection各个开发商具体的实现类。这个我是看其它源码分析文章看到的,先记录在这儿,后面继续深入看druid源码的时候再慢慢看。