第四部分-并发编程案例分析4:高性能数据库连接池HikariCP
1.高性能数据库连接池HiKarCP
c3p0,DBCP,Tomcat JDBC Connection Pool,Druid
最火的是Hikaricp
最快的数据库连接池,springboot2.0作为默认数据库连接池
2.数据库连接池
池化资源,避免重量级资源的频繁创建和销毁。数据库连接池就是避免数据库连接频繁创建爱你和销毁。
需要时从池子里取,用完,将其归还到池子中
实际工作中,持久层框架来完成数据库额crud
3.HiKariCP性能高秘籍1,使用自定义数据结构FastList
数据库操作之后,需要关闭ResultSet,Statement,Connection
有同学只关connection,为了解决这个问题,就希望自动关闭statement及resultSet
思路?
connection跟踪创建的statement,将statement保存在ArrayList中,
HiKariCP做了什么?用ArrayList太慢。因为arraylist的remove()删除statement时候,有优化余地的
正产一个请求,依次创建6个statement,S1-S6,关闭的时候一般是逆序的S6-S1,而ArrayList的remove()方法是顺序遍历查找,再逆序删除statement的时候使用正向查找,效率就太慢了,
所以进行了优化成逆序查找
自定义FastList就是重写remove()方法,删除时候逆序遍历查找。同时get(index)没有对index进行越界检查。提高了性能够。
@Override
public boolean remove(Object element)
{
for (int index = size - 1; index >= 0; index--) {
if (element == elementData[index]) {
final int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
elementData[--size] = null;
return true;
}
}
return false;
}
@Override
public T get(int index)
{
return elementData[index];
}
4.HiKariCP性能高秘籍2,使用自定义数据结构ConcurrentBag
核心思想:使用ThreadLocal 避免部分并发问题
ConcurrentBag关键属性
//用于存储所有的数据库连接
CopyOnWriteArrayList<T> sharedList;
//线程本地存储中的数据库连接
ThreadLocal<List<Object>> threadList;
//等待数据库连接的线程数
AtomicInteger waiters;
//分配数据库连接的工具
SynchronousQueue<T> handoffQueue;
线程池初始化时候,调用add将连接加入ConncurrentBag
(加入共享队列sharedList,如果有线程等待数据库连接,就handoffQueue将新创建的连接分配出去)
//将空闲连接添加到队列
void add(final T bagEntry){
//加入共享队列
sharedList.add(bagEntry);
//如果有等待连接的线程,
//则通过handoffQueue直接分配给等待的线程
while (waiters.get() > 0
&& bagEntry.getState() == STATE_NOT_IN_USE
&& !handoffQueue.offer(bagEntry)) {
yield();
}
}
获取数据库连接borrow方法
1.先看本地存储ThreadLocal是否有空闲,如果有,返回空闲连接
2.本地存储无空闲,共享队列中获取连接
3.共享队列也无空闲连接,请求线程需要等待
(注意:线程本地存储的连接是可以被其他线程窃取的,需要用CAS方法防止重复分配)
T borrow(long timeout, final TimeUnit timeUnit){
// 先查看线程本地存储是否有空闲连接
final List<Object> list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
final T bagEntry = weakThreadLocals
? ((WeakReference<T>) entry).get()
: (T) entry;
//线程本地存储中的连接也可以被窃取,
//所以需要用CAS方法防止重复分配
if (bagEntry != null
&& bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
// 线程本地存储中无空闲连接,则从共享队列中获取
final int waiting = waiters.incrementAndGet();
try {
for (T bagEntry : sharedList) {
//如果共享队列中有空闲连接,则返回
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
//共享队列中没有连接,则需要等待
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);
//超时没有获取到连接,返回null
return null;
} finally {
waiters.decrementAndGet();
}
}
释放连接,requite()
修改连接状态STATE_NOT_IN_USE,查看是否在等待线程,有,分配给线程,没有,保存到ThreadLocal本地存储里
//释放连接
void requite(final T bagEntry){
//更新连接状态
bagEntry.setState(STATE_NOT_IN_USE);
//如果有等待的线程,则直接分配给线程,无需进入任何队列
for (int i = 0; waiters.get() > 0; i++) {
if (bagEntry.getState() != STATE_NOT_IN_USE
|| handoffQueue.offer(bagEntry)) {
return;
} else if ((i & 0xff) == 0xff) {
parkNanos(MICROSECONDS.toNanos(10));
} else {
yield();
}
}
//如果没有等待的线程,则进入线程本地存储
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals
? new WeakReference<>(bagEntry)
: bagEntry);
}
}
5.总结
HikariCP的快
(1)无锁算法
(2)自定义数据结构FastList,解决statement等资源的释放时性能问题
(3)自定义数据结构ConcurrentBag,通过ThreadLocal做了一次预分配,将共享连接池前置加了一个类似缓存的东西。避免了直接竞争共享资源。
(4)为什么本地连接会被窃取?ThreadLocal里没空闲的,会去shardList里取NOT_IN_USE的连接,这个连接可以已经在其他ThreadLocal里存在了,可能会出现线程T2从shardList里取到了T1存在ThreadLocal里的还没使用的连接。