HikariCP号称是业界跑的最快的数据库连接池,这几年的发展很快,而且SpringBoot 2.0将其作为了默认的数据库连接池
那么他为什么这么快呢,现在可以探讨一下
首先说什么是数据库连接池,其实就是一种对象池,不过在任何编程语言中,关于数据库的连接对象,都是重量级的对象,所以需要避免频繁的创建和销毁,需要在使用完成后将其归还给连接池
对于执行一个sql的过程,可以分为
1.获取一个数据库的连接
2.创建statement
3.执行sql
4.得到ResultSet
5.释放ResultSet
6.释放statement
7.释放数据库连接池
一个简单的数据库执行流程如下
//数据库连接池配置
HikariConfig config = new HikariConfig(); config.setMinimumIdle(1); config.setMaximumPoolSize(2); config.setConnectionTestQuery(“SELECT 1”); config.setDataSourceClassName(“org.h2.jdbcx.JdbcDataSource”); config.addDataSourceProperty(“url”, “jdbc:h2:mem:test”); // 创建数据源 DataSource ds = new HikariDataSource(config); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 获取数据库连接 conn = ds.getConnection(); // 创建Statement stmt = conn.createStatement(); // 执行SQL rs = stmt.executeQuery(“select * from abc”); // 获取结果 while (rs.next()) { int id = rs.getInt(1); …… } } catch(Exception e) { e.printStackTrace(); } finally { //关闭ResultSet close(rs); //关闭Statement close(stmt); //关闭Connection close(conn); } //关闭资源 void close(AutoCloseable rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } |
依次的打开依次的关闭,这就是一个数据库的连接使用过程
那么了解完了数据库的使用过程,我们看下为什么HiKariCP为什么这么快
从底层来说,HiKariCP从字节码的程度上去优化了执行效率,但是这个并没有开源
从数据结构来说,HiKariCp提供了两个自定义的数据结构
1.FastList
2.ConcurrentBag
1.FastList
对于可能用户忘记关闭Result和Statement两种问题,进行了相对应的优化
将Statement放入了Connection中的一个队列,在关闭Connection时候顺便关闭了所有的Statement
但是使用ArrayList直接去存储Statement还是太慢,于是HiKariCp自定义了一个更加适合关闭的List
一般来说List都是正序查找,然后逆序删除,但并不适用这个场景,于是,逆向查找,逆向删除就更好了
毕竟Connection一般是依次创建6个Statement,分别是S1,S2,S3,S4,S5,S6
按照正常的编码习惯,关闭是逆向的,逆向删除但是查找是顺序的,导致查找顺序变慢了,如果变为逆向查找,逆向删除,必然就快多了
2.使用ConcurrentBag进行连接分配
可以分为两个队列,一个用来保存被使用的队列Busy,一个用于保存空闲的队列 idle
连接就是在两者之间传递连接
但是并没有直接使用Java提供的阻塞队列,使用了阻塞队列是用锁实现的,会在高并发情况下影响性能
那么我们研究下其如何具体实现的
其中关键的属性有4个,分别是存储所有的数据的共享队列sharedList,线程的本地存储 threadList,等待数据连接的线程数 waiters
分配连接的工具 handoffQueue
在其中handoffQueue使用的Java的Synchronous Queue,用于传递数据
//用于存储所有的数据库连接
CopyOnWriteArrayList<T> sharedList; //线程本地存储中的数据库连接 ThreadLocal<List<Object>> threadList; //等待数据库连接的线程数 AtomicInteger waiters; //分配数据库连接的工具 SynchronousQueue<T> handoffQueue; |
创建连接的过程很简单,就是在创建完成后加入shardList中,让handoffQueue进行分配
获取连接的方式就是调用borrow()方法,其主要的查询逻辑为
首先查看本地存储中是否有空闲连接,有就直接返回
然后查看线程本地存储中是否有空闲连接
如果共享队列中没有空闲连接,没有直接等待
注意,可能出现一个问题,就是从共享队列中到本地存储的过程中,可能在加入时候被其他线程抢走了,让ThreadLocal这个应该是本地线程安全的变得有所共享了,不再安全了,故采用了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();
}
}
释放连接的过程也很简单,就是将连接状态从使用改为未使用中,然后查看是否有等待线程,有直接分配给等待线程,没有直接保存在本地存储中
//释放连接
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);
}
}
最后是添加连接的方法
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()”);
}
//放入共享队列
sharedList.add(bagEntry);//新添加的资源优先放入CopyOnWriteArrayList
// spin until a thread takes it or none are waiting
// 当有等待资源的线程时,将资源交到某个等待线程后才返回(SynchronousQueue)
while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
yield();
}
}
其中的FastList和ConcurrentBag你说很精妙吧,也挺好的,但没那么复杂,其实编程就是这样,使用合适特定场景的数据结构才是最重要的
做好对症下药