设计一个可靠的连接池
在应用中连接池的使用非常普遍,如访问数据库,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的时候就不是回归到池里,而是释放掉了(不过这个方法并不算太好)。