一、引子
合理配置一個應用的數據庫參數,使其運行良好,這很重要。本文以某務中台的生產環境為例,從Apollo上拔下來一套配置,分析是否合理。
二、MybatisPlus配置
由於我們使用Apollo配置參數,所以分兩部分:1.個體配置 2.全局配置
2.1 mybatisplus個體配置
mybatis-plus.mapper-locations = classpath*:/mapper/*Mapper.xml mapper文件地址匹配
mybatis-plus.type-aliases-package =xx.po 映射的實體包路徑,
mybatis-plus.tenant-config.ignoretable = table1,table2
mybatis-plus.auth-config = []
mybatis-plus.global-config.sql-parser-cache = true 緩存sql解析
2.2 mybatis-plus全局配置
mybatis-plus.mapper-locations = classpath:/mapper/*Mapper.xml mapper文件地址匹配
mybatis-plus.configuration.map-underscore-to-camel-case = true 下划線轉駝峰
mybatis-plus.global-config.logic-delete-value = true 邏輯已刪除值
mybatis-plus.global-config.logic-not-delete-value = false 邏輯未刪除值
mybatis-plus.max-query-records-size = 10000
三、Datasource配置
3.1 dataSource個體配置
=========數據庫配置=========
spring.datasource.connectionProperties = druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄5秒
spring.datasource.type = com.alibaba.druid.pool.DruidDataSource 使用德魯伊連接池
spring.datasource.driver-class-name = org.postgresql.Driver 驅動類名
spring.datasource.url = xx 數據庫連接url
spring.datasource.username = ${dbUserName} 用戶名
spring.datasource.password = ${dbPassword} 密碼
spring.datasource.minIdle = 5 最小空閑連接數 5
spring.datasource.initialSize = 5 初始連接數 5
spring.datasource.maxActive = 100 最大連接數
spring.datasource.maxWait = 60000 獲取連接等待超時的時間 60s=1分鍾
spring.datasource.filters = stat,wall 監控統計攔截,用於監控界面sql統計
spring.datasource.poolPreparedStatements = false 是否啟用緩存PreparedStatements
spring.datasource.maxPoolPreparedStatementPerConnectionSize = 20 指定每個連接上preStatement緩存數---》未生效!!!
=========健康檢查=========
spring.datasource.validationQuery = SELECT 1 連接池的健康檢查SQL
spring.datasource.testOnBorrow = false 申請連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能。
spring.datasource.testOnReturn = false 歸還連接時執行validationQuery檢測連接是否有效,做了這個配置會降低性能
spring.datasource.testWhileIdle = true 建議配置為true,不影響性能,並且保證安全性。申請連接的時候檢測,如果空閑時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
#每timeBetweenEvictionRunsMillis毫秒檢查一次連接池中空閑的連接,把空閑時間超過minEvictableIdleTimeMillis毫秒的連接斷開,直到連接池中的連接數到minIdle為止
spring.datasource.minEvictableIdleTimeMillis = 300000 最小可驅逐空閑時間,連接保持空閑而不被驅逐的最長時間,單位是毫秒 300s=5分鍾
spring.datasource.timeBetweenEvictionRunsMillis = 60000 間隔多久才進行一次驅逐檢測,單位是毫秒 60s=1分鍾
=========連接超時=========
# 關閉abanded連接時輸出錯誤日志,預生產/生產不建議開啟,對性能影響
spring.datasource.logAbandoned = false
# 是否清除已經超過“removeAbandonedTimout”設置的無效連接。
spring.datasource.removeAbandoned = true
# 連接超過指定時間未關閉,就會被強行回收 180s=3分鍾
spring.datasource.removeAbandonedTimeoutMillis = 180000
四、源碼剖析
看完配置,大家心里還是懵逼對吧,參數如何生效,druid到底如何運行?
下面,帶着問題,深入源碼,直接剖析druid如何申請連接、釋放連接、連接泄露檢查。
4.1.申請連接
最終跟進到DruidDataSource的getConnectionDirect(long maxWaitMillis),獲取得到連接后,validationQuery有效性檢查,源碼如下:

1.testOnBorrow =true,先直接校驗,執行validationQuery,失敗就關閉連接JdbcUtils.close(realConnection);
2.testWhileIdle=true,如果testOnBorrow =false, 測試空閑的連接,執行validationQuery,失敗就關閉連接JdbcUtils.close(realConnection);
3.removeAbandoned=true,如果開啟了泄露回收:把連接添加進Map<DruidPooledConnection, Object> activeConnections 。供泄露回收時使用。
分支1和2只會有一個執行。
4.2.釋放連接
德魯伊連接池在獲取連接時,會調用一次DruidDataSource的init()。方法中createAndStartDestroyThread()開啟了一個銷毀線程。

銷毀連接的線程包含了run(),如下:

在一個for空條件循環中,根據配置的timeBetweenEvictionRunsMillis連接檢測間隔時間,執行一次DestroyTask.run()就休眠一次間隔時間。未設置默認60s。(實際源碼中定義了60spublic static final long DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS = 60 * 1000L;,所以用戶未設置,默認60s,上圖中else分支sleep1秒不會執行到)
追蹤DestroyTask.run()如下:

2個步驟:
- shrink()收縮校驗
- removeAbandoned()連接泄露移除
shrink()收縮校驗
DruidDataSource內部定義了DruidConnectionHolder[] 類型的3個數組:
- 1.connections:可用連接數組。申請連接就從這里數組隊尾拿連接。
- 2.evictConnections:待移除連接數組。
- 3.keepAliveConnections:待保活檢測數組。
塞進數組
shrink()中計算出需要校驗的數量checkCount,執行收縮校驗核心邏輯:
- 校驗物理連接的超時時間phyTimoutMills:超時放入evictConnections中,等待移除。
- 空余時間大於minEvictableIdleTimeMillis(受保最小空閑時間),並且索引(poolingCount)小於checkCount的連接則放入evictConnections;
- 空余時間大於minEvictableIdleTimeMillis(受保最小空閑時間),並且索引大於checkCount的連接,假若空余時間大於maxEvictableIdleTimeMillis則放入evictConnections,否則放入keepAliveConnections中進行keepAlive檢測。
如下圖:

數組處理
1.evictConnections:待移除連接數組。使用JdbcUtils.close() 關閉連接。

2.keepAliveConnections:待保活檢測數組。根據配置的validationQuery查詢SQL執行連接可用性校驗。校驗通過后再put(holder)塞進connections可用連接數組。

4.3.泄露連接移除
如果開啟了removeAbandoned ,執行removeAbandoned()。移除泄露連接邏輯如下:

實際上,就是對可能的連接泄露(打開連接后長時間不關閉)兜底。
1)遍歷活躍連接Map<DruidPooledConnection, Object> activeConnections。
2)跳過運行中的連接,running定義:執行SQL前賦值true ,執行完后置false。---》問題1得到答案,不會暴力關閉執行中的連接。
3)如果當前連接已連接時間>=removeAbandonedTimeoutMillis ,直接從activeConnections map 中移除。
這里消耗性能主要兩步驟:
- 1.內存中記錄+移除泄露連接
- 2.打印相關日志的IO---》logAbandoned=false 可關閉寫日志
spring 的druid 連接池一般不會造成泄露。如果出現連接泄露,應該找到問題解決。---》問題2得到答案,目前關閉了寫日志,就剩下了第一點“內存占用+過濾的性能”成本,要求不高的場景可以作為兜底方案使用。如果項目已穩定,推薦關閉。
五.分析&總結
本節為我們根據:申請、釋放連接相關的參數配置,剖析策略是否合理。
5.1 配置分析
spring.datasource.testOnBorrow = false 申請連接時執行validationQuery檢測連接是否有效
spring.datasource.testOnReturn = false 歸還連接時執行validationQuery檢測連接是否有效
spring.datasource.testWhileIdle = true testOnBorrow=false時才生效,申請連接的時候檢測,如果空閑時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連接是否有效。
spring.datasource.initialSize = 5 初始連接數 5
spring.datasource.maxActive = 100 最大連接數
spring.datasource.minIdle = 5 最小空閑連接數 5
timeBetweenEvictionRunsMillis= 60000 60s=1分鍾檢測一次
minEvictableIdleTimeMillis=300000 300s=5分鍾 最小空閑不移除時間
maxEvictableIdleTimeMillis 未設置最大空閑移除時間,默認DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS = 1000L * 60L * 60L * 7 = 7小時。
keepAlive: 未設置保活開關,默認false關閉。不執行保活測試策略。
上述配置對應的策略:
1.初始策略
初始5個連接,最多可開啟100個連接。
2.申請策略
申請連接的時候檢測,如果連接空閑時間大於1分鍾(檢測間隔時間),執行validationQuery檢測連接是否有效。---》這里可確保我們空閑時間超過1分鍾的連接,校驗后使用。
3.回收策略
每一分鍾執行一次檢測,策略如下:
1.連接空閑小於5分鍾,不移除。
2.連接空閑大於5分鍾,保留”minIdle設置的5個idle連接”,可移除(總數-5)個連接。
3.連接空閑大於7小時,可移除“minIdle設置的5個idle連接”。---》因為沒有設置maxEvictableIdleTimeMillis ,默認空閑7小時后才會移除。不過一共就5個倒也沒什么事。
4.連接空閑5分鍾~7小時,由於沒開啟keepAlive保活開關,無法對“minIdle設置的5個idle連接”保活測試。-->minIdle設置的5個idle連接,這段時間一直不回收,也不做保活測試,連接是否有效無法保證。
5.2總結
1.現有項
removeAbandoned=true 開啟連接泄露檢測,要求不高的場景可以作為兜底方案使用。如果項目已穩定,推薦關閉。
2.可添加項
phyTimeoutMillis:看需要開啟。物理超時時間。不管空閑時間,超時直接移除。---》這個是終極兜底方案,可以確保超時強制移除。
maxEvictableIdleTimeMillis:建議開啟,實現精細化控制。
keepAlive: 建議開啟。可針對“minIdle設置的空閑連接”,進行保活測試,從而提升連接的質量。
