一、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源碼的時候再慢慢看。