现在市面上的数据库连接池非常多,其中HikariCP被Sping Boot2选中为默认的数据库连接池,且体积仅有152kb
为何选择HikariCP?
高性能
,可以PK掉其它所有连接池,这个原因就足够了
HikariCP为什么这么快?
- 对JDBC主要核心对象Connection、Statement、PreparedStatement、CallableStatement以及ResultSet的封装
- 使用
JAVAASSIST动态代理
- 专为连接池设计的lock-less集合:
ConcurrentBag
- 使用
FastList
替代ArrayList - 职责单一,仅作连接池
接下来我们从这几个方面逐个进行分析
动态代理(字节码类库JAVASSIST实现)
我们常见的字节码类库有ASM、CGLIB等,而JAVASSIST它的优点在于其速度更快、生成的字节码相比于JDK Proxy更少,精简了很多不必要的字节码
这篇来自阿里的文章做了一个动态代理的性能对比(http://javatar.iteye.com/blog/814426),得出的
结论如下:
- ASM和JAVASSIST字节码生成方式不相上下,都很快,是CGLIB的5倍。
- CGLIB次之,是JDK自带的两倍。
- JDK自带的再次之,因JDK1.6对动态代理做了优化,如果用低版本JDK更慢,要注意的是JDK也是通过字节码生成来实现动态代理的,而不是反射。
- JAVASSIST提供者动态代理接口最慢,比JDK自带的还慢。 (这也是为什么网上有人说JAVASSIST比JDK还慢的原因,用JAVASSIST最好别用它提供的动态代理接口,而可以考虑用它的字节码生成方式)
每一个都有对应的使用场景,JDK、CGLIB都考虑比较多的因素,以及继承或包装了自己的一些类,所以字节码非常大,而我们很多时候用不上,所以可以选择JAVASSIST
ConcurrentBag:更好的并发集合类实现
优势:
- 这是一个专门为连接池设计的lock-less集合,其实现比LickedBlockingQueue、LinkedTransferQueue更好的并发性能。
- ConcurrentBag通过拆分 CopyOnWriteArrayList、ThreadLocal和SynchronousQueue进行并发数据交互。ThreadLocal和CopyOnWriteArrayList在ConcurrentBag中都是成员变量,线程间不共享,避免了伪共享(false sharing)的发生。
- ConcurrentBag采用了queue-stealing的机制获取元素:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则再次从共享的CopyOnWriteArrayList中获取。
源码分析
自主实现的并发包:ConcurrentBag
,一个包肯定有一些基本的方法,比如添加、移除、借取资源、归还资源,接下来我们通过源码分析来解开它的高性能的神秘面纱
先来看看几个重要的字段:
// 负责存放ConcurrentBag中全部用于借出去的资源
private final CopyOnWriteArrayList<T> sharedList;
// 用于加速本地资源访问,避免线程交互
private final ThreadLocal<List<Object>> threadList;
// 用于存在资源等待线程时的第一手资源交接
private final SynchronousQueue<T> handoffQueue;
然后看看add方法:
/**
* 添加一个对象,所以的资源仅能通过此方法添加
*
* @param 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()");
}
// 新加入的资源优先放入CopyOnWriteArrayList
sharedList.add(bagEntry);
// 当有等待资源的线程时,将资源交到某个等待线程(handoffQueue.offer(bagEntry))后才返回
while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {
Thread.yield();
}
}
没什么复杂逻辑,就是将资源放入sharedList集合然后看看是否有线程需要,就给出去
然后是remove
/**
* 从bag中取出一个值
*/
public boolean remove(final T bagEntry) {
// 如果资源正在使用且无法进行状态转换,则返回失败
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;
}
// 从CopyOnWriteArrayList中移出
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
// 从当前线程资源中移除
threadList.get().remove(bagEntry);
return removed;
}
同理,先进行资源状态更新,然后刚才的sharedList中remove,再从本地化资源remove
/**
* 从bag中借用BagEntry资源
*/
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
// 优先在thread-local中查看有没可用资源
final List<Object> 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<T>) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
// 当无可用本地化资源时,查看sharedList是否存在可用资源
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;
}
}
// 因为可能“抢走”了其他线程的资源,因此提醒包裹进行资源添加
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final long start = currentTime();
// 当现有资源全部在使用的时候,等待handoffQueue一个被释放的资源或一个新的资源
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 {
waiters.decrementAndGet();
}
}
借取资源,基本查找顺序是:threadList->sharedList
/**
* 还回一个借出去的对象,如果只有借出去borrowed()而没有调用还requite()方法,则会导致内存泄漏
*/
public 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 {
Thread.yield();
}
}
// 否则进行资源本地化到ThreadLocal
final List<Object> threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
}
归还资源,先更新状态,如果有线程在等待资源,直接给它,否则存入本地化资源threadList,这里有一个判断是否弱引用的标记weakThreadLocals,我们来看看判断方法
/**
* 根据在该类和系统类加载器之间是否有一个自定义类加载器实现来确定是否使用WeakReferences。
*/
private boolean useWeakThreadLocals() {
try {
// 人工指定是否使用弱引用,但官方不推荐自主配置
if (System.getProperty("com.zaxxer.hikari.useWeakReferences") != null) { // undocumented manual override of WeakReference behavior
return Boolean.getBoolean("com.zaxxer.hikari.useWeakReferences");
}
// 通过判断初始化的ClassLoader是否是系统的ClassLoader来确定是否弱引用
return getClass().getClassLoader() != ClassLoader.getSystemClassLoader();
} catch (SecurityException se) {
return true;
}
}
基本就是通过来加载器对比来判断是否属于弱引用
使用FastList替代ArrayList
FastList是List接口的精简实现,仅实现了必要的几个方法。去除了ArrayList中get()的数组越界检查。
@Override
public boolean add(T element)
{
if (size < elementData.length) {
elementData[size++] = element;
}
else {
// overflow-conscious code
final int oldCapacity = elementData.length;
final int newCapacity = oldCapacity << 1;
@SuppressWarnings("unchecked")
final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity);
System.arraycopy(elementData, 0, newElementData, 0, oldCapacity);
newElementData[size++] = element;
elementData = newElementData;
}
return true;
}
HikariCP使用FastList来保存Statement,一般情况下,同一个Connection会同时创建多个Statement,后打开的会先关闭,ArrayList中是从头开始遍历数组,而FastList是从数组的尾部开始遍历,针对此情况是更高效
/**
* 删除元素,从数组的最后一个元素开始遍历
*
* @param element the element to remove
*/
@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;
}
调用链路
竞品比较
功能类别 | 功能 | Druid | HikariCP | DBCP | Tomcat-jdbc | C3P0 |
---|---|---|---|---|---|---|
性能 | PSCache | 是 | 否 | 是 | 是 | 是 |
LRU | 是 | 是 | 是 | 是 | 是 | |
SLB负载均衡支持 | 是 | 否 | 否 | 否 | 否 | |
稳定性 | ExceptionSorter | 是 | 否 | 否 | 否 | 否 |
扩展 | 扩展 | Filter | JdbcIntercepter | |||
监控 | 监控方式 | jmx/log/http | jmx/metrics | jmx | jmx | jmx |
支持SQL级监控 | 是 | 否 | 否 | 否 | 否 | |
Spring/Web关联监控 | 是 | 否 | 否 | 否 | 否 | |
诊断支持 | LogFilter | 否 | 否 | 否 | 否 | |
连接泄露诊断 | logAbandoned | 是 | 否 | 否 | 否 | 否 |
安全 | SQL防注入 | 是 | 无 | 无 | 无 | 无 |
支持配置加密 | 是 | 否 | 否 | 否 | 否 |
这是官方提供的性能测试OPS对比,可以看到HikariCP明显高出了一个量级
总结
在线上系统,性能测试与压测之后,一核心系统与druid相比,性能提高一倍左右。
HikariCP真正做到了大道至简,职责单一。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17869.html