Hikari連線池原始碼解讀
導讀 | 幾年前,我最開始接觸的資料庫連線池是 C3P0,後來是阿里的 Druid,但隨著 Springboot 2.0 選擇 HikariCP 作為預設資料庫連線池這一事件之後,HikariCP 作為一個後起之秀出現在大眾的視野中,以其速度快,效能高等特點受到越來越多人青睞。 |
幾年前,我最開始接觸的資料庫連線池是 C3P0,後來是阿里的 Druid,但隨著 Springboot 2.0 選擇 HikariCP 作為預設資料庫連線池這一事件之後,HikariCP 作為一個後起之秀出現在大眾的視野中,以其速度快,效能高等特點受到越來越多人青睞。
在實際開發工作中,資料庫一直是引發報警的重災區,而與資料庫打交道的就是 Hikari 連線池,看懂 Hikari 報警日誌並定位異常原因,是實際工作中必不可少的技能!
本文以 Hikari 2.7.9 版本原始碼進行分析,帶大家理解 Hikari 原理,學會處理線上問題!
原始碼地址:
在學習一項技術之前,需要先在宏觀的層面去看到它的位置,比如我們今天學習的 HikariCP,它在什麼位置?
以 Spring Boot 專案為例,我們有 Service 業務層,編寫業務程式碼,而與資料庫打交道的是 ORM 框架(例如 MyBatis),ORM 框架的下一層是 Hikari 連線池,Hikari 連線池的下一層是 MySQL 驅動,MySQL 驅動的下一層是 MySQL 伺服器。理解了這個宏觀層次,我們再去學習 Hikari 就不會學的那麼稀裡糊塗了。
其次,我們需要明白資料庫連線池是幹什麼的?
簡單來說,資料庫連線池負責分配、管理和釋放資料庫的連線。有了資料庫連線池就可以複用資料庫連線,可以避免連線頻繁建立、關閉的開銷,提升系統的效能。它可以幫助我們釋放過期的資料庫連線,避免因為使用過期的資料庫連線而引起的異常。
至於 Hikari,它是一個“零開銷”生產就緒的 JDBC 連線池。庫非常輕,大約 130 Kb。
我們先來看一個線上 Hikari 連線池配置需要哪些引數。
@Bean("dataSource") public DataSource dataSource() { HikariConfig cfg = new HikariConfig(); // 從池中借出的連線是否預設自動提交事務,預設開啟 cfg.setAutoCommit(false); // 從池中獲取連線時的等待時間 cfg.setConnectionTimeout(); // MYSQL連線相關 cfg.setJdbcUrl(); cfg.setDriverClassName(); cfg.setUsername(); cfg.setPassword(); // 連線池的最大容量 cfg.setMaximumPoolSize(); // 連線池的最小容量,官網不建議設定,保持與 MaximumPoolSize 一致,從而獲得最高效能和對峰值需求的響應 // cfg.setMinimumIdle(); // 連線池的名稱,用於日誌監控,多資料來源要區分 cfg.setPoolName(); // 池中連線的最長存活時間,要比資料庫的 wait_timeout 時間要小不少 cfg.setMaxLifetime(); // 連線在池中閒置的最長時間,僅在 minimumIdle 小於 maximumPoolSize 時生效(本配置不生效) cfg.setIdleTimeout(); // 連線洩露檢測,預設 0 不開啟 // cfg.setLeakDetectionThreshold(); // 測試連結是否有效的超時時間,預設 5 秒 // cfg.setValidationTimeout(); // MYSQL驅動環境變數 // 字元編解碼 cfg.addDataSourceProperty("characterEncoding", ); cfg.addDataSourceProperty("useUnicode", ); // 較新版本的 MySQL 支援伺服器端準備好的語句 cfg.addDataSourceProperty("useServerPrepStmts", ); // 快取SQL開關 cfg.addDataSourceProperty("cachePrepStmts", ); // 快取SQL數量 cfg.addDataSourceProperty("prepStmtCacheSize", ); // 快取SQL長度,預設256 // prepStmtCacheSqlLimit return new HikariDataSource(cfg); }
官方配置說明:
萬事開頭難,下載 Hikari 原始碼到本地後該從哪開始去看呢?不妨從下面兩個入口去分析。
// 1、初始化入口 new HikariDataSource(cfg) // 2、獲取連線 public interface DataSource extends CommonDataSource, Wrapper { Connection getConnection() throws SQLException; }
初始化分析主要有兩部分工作,一是校驗配置並且會矯正不符合規範的配置;二是例項化 Hikari 連線池。
public HikariDataSource(HikariConfig configuration) { // 1、校驗配置 並 矯正配置 configuration.validate(); configuration.copyStateTo(this); LOGGER.info("{} - Starting...", configuration.getPoolName()); // 2、建立連線池,注意這裡設定了 fastPathPool pool = fastPathPool = new HikariPool(this); LOGGER.info("{} - Start completed.", configuration.getPoolName()); this.seal(); }
矯正配置
校驗配置會直接拋異常,大部分坑來源於矯正配置這一步,這會使你的配置不生效。
private void validateNumerics() { // maxLifetime 連結最大存活時間最低30秒,小於30秒不生效 if (maxLifetime != 0 && maxLifetime < SECONDS.toMillis(30)) { LOGGER.warn("{} - maxLifetime is less than 30000ms, setting to default {}ms.", poolName, MAX_LIFETIME); maxLifetime = MAX_LIFETIME; } // idleTimeout 空閒超時不能大於或者接近 maxLifetime,否則設定 0,禁用空閒執行緒回收 if (idleTimeout + SECONDS.toMillis(1) > maxLifetime && maxLifetime > 0) { LOGGER.warn("{} - idleTimeout is close to or more than maxLifetime, disabling it.", poolName); idleTimeout = 0; } // idleTimeout 空閒超時不能低於預設值 10 秒 if (idleTimeout != 0 && idleTimeout < SECONDS.toMillis(10)) { LOGGER.warn("{} - idleTimeout is less than 10000ms, setting to default {}ms.", poolName, IDLE_TIMEOUT); idleTimeout = IDLE_TIMEOUT; } // 連線洩露檢測的時間,預設 0 不開啟,不能低於 2 秒,不能比 maxLifetime 大,否則不開啟 if (leakDetectionThreshold > 0 && !unitTest) { if (leakDetectionThreshold < SECONDS.toMillis(2) || (leakDetectionThreshold > maxLifetime && maxLifetime > 0)) { LOGGER.warn("{} - leakDetectionThreshold is less than 2000ms or more than maxLifetime, disabling it.", poolName); leakDetectionThreshold = 0; } } // 從連線池獲取連線時最大等待時間,預設值 30 秒, 低於 250 毫秒不生效 if (connectionTimeout < 250) { LOGGER.warn("{} - connectionTimeout is less than 250ms, setting to {}ms.", poolName, CONNECTION_TIMEOUT); connectionTimeout = CONNECTION_TIMEOUT; } // 檢測連線是否有效的超時時間,預設 5 秒,低於 250 毫秒不生效 if (validationTimeout < 250) { LOGGER.warn("{} - validationTimeout is less than 250ms, setting to {}ms.", poolName, VALIDATION_TIMEOUT); validationTimeout = VALIDATION_TIMEOUT; } // 連線池中連線的最大數量,minIdle 大於 0 與其保持一致,否則預設 10 if (maxPoolSize < 1) { maxPoolSize = (minIdle <= 0) ? DEFAULT_POOL_SIZE : minIdle; } // 維持的最小連線數量,不配置預設等於 maxPoolSize if (minIdle < 0 || minIdle > maxPoolSize) { minIdle = maxPoolSize; } }
建立連線池
透過分析連線池例項化過程,可以看到 Hikari 的作者是多麼喜歡用非同步操作了,包括空閒執行緒處理、新增連線、關閉連線、連線洩露檢測等。
這一步會建立 1 個 LinkedBlockQueue 阻塞佇列,需要明確的是,這個佇列並不是實際連線池的佇列, 只是用來放置新增連線的請求。
public HikariPool(final HikariConfig config) { super(config); // 建立 ConcurrentBag 管理連線池,有連線池的四個重要操作:borrow獲取連線,requite歸還連線,add新增連線,remove移除連線。 this.connectionBag = new ConcurrentBag<>(this); // getConnection 獲取連線時的併發控制,預設關閉 this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK; // 空閒執行緒池 處理定時任務 this.houseKeepingExecutorService = initializeHouseKeepingExecutorService(); // 快速預檢查 建立1個連結 checkFailFast(); // Metrics 監控收集相關 if (config.getMetricsTrackerFactory() != null) { setMetricsTrackerFactory(config.getMetricsTrackerFactory()); } else { setMetricRegistry(config.getMetricRegistry()); } // 健康檢查註冊相關,預設 無 setHealthCheckRegistry(config.getHealthCheckRegistry()); // 處理JMX監控相關 registerMBeans(this); ThreadFactory threadFactory = config.getThreadFactory(); // 建立 maxPoolSize 大小的 LinkedBlockQueue 阻塞佇列,用來構造 addConnectionExecutor LinkedBlockingQueueaddConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize()); // 映象只讀佇列 this.addConnectionQueue = unmodifiableCollection(addConnectionQueue); // 建立 新增連線的 執行緒池,實際執行緒數只有1,拒絕策略是丟棄不處理 this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy()); // 建立 關閉連線的 執行緒池,實際執行緒數只有1,拒絕策略是呼叫執行緒同步執行 this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy()); // 建立 檢測連線洩露 的工廠,使用的時候只需要傳1個連線物件 this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService); // 延時100ms後,開啟任務,每30s執行空閒執行緒處理 this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS); }
Hikari 的連線獲取分為兩步,一是呼叫 connectionBag.borrow() 方法從池中獲取連線,這裡等待超時時間是 connectionTimeout;二是獲取連線後,會主動檢測連線是否可用,如果不可用會關閉連線,連線可用的話會繫結一個定時任務用於連線洩露的檢測。
很多時候,會在異常日誌中看到 Connection is not available 錯誤日誌後攜帶的 request timed out 耗時遠超 connectionTimeout,仔細分析原始碼這也是合理的。
HikariDataSource
@Override public Connection getConnection() throws SQLException { if (isClosed()) { throw new SQLException("HikariDataSource " + this + " has been closed."); } // 因為初始化 HikariDataSource 的時候已經設定了,所以這裡直接走 return if (fastPathPool != null) { return fastPathPool.getConnection(); } // See HikariPool result = pool; if (result == null) { synchronized (this) { result = pool; if (result == null) { validate(); LOGGER.info("{} - Starting...", getPoolName()); try { pool = result = new HikariPool(this); this.seal(); } catch (PoolInitializationException pie) { if (pie.getCause() instanceof SQLException) { throw (SQLException) pie.getCause(); } else { throw pie; } } LOGGER.info("{} - Start completed.", getPoolName()); } } } return result.getConnection(); } HikariPool public Connection getConnection() throws SQLException { // 這裡傳了設定的連結超時 return getConnection(connectionTimeout); } public Connection getConnection(final long hardTimeout) throws SQLException { suspendResumeLock.acquire(); // 併發數量控制,預設關閉 final long startTime = currentTime(); try { long timeout = hardTimeout; do { // 此處等待 connectionTimeout ,獲取不到拋異常 PoolEntry poolEntry = connectionBag.borrow(timeout, MILLISECONDS); if (poolEntry == null) { break; // We timed out... break and throw exception } final long now = currentTime(); // 移除已經標記為廢棄的連線 或者 空閒超過 500 毫秒且不可用的連線(超時時間是 validationTimeout,預設5秒) if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) { closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE); timeout = hardTimeout - elapsedMillis(startTime); } else { metricsTracker.recordBorrowStats(poolEntry, startTime); // 先新增連線洩露檢測任務,再透過Javassist建立代理連線 return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry), now); } } while (timeout > 0L); metricsTracker.recordBorrowTimeoutStats(startTime); // 拋異常 Connection is not available, request timed out after {}ms. throw createTimeoutException(startTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(poolName + " - Interrupted during connection acquisition", e); } finally { suspendResumeLock.release(); } }
Hikari 在初始化連線池的時候,就已經開啟了一條非同步定時任務。該任務每 30 秒執行一次空閒連線回收,程式碼如下:
/** * The house keeping task to retire and maintain minimum idle connections. * 用於補充和移除最小空閒連線的管理任務。 */ private final class HouseKeeper implements Runnable { private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS); @Override public void run() { try { // refresh timeouts in case they changed via MBean connectionTimeout = config.getConnectionTimeout(); validationTimeout = config.getValidationTimeout(); leakTaskFactory.updateLeakDetectionThreshold(config.getLeakDetectionThreshold()); final long idleTimeout = config.getIdleTimeout(); final long now = currentTime(); // Detect retrograde time, allowing +128ms as per NTP spec. // 為了防止時鐘回撥,給了128ms的gap,正常情況下,ntp的校準回撥不會超過128ms // now = plusMillis(previous, HOUSEKEEPING_PERIOD_MS) + 100ms if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) { LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.", poolName, elapsedDisplayString(previous, now)); previous = now; softEvictConnections(); return; } else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) { // No point evicting for forward clock motion, this merely accelerates connection retirement anyway LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now)); } previous = now; String afterPrefix = "Pool "; // 回收符合條件的空閒連線:如果最小連線數等於最大連線數,就不會回收 if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) { logPoolState("Before cleanup "); afterPrefix = "After cleanup "; final ListnotInUse = connectionBag.values(STATE_NOT_IN_USE); int toRemove = notInUse.size() - config.getMinimumIdle(); for (PoolEntry entry : notInUse) { // 有空閒連線 且 空閒時間達標 且 CAS更改狀態成功 if (toRemove > 0 && elapsedMillis(entry.lastAccessed, now) > idleTimeout && connectionBag.reserve(entry)) { // 關閉連線 closeConnection(entry, "(connection has passed idleTimeout)"); toRemove--; } } } logPoolState(afterPrefix); // 補充連結 fillPool(); // Try to maintain minimum connections } catch (Exception e) { LOGGER.error("Unexpected exception in housekeeping task", e); } } }
Hikari 在建立一個連線例項的時候,就已經為其繫結了一個定時任務用於關閉連線。
private PoolEntry createPoolEntry() { try { final PoolEntry poolEntry = newPoolEntry(); final long maxLifetime = config.getMaxLifetime(); if (maxLifetime > 0) { // variance up to 2.5% of the maxlifetime // 減去一部分隨機數,避免大範圍連線斷開 final long variance = maxLifetime > 10_000 ? ThreadLocalRandom.current().nextLong( maxLifetime / 40 ) : 0; final long lifetime = maxLifetime - variance; // 此處 maxLifetime 不能超過資料庫最大允許連線時間 poolEntry.setFutureEol(houseKeepingExecutorService.schedule( () -> { if (softEvictConnection(poolEntry, "(connection has passed maxLifetime)", false /* not owner */)) { addBagItem(connectionBag.getWaitingThreadCount()); } }, lifetime, MILLISECONDS)); } return poolEntry; } catch (Exception e) { if (poolState == POOL_NORMAL) { // we check POOL_NORMAL to avoid a flood of messages if shutdown() is running concurrently LOGGER.debug("{} - Cannot acquire connection from data source", poolName, (e instanceof ConnectionSetupException ? e.getCause() : e)); } return null; } } 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 關閉連線的過程是先將連線例項標記為廢棄,這樣哪怕因為連線正在使用導致關閉失敗,也可以在下次獲取連線時再對其進行關閉。 複製 private boolean softEvictConnection(final PoolEntry poolEntry, final String reason, final boolean owner) { // 先標記為廢棄、哪怕下面關閉失敗,getConnection 時也會移除 poolEntry.markEvicted(); // 使用中的連線不會關閉 if (owner || connectionBag.reserve(poolEntry)) { closeConnection(poolEntry, reason); return true; } return false; }
Hikari 在處理連線洩露時使用到了工廠模式,只需要將連線例項 PoolEntry 傳入工廠,即可提交連線洩露檢測的延時任務。而所謂的連結洩露檢測只是列印 1 次 WARN 日誌。
class ProxyLeakTaskFactory { private ScheduledExecutorService executorService; private long leakDetectionThreshold; ProxyLeakTaskFactory(final long leakDetectionThreshold, final ScheduledExecutorService executorService) { this.executorService = executorService; this.leakDetectionThreshold = leakDetectionThreshold; } // 1、傳入連線物件 ProxyLeakTask schedule(final PoolEntry poolEntry) { // 連線洩露檢測時間等於 0 不生效 return (leakDetectionThreshold == 0) ? ProxyLeakTask.NO_LEAK : scheduleNewTask(poolEntry); } void updateLeakDetectionThreshold(final long leakDetectionThreshold) { this.leakDetectionThreshold = leakDetectionThreshold; } // 2、提交延時任務 private ProxyLeakTask scheduleNewTask(PoolEntry poolEntry) { ProxyLeakTask task = new ProxyLeakTask(poolEntry); task.schedule(executorService, leakDetectionThreshold); return task; } } ProxyLeakTask class ProxyLeakTask implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(ProxyLeakTask.class); static final ProxyLeakTask NO_LEAK; private ScheduledFuture scheduledFuture; private String connectionName; private Exception exception; private String threadName; private boolean isLeaked; static { NO_LEAK = new ProxyLeakTask() { @Override void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) {} @Override public void run() {} @Override public void cancel() {} }; } ProxyLeakTask(final PoolEntry poolEntry) { this.exception = new Exception("Apparent connection leak detected"); this.threadName = Thread.currentThread().getName(); this.connectionName = poolEntry.connection.toString(); } private ProxyLeakTask() { } void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold) { scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS); } /** {@inheritDoc} */ @Override public void run() { isLeaked = true; final StackTraceElement[] stackTrace = exception.getStackTrace(); final StackTraceElement[] trace = new StackTraceElement[stackTrace.length - 5]; System.arraycopy(stackTrace, 5, trace, 0, trace.length); // 列印 1 次連線洩露的 WARN 日誌 exception.setStackTrace(trace); LOGGER.warn("Connection leak detection triggered for {} on thread {}, stack trace follows", connectionName, threadName, exception); } void cancel() { scheduledFuture.cancel(false); if (isLeaked) { LOGGER.info("Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)", connectionName, threadName); } } }
ConcurrentBag 才是真正的連線池,也是 Hikari “零開銷”的奧秘所在。
簡而言之,Hikari 透過 CopyOnWriteArrayList + State(狀態) + CAS 來避免了上鎖。
CopyOnWriteArrayList 存放真正的連線物件,每個連線物件都有四種狀態:
- STATE_NOT_IN_USE:空閒
- STATE_IN_USE:活躍
- STATE_REMOVED:移除
- STATE_RESERVED:不可用
比如在獲取連線時,透過呼叫 bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE) 方法解決併發問題。
public class ConcurrentBagimplements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class); // 所有連線:透過CopyOnWriteArrayList + State + cas 來避免了上鎖 private final CopyOnWriteArrayListsharedList; // threadList是否使用弱引用 private final boolean weakThreadLocals; // 歸還的時候快取空閒連線到 ThreadLocal:requite()、borrow() private final ThreadLocal<1list> threadList; private final IBagStateListener listener; // 等待獲取連線的執行緒數:調 borrow() 方法+1,調完-1 private final AtomicInteger waiters; // 連線池關閉標識 private volatile boolean closed; // 佇列大小為0的阻塞佇列:生產者消費者模式 private final SynchronousQueuehandoffQueue; public interface IConcurrentBagEntry { int STATE_NOT_IN_USE = 0; // 空閒 int STATE_IN_USE = 1; // 活躍 int STATE_REMOVED = -1; // 移除 int STATE_RESERVED = -2; // 不可用 boolean compareAndSet(int expectState, int newState); void setState(int newState); int getState(); } public interface IBagStateListener { void addBagItem(int waiting); } public ConcurrentBag(final IBagStateListener listener) { this.listener = listener; this.weakThreadLocals = useWeakThreadLocals(); this.handoffQueue = new SynchronousQueue<>(true); this.waiters = new AtomicInteger(); this.sharedList = new CopyOnWriteArrayList<>(); if (weakThreadLocals) { this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16)); } else { this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16)); } } public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException { // Try the thread-local list first // 先從 threadLocal 快取中獲取 final List list = threadList.get(); for (int i = list.size() - 1; i >= 0; i--) { // 從尾部讀取:後快取的優先用,細節! final Object entry = list.remove(i); @SuppressWarnings("unchecked") final T bagEntry = weakThreadLocals ? ((WeakReference) entry).get() : (T) entry; if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } } // Otherwise, scan the shared list ... then poll the handoff queue // 如果本地快取獲取不到,從 shardList 連線池中獲取,等待連線數+1 final int waiting = waiters.incrementAndGet(); try { for (T bagEntry : sharedList) { if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { // If we may have stolen another waiter's connection, request another bag add. // 併發情況下,保證能夠及時補充連線 if (waiting > 1) { listener.addBagItem(waiting - 1); } return bagEntry; } } // 如果 shardList 連線池中也沒獲得連線,提交新增連線的非同步任務,然後再從 handoffQueue 阻塞獲取。 listener.addBagItem(waiting); timeout = timeUnit.toNanos(timeout); do { final long start = currentTime(); final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS); if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { return bagEntry; } timeout -= elapsedNanos(start); } while (timeout > 10_000); return null; } finally { // 等待連線數減 1 waiters.decrementAndGet(); } } public void requite(final T bagEntry) { bagEntry.setState(STATE_NOT_IN_USE); // 如果有執行緒正在獲取連結,則優先透過 handoffQueue 阻塞佇列歸還給其他執行緒使用 for (int i = 0; waiters.get() > 0; i++) { if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) { return; } else if ((i & 0xff) == 0xff) { // 每遍歷 255 個休眠 10 微妙 parkNanos(MICROSECONDS.toNanos(10)); } else { // 執行緒讓步 yield(); } } // 沒有其它執行緒用,就放入本地快取 final List threadLocalList = threadList.get(); threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry); } public void add(final T bagEntry) { if (closed) { LOGGER.info("ConcurrentBag has been closed, ignoring add()"); throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()"); } sharedList.add(bagEntry); // spin until a thread takes it or none are waiting // 如果有執行緒等待獲取連線,迴圈透過 handoffQueue 提交連線 while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) { yield(); } } public boolean remove(final T bagEntry) { // 使用 CAS 將連線置為 STATE_REMOVED 狀態 if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) { LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry); return false; } // CAS 成功後再刪除連線 final boolean removed = sharedList.remove(bagEntry); if (!removed && !closed) { LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry); } return removed; } @Override public void close() { closed = true; } public boolean reserve(final T bagEntry) { return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED); } }
報警日誌
先來看一個真實的線上報警:
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 6791ms. at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:81) at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80) at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67) at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337) at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89) ... 125 common frames omitted Caused by: java.sql.SQLTransientConnectionException: hikari-pool-storecenter - Connection is not available, request timed out after 6791ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100) at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151) at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115) at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78) ... 134 common frames omitted Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: [DataSource IP:127.0.0.1:3306] No operations allowed after connection closed. at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) at com.mysql.jdbc.Util.getInstance(Util.java:408) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:919) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:898) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:887) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:861) at com.mysql.jdbc.ConnectionImpl.throwConnectionClosedException(ConnectionImpl.java:1184) at com.mysql.jdbc.ConnectionImpl.checkClosed(ConnectionImpl.java:1179) at com.mysql.jdbc.ConnectionImpl.setNetworkTimeout(ConnectionImpl.java:5498) at com.zaxxer.hikari.pool.PoolBase.setNetworkTimeout(PoolBase.java:541) at com.zaxxer.hikari.pool.PoolBase.isConnectionAlive(PoolBase.java:162) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:172) ... 139 common frames omitted
思路分析
No operations allowed after connection closed 表示訪問了已經被 MySQL 關閉的連線。
request timed out after 6791ms 包含等待連線超時 connectionTimeout(配置 2 秒) 和測試連線可用 validationTimeout(預設 5 秒) 兩個時間。
boolean isConnectionAlive(final Connection connection) { try { try { setNetworkTimeout(connection, validationTimeout); // validationTimeout 預設 5 秒,最低 1 秒 final int validationSeconds = (int) Math.max(1000L, validationTimeout) / 1000; // 測試連結是否有效 if (isUseJdbc4Validation) { return connection.isValid(validationSeconds); } try (Statement statement = connection.createStatement()) { if (isNetworkTimeoutSupported != TRUE) { setQueryTimeout(statement, validationSeconds); } statement.execute(config.getConnectionTestQuery()); } } finally { setNetworkTimeout(connection, networkTimeout); if (isIsolateInternalQueries && !isAutoCommit) { connection.rollback(); } } return true; } catch (Exception e) { lastConnectionFailure.set(e); // 此處列印 WARN 日誌,可以透過 console.log 檢視是否存在 獲取到已被關閉連線 的情況 LOGGER.warn("{} - Failed to validate connection {} ({})", poolName, connection, e.getMessage()); return false; } }
檢視 console.log,存在大量獲取到已關閉連線的情況:
2022-06-15 01:34:20.445 WARN com.zaxxer.hikari.pool.PoolBase : hikari-pool - Failed to validate connection com.mysql.jdbc.JDBC4Connection@200203c3 ([DataSource IP:127.0.0.1:3306] No operations allowed after connection closed.)
所以推斷報警原因是因為獲取到已經被資料庫關閉的連線。
解決方法
DBA 反饋資料庫的 wait_timeout 是 600 秒,線上配置的 maxLifeTime 是 900 秒,配置有誤,更改為 450 秒。
上線後驗證 console.log 不再持續列印 Failed to validate connection 日誌,並且沒有 No operations allowed after connection closed 報警日誌。
報警日誌
最佳化上線後,觀察到又發生了幾十條報警,並且只集中在 1 臺機器:
Caused by: org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 2000ms. at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:81) at org.mybatis.spring.transaction.SpringManagedTransaction.openConnection(SpringManagedTransaction.java:80) at org.mybatis.spring.transaction.SpringManagedTransaction.getConnection(SpringManagedTransaction.java:67) at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337) at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89) ... 125 common frames omitted Caused by: java.sql.SQLTransientConnectionException: hikari-pool - Connection is not available, request timed out after 2000ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:669) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:183) at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:148) at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100) at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:151) at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:115) at org.springframework.jdbc.datasource.DataSourceUtils.getConnection(DataSourceUtils.java:78) ... 134 common frames omitted
思路分析
報警日誌中沒有 No operations allowed after connection closed,且耗時為 connectionTimeout,推測是沒有獲取到連線,原因可能有:
機器異常:機器負載過大有可能引起 IO 夯。
連線池被打滿:比如存在慢SQL,或者流量太大支撐不住等,連線數實在不夠用。Hikari 提供 HikariPoolMXBean 介面獲取連線池監控資訊。
連線洩露:開啟連線洩露引數後,可在日誌中檢視。
解決方法
機器異常:遷移機器,觀察後續情況。
連線池被打滿:增加 Hikari 連線池監控日誌,觀察連線池使用情況,進一步再做判斷。比如可以透過一個定時任務,每秒列印連線池相關狀態:
@Slf4j @Component public class HikariPoolTask { @Resource private MapdataSourceMap; /** * 延時1秒,每隔1秒 */ @Scheduled(initialDelay = 1000, fixedDelay = 1000) public void run() { if (CollUtil.isNotEmpty(dataSourceMap)) { for (HikariDataSource dataSource : dataSourceMap.values()) { // 連線池名稱 String poolName = dataSource.getPoolName(); HikariPoolMXBean hikariPoolMXBean = dataSource.getHikariPoolMXBean(); // 活躍連線數量 int activeConnections = hikariPoolMXBean.getActiveConnections(); // 空閒連線數量 int idleConnections = hikariPoolMXBean.getIdleConnections(); // 全部連線數量 int totalConnections = hikariPoolMXBean.getTotalConnections(); // 等待連線數量 int threadsAwaitingConnection = hikariPoolMXBean.getThreadsAwaitingConnection(); log.info("{} - activeConnections={}, idleConnections={}, totalConnections={}, threadsAwaitingConnection={}", poolName, activeConnections, idleConnections, totalConnections, threadsAwaitingConnection); } } } }
連線洩露:增加連線洩露檢測引數,比如可以設定 10 秒
leakDetectionThreshold=10000
作者介紹
薛師兄,在某頭部網際網路公司擔任高階研發工程師,熱衷於Java技術棧,對底層原理有獨特的追求。
原文來自:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2908264/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- MOSN 原始碼解析 - 連線池原始碼
- Netty-新連線接入原始碼解讀Netty原始碼
- 資料庫連線池-Druid資料庫連線池原始碼解析資料庫UI原始碼
- spring 簡單的使用 Hikari連線池 和 jdbc連線mysql 的一個簡單例子SpringJDBCMySql單例
- ServiceStack.Redis的原始碼分析(連線與連線池)Redis原始碼
- 《四 資料庫連線池原始碼》手寫資料庫連線池資料庫原始碼
- 連線池和連線數詳解
- OkHttp3原始碼解析(三)——連線池複用HTTP原始碼
- ArrayPool 原始碼解讀之 byte[] 也能池化?原始碼
- 從原始碼分析DBCP資料庫連線池的原理原始碼資料庫
- 連線池
- django連線池Django
- HTTP連線池HTTP
- 在SpringBoot中使用R2DBC連線池的原始碼和教程Spring Boot原始碼
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- MQTT(EMQX) - SpringBoot 整合MQTT 連線池 Demo - 附原始碼 + 線上客服聊天架構圖MQQTSpring Boot原始碼架構
- MongoDB原始碼分析之連結池(ConnPool)GWMongoDB原始碼
- PostgreSQL 原始碼解讀(36)- 查詢語句#21(查詢優化-消除外連線)SQL原始碼優化
- Http持久連線與HttpClient連線池HTTPclient
- FairyGUI原始碼解讀AIGUI原始碼
- ZooKeeper原始碼解讀原始碼
- WeakHashMap,原始碼解讀HashMap原始碼
- Laravel 原始碼解讀Laravel原始碼
- reselect原始碼解讀原始碼
- Swoft 原始碼解讀原始碼
- Seajs原始碼解讀JS原始碼
- ReentrantLock原始碼解讀ReentrantLock原始碼
- MJExtension原始碼解讀原始碼
- Axios 原始碼解讀iOS原始碼
- SDWebImage原始碼解讀Web原始碼
- MJRefresh原始碼解讀原始碼
- Handler原始碼解讀原始碼
- LifeCycle原始碼解讀原始碼
- ThreadLocal 原始碼解讀thread原始碼
- Masonry原始碼解讀原始碼
- LinkedHashMap原始碼解讀HashMap原始碼
- ConcurrentHashMap原始碼解讀HashMap原始碼
- Redux原始碼解讀Redux原始碼