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你说很精妙吧,也挺好的,但没那么复杂,其实编程就是这样,使用合适特定场景的数据结构才是最重要的

做好对症下药

发表评论

邮箱地址不会被公开。 必填项已用*标注