最近突然对底层原理比较痴迷,本着第一性原理的原则,对号称性能最强Hikari的源码做了深入解读,了解下为什么它能成为目前为止最快的数据库连接池技术。
【JDBC的出现】JDBC是java连接数据库的一套JDK API标准,其实在JDK1.1版本就已经提出了,但是它也只提供了最基本的连接数据库方式。
【数据库连接池技术诞生】在应用的性能优化过程中,池化技术是一种通过重复利用资源的技术,因为重复利用可以避免资源重复创建,进而节省性能损耗。在数据库的连接技术上尤为需要这种技术加持,所以就诞生了 各种数据库连接池技术。
【数据库连接池技术历史】
按照诞生顺序:Apache Commons的DBCP—>C3P0->Hikari->Druid
性能方面: Hikari最好,在并发数提升后也没有明显的性能下降。
以下是hikari获取connection的核心流程,可以看出来有多级缓存来获取资源
以下是核心获取connection的处理逻辑,有多级池来保障资源重复利用,threadLocal池尤其高效。
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// Try the thread-local list first
final var list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final var 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;
}
}
// Otherwise, scan the shared list ... then poll the handoff queue
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 var 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 {
waiters.decrementAndGet();
}
}connnection多级缓存,第一级使用了threadlocal获取当前线程缓存的连接,第二级使用了sharedList获取整个池子里的缓存,第三级使用SynchronousQueue同步器来获取新构造的connection,依赖SynchronousQueue的特性完成线程间高效通信。
使用fastlist移除,尾部遍历,针对数据库连接池的场景效率更高。
使用javasist技术代理connection,调用栈比继承少。
尽量使用CAS保证操作线程安全,没有锁阻塞
目前Hikari性能最好,druid次之,不过druid的强项在监控上,所以如果是企业级应用且性能要求不高,还是推荐使用Druid,加入性能要求很高,使用Hikari可以节省很多资源,不过得丰富其监控能力。
根据具体服务的CPU核数而定,不需要设置太多的原因是设置的多的话也没有足够的CPU来处理线程,而且对于数据库连接很多操作是IO操作,会阻塞线程,线程太多最后会导致线程频繁切换,带来巨大的性能损耗。
参考公式:连接数 = ((CPU核数 * 2) + 有效磁盘数),