在應用中連線池的使用非常普遍,如訪問資料庫,Redis等等網路產品的Client
都整合了連線池機制;由於最近在編寫微服務閘道器因此涉及到連線池的編寫,在這裡分享一下實現一個可靠連線池的心得。其實編寫一個連線池並不因難,基礎的Stack
結構就能滿足需要;但在設計的時候有些情況是需要考慮的,怎樣使連線池的效益最大化,特別是如何設計連線池的最大負載,當超過最大負載後應該怎麼做這些問題都衡量一個連線池好壞的標準。接下來通過程式碼的方式一步一步地實現它。
基礎實現
public class ConnectionPool<T> where T : IDisposable, new() { private ConcurrentStack<T> mPool = new ConcurrentStack<T>(); public T Pop() { if (!mPool.TryPop(out T item)) { item = new T(); } return item; } public void Push(T item) { mPool.Push(item); } }
以上是一個最簡單物件池,當然這個物件池是不能真的投入生產,只是大概瞭解基礎原理;因為它是無限量增長的物件池,不過用來做物件池免強還是可以的,用在連線池上那就不太可行了,畢竟大量的連線不僅增加自己的損耗還增加了對端的損耗。
增加最大限制
public class ConnectionPool<T> where T : IDisposable, new() { public ConnectionPool(int max = 100) { mMaxCount = max; } private ConcurrentStack<T> mPool = new ConcurrentStack<T>(); private int mMaxCount; private int mCount; public T Pop() { if (!mPool.TryPop(out T item)) { int count = System.Threading.Interlocked.Increment(ref mCount); if (mCount > mMaxCount) { System.Threading.Interlocked.Decrement(ref mCount); return default(T); } item = new T(); } return item; } public void Push(T item) { mPool.Push(item); } }
以上增加了最大數限制,但在使用上就要面對一個問題,當池負載滿了返回為空的時候程式又要怎樣處理呢?直接拋異常?自旋或sleep
指定次數後還是獲取為空再報異常?對於呼叫者來說最不想看到的肯是異常,就算延時能處理也相對是一件不錯的方法;但當在滿負載的情況大量的執行緒自旋或sleep
又只會讓系統變得更糟糕!所以以上兩種方式並不算是一個好的解決方法。
引入事件驅動
如果請求者要等待使用自旋或sleep
基本上是不可行,這種方法容易損耗CPU資源;接下來引入基於事件驅動
的方法模式,聽上去是不是很高大上,其實設計方式比較簡單就是在Push
引入一個事件通知機制,讓後面等待的請求進行處理;這樣就不用通過自旋或sleep
來完成這個功能。一說到事件驅動相信很多朋友感覺一下子變成了非常複雜,但.net core提供給我們一個好東西async/await
語法糖輕易解決這一問題。
public class ConnectionPool<T> where T : IDisposable, new() { public ConnectionPool(int max = 100) { mMaxCount = max; } private int mWaitQueueLength = 1000; private Stack<T> mPool = new Stack<T>(); private Queue<TaskCompletionSource<T>> mWaitQueue = new Queue<TaskCompletionSource<T>>(); private int mMaxCount; private int mCount; private object mLockPool = new object(); public Task<T> Pop() { lock (mLockPool) { TaskCompletionSource<T> result = new TaskCompletionSource<T>(); if (mPool.Count > 0) { result.SetResult(mPool.Pop()); } else { mCount++; if (mCount < mMaxCount) { result.SetResult(new T()); } else { if (mWaitQueue.Count >= mWaitQueueLength) { result.SetResult(default(T)); } else { mWaitQueue.Enqueue(result); } } } return result.Task; } } public void Push(T item) { lock (mLockPool) { if (mWaitQueue.Count > 0) { var waitItem = mWaitQueue.Dequeue(); Task.Run(() => waitItem.SetResult(item)); } else { mPool.Push(item); } } } }
在物件池中引入了一個佇列,當負載滿的時候請求會扔到佇列中,當物件迴歸後會檢測佇列並觸發請求的狀態機程式碼執行。注意:一定要通過執行緒隔離這個執行,畢竟這程式碼還在一個鎖的程式碼塊裡,如果不用執行緒隔離有能可能會導致下一次其它呼叫進來時產生死鎖的情況。加入了事件驅動使用程式碼如下:
var item = await Pool.Pop(); if(item==null) throw System busy else run item
縮減
池在負載的時候有增長,那在空閒的時候自然也應該有縮減的設計才算合理,同樣這個縮減也可以在Push中設計一下,程式碼就留給大家了,簡單的方法是獲取當前Pool的Pop的併發量,如果併發量少於當前池中的物件數量,那Push的時候就不是迴歸到池裡,而是釋放掉了(不過這個方法並不算太好)。