2、hikariCP原理分析
2.1 HikariCP为什么这么快
1、优化并精简字节码:HikariCP利用了一个第三方的Java字节码修改类库Javassist来生成委托实现动态代理。之所以使用Javassist生成动态代理,是因为其速度更快,相比于JDK Proxy生成的字节码更少,精简了很多不必要的字节码。
2. 优化代理和拦截器:减少代码,例如HikariCP的Statement proxy只有100行代码,只有BoneCP的十分之一;
3、ConcurrentBag:更好的并发集合类实现
ConcurrentBag内部同时使用了ThreadLocal和CopyOnWriteArrayList来存储元素,其中CopyOnWriteArrayList是线程共享的。ConcurrentBag采用了queue-stealing的机制获取元素:首先尝试从ThreadLocal中获取属于当前线程的元素来避免锁竞争,如果没有可用元素则扫描公共集合、再次从共享的CopyOnWriteArrayList中获取
4、使用FastList替代ArrayList:自定义数组类型(FastList)代替ArrayList:避免每次get()调用都要进行range check,避免调用remove()时的从头到尾的扫描
5. 其他针对BoneCP缺陷的优化,比如对于耗时超过一个CPU时间片的方法调用的研究。
2.2连接池获取
- 1、采用 threadLocal 缓存同一个线程使用过的db连接,并且是从最近一个开始遍历int i = list.size() - 1, 这样同一个java线程多次获取db连接的时候就能快速获取到连接,而且获取到最鲜活的,还可避免 HikariPool#getConnection 的连接可用性的检查
- 2、ConcurrentBag 底层 sharedList 用的 copyOnWriteArrayList,所以读和写之间就不需要加锁了
- 3、ConcurrentBag 申请连接borrow 和归还连接 requite 只是进行 cas 状态变更的无锁操作,并不会从 copyOnWriteArrayList 移除元素
- 4、单线程的线程池:新建和关闭连接的线程池都是单线程的,也就避免了线程间的协调开销
- 5、handoffQueue: SynchronousQueue 当申请连接 borrow() 的时候,如果空闲连接不够用,线程就会阻塞在 handoffQueue#poll ; 在连接新增 add() 或归还 requite() 时,如果发现有线程在等待中则会把这个连接交给 handoffQueue ,这样等待中的线程直接能直接获取到,而不用再遍历 sharedList 同时也保障了公平性 先到先得
- 6、minimumIdle 默认等于 maximumPoolSize, 尽量减少在申请连接的时候还需要创建连接的开销
public Connection getConnection(final long hardTimeout) throws SQLException
{
suspendResumeLock.acquire();
final long startTime = currentTime();
try {
long timeout = hardTimeout;
PoolEntry poolEntry = null;
try {
do {
poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
if (poolEntry == null) {
break; // We timed out... break and throw exception
}
final long now = currentTime();
//1、是否标志为抛弃
//2、当前连接对象距离上次被使用时间是否超过500(默认500 可配置)
//3、isConnectionAlive
if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > ALIVE_BYPASS_WINDOW_MS && !isConnectionAlive(poolEntry.connection))) {
closeConnection(poolEntry, "(connection is evicted or dead)"); // Throw away the dead connection (passed max age or failed alive test)
//减去本次耗时,重试
timeout = hardTimeout - elapsedMillis(startTime);
}
else {
metricsTracker.recordBorrowStats(poolEntry, startTime);
return poolEntry.createProxyConnection(leakTask.schedule(poolEntry), now);
}
} while (timeout > 0L);
}
catch (InterruptedException e) {
if (poolEntry != null) {
poolEntry.recycle(startTime);
}
Thread.currentThread().interrupt();
throw new SQLException(poolName + " - Interrupted during connection acquisition", e);
}
}
finally {
suspendResumeLock.release();
}
throw createTimeoutException(startTime);
}
2.3 链接池初始化
初始化入口:com.zaxxer.hikari.pool.HikariPool#HikariPool
该流程用于初始化整个连接池,这个流程会给连接池内所有的属性做初始化的工作,其中比较主要的几个流程上图已经指出,简单概括如下:
- 利用config初始化各种连接池属性,并且产生一个用于生产物理连接的数据源DriverDataSource
- 初始化存放连接对象的核心类ConcurrentBag
- 初始化一个延时任务线程池类型的对象houseKeepingExecutorService,用于后续执行一些延时/定时类任务,比如连接泄漏检查延时任务,除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的
- 预热连接池,HikariCP会在该流程的checkFailFast里初始化好一个连接对象放进池子内,当然触发该流程得保证initializationTimeout > 0(默认值1),这个配置属性表示留给预热操作的时间,默认值1在预热失败时不会发生重试
- 初始化一个线程池对象addConnectionExecutor,用于后续扩充连接对象
public HikariPool(final HikariConfig config)
{
//利用config初始化各种连接池属性,并且产生一个用于生产物理连接的数据源DriverDataSource
super(config);
//初始化存放连接对象的核心类ConcurrentBag
this.connectionBag = new ConcurrentBag<>(this);
//初始化型号量,用于控制获取连接的速度,isAllowPoolSuspension=true生效
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
//初始化一个延时任务线程池类型的对象houseKeepingExecutorService,用于后续执行一些延时/定时类任务,
// 比如连接泄漏检查延时任务,除此之外maxLifeTime后主动回收关闭连接也是交由该对象来执行的
this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
//预热连接池,HikariCP会在该流程的checkFailFast里初始化好一个连接对象放进池子内,
// 当然触发该流程得保证initializationTimeout > 0(默认值1),
// 这个配置属性表示留给预热操作的时间,默认值1在预热失败时不会发生重试
checkFailFast();
if (config.getMetricsTrackerFactory() != null) {
setMetricsTrackerFactory(config.getMetricsTrackerFactory());
}
else {
//注入MetricRegistry来实现对连接池指标的收集。这样我们可以较为方便的监控连接池的运行状态
setMetricRegistry(config.getMetricRegistry());
}
setHealthCheckRegistry(config.getHealthCheckRegistry());
registerMBeans(this);
ThreadFactory threadFactory = config.getThreadFactory();
LinkedBlockingQueue<Runnable> addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
//初始化一个线程池对象addConnectionExecutor,用于后续扩充连接对象
this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
//初始化一个线程池对象closeConnectionExecutor,用于关闭一些连接对象
this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
//初始化连接泄露告警器
this.leakTask = new ProxyLeakTask(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
//初始化扩/缩容定时器
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);
}
2.3 checkFailFast
与Druid通过initialSize控制预热连接对象数不一样的是,HikariCP仅预热进池一个连接对象。
2.4 ConcurrentBag
ConcurrentBag为整个链接池中最为核心的类,这个类用来存放最终的PoolEntry(原生链接的包装)类型的连接对象,提供了基本的增删查的功能,被HikariPool持有,上面那么多的操作,几乎都是在HikariPool中完成的,HikariPool用来管理实际的连接生产动作和回收动作,实际操作的是ConcurrentBag类。ConcurrentBag是个并发管理类,性能非常好。
2.4.1 状态转换
public static interface IConcurrentBagEntry
{
// // 闲置
int STATE_NOT_IN_USE = 0;
// 使用中
int STATE_IN_USE = 1;
// 已废弃
int STATE_REMOVED = -1;
// 标记保留,介于闲置和废弃之间的中间状态,主要由缩容那里触发修改
int STATE_RESERVED = -2;
//// 利用cas修改连接对象的状态值
boolean compareAndSet(int expectState, int newState);
void setState(int newState);
int getState();
}
2.4.2 成员属性
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
// sharedList保存了所有的连接,CopyOnWriteArrayList
private final CopyOnWriteArrayList<T> sharedList;
// 是否弱引用,默认false
private final boolean weakThreadLocals;
// 线程级的缓存,从sharedList拿到的连接对象,使用后会被缓存进当前线程内,borrow时会先从缓存中拿,从而达到池内无锁实现
private final ThreadLocal<List<Object>> threadList;
// 内部接口,HikariPool实现了该接口,主要用于ConcurrentBag主动通知HikariPool触发添加连接对象的异步操作
private final IBagStateListener listener;
// 当前等待获取连接的线程数
private final AtomicInteger waiters;
private volatile boolean closed;
// 这是个即产即销的队列,用于在连接不够用时,及时获取到add方法里新创建的连接对象
private final SynchronousQueue<T> handoffQueue;
2.4.3 borrow()获取链接
com.zaxxer.hikari.util.ConcurrentBag#borrow
- 先从ThreadLocal中获取链接
- ThreadLocal中获取不到链接,那么从sharedList中获取
- sharedList中还获取不到,触发添加链接操作
- 在timeout时间内循环,从handoffQueue中poll链接
- 超出了timeout时间,返回null
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// Try the thread-local list first
// 首先从当前线程的缓存里拿到之前被缓存进来的连接对象集合
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;
// 获取到对象后,通过cas尝试把其状态从STATE_NOT_IN_USE 变为 STATE_IN_USE,
// 注意,这里如果其他线程也在使用这个连接对象,并且成功修改属性,那么当前线程的cas会失败,那么就会继续循环尝试获取下一个连接对象
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
// Otherwise, scan the shared list ... then poll the handoff queue
// 如果缓存内找不到一个可用的连接对象,则认为需要“回源”,waiters+1
final int waiting = waiters.incrementAndGet();
try {
for (T bagEntry : sharedList) {
// 循环sharedList,尝试把连接状态值从STATE_NOT_IN_USE 变为 STATE_IN_USE
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
// If we may have stolen another waiter's connection, request another bag add.
// 阻塞线程数大于1时,需要触发HikariPool的addBagItem方法来进行添加连接入池
if (waiting > 1) {
listener.addBagItem(waiting - 1);
}
// cas设置成功,跟上面的逻辑一样,表示当前线程绕过其他线程干扰,成功获取到该连接对象,直接返回
return bagEntry;
}
}
// 走到这里说明不光线程缓存里的列表竞争不到连接对象,连sharedList里也找不到可用的连接,这时则认为需要通知HikariPool,该触发添加连接操作了
listener.addBagItem(waiting);
// 利用timeout控制获取时间
timeout = timeUnit.toNanos(timeout);
do {
final long start = currentTime();
// 尝试从handoffQueue队列里获取最新被加进来的连接对象(一般新入的连接对象除了加进sharedList之外,还会被offer进该队列)
final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
// 如果超出指定时间后仍然没有获取到可用的连接对象,或者获取到对象后通过cas设置成功,这两种情况都不需要重试,直接返回对象
if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
// 走到这里说明从队列内获取到了连接对象,但是cas设置失败,说明又该对象又被其他线程率先拿去用了,若时间还够,则再次尝试获取
timeout -= elapsedNanos(start);
} while (timeout > 10_000);
return null;
}
finally {
// 这一步出去后,HikariPool收到borrow的结果,算是走出阻塞,所以waiters-1
waiters.decrementAndGet();
}
}
2.5 FastList
FastList是一个List接口的精简实现,只实现了接口中必要的几个方法。JDK ArrayList每次调用get()方法时都会进行rangeCheck检查索引是否越界,FastList的实现中去除了这一检查,只要保证索引合法那么rangeCheck就成为了不必要的计算开销(当然开销极小)。此外,HikariCP使用List来保存打开的Statement,当Statement关闭或Connection关闭时需要将对应的Statement从List中移除。通常情况下,同一个Connection创建了多个Statement时,后打开的Statement会先关闭。ArrayList的remove(Object)方法是从头开始遍历数组,而FastList是从数组的尾部开始遍历,因此更为高效。
java.util.ArrayList
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
com.zaxxer.hikari.util.FastList
public final class FastList<T> implements List<T>, RandomAccess, Serializable
我们先看一下java.util.ArrayList的get方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
我们可以看到每次get的时候都会进行rangeCheck
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
我们对比一下com.zaxxer.hikari.util.FastList的get方法,可以看到取消了rangeCheck,一定程度下追求了极致
@Override
public T get(int index)
{
return elementData[index];
}
我们再来看一下ArrayList的remove(Object)方法
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
再对比看一下FastList的remove(Object方法)
@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;
}
2.6 HouseKeeper
HouseKeeper主要做了三件事
- 检测时钟回拨
- 关闭空闲时间过长的链接
- 添加链接
hikariCP在解决系统时钟被回拨时做出的一种措施,通过流程图可以看到,它是直接把池子里所有的连接对象取出来挨个的标记成废弃,并且尝试把状态值修改为STATE_RESERVED,如果系统时钟没有发生改变,会把当前池内所有处于闲置状态(STATE_NOT_IN_USE)的连接拿出来,然后计算需要检查的范围,然后循环着修改连接的状态
private final class HouseKeeper implements Runnable
{
private volatile long previous = plusMillis(currentTime(), -HOUSEKEEPING_PERIOD_MS);
@Override
public void run()
{
try {
// refresh timeouts in case they changed via MBean
connectionTimeout = config.getConnectionTimeout();
validationTimeout = config.getValidationTimeout();
leakTask.updateLeakDetectionThreshold(config.getLeakDetectionThreshold());
final long idleTimeout = config.getIdleTimeout();
final long now = currentTime();
// Detect retrograde time, allowing +128ms as per NTP spec.
if (plusMillis(now, 128) < plusMillis(previous, HOUSEKEEPING_PERIOD_MS)) {
LOGGER.warn("{} - Retrograde clock change detected (housekeeper delta={}), soft-evicting connections from pool.",
poolName, elapsedDisplayString(previous, now));
previous = now;
softEvictConnections();
fillPool();
return;
}
else if (now > plusMillis(previous, (3 * HOUSEKEEPING_PERIOD_MS) / 2)) {
// No point evicting for forward clock motion, this merely accelerates connection retirement anyway
LOGGER.warn("{} - Thread starvation or clock leap detected (housekeeper delta={}).", poolName, elapsedDisplayString(previous, now));
}
previous = now;
String afterPrefix = "Pool ";
if (idleTimeout > 0L && config.getMinimumIdle() < config.getMaximumPoolSize()) {
logPoolState("Before cleanup ");
afterPrefix = "After cleanup ";
connectionBag
.values(STATE_NOT_IN_USE)
.stream()
.sorted(LASTACCESS_REVERSE_COMPARABLE)
.skip(config.getMinimumIdle())
.filter(p -> elapsedMillis(p.lastAccessed, now) > idleTimeout)
.filter(connectionBag::reserve)
.forEachOrdered(p -> closeConnection(p, "(connection has passed idleTimeout)"));
}
logPoolState(afterPrefix);
fillPool(); // Try to maintain minimum connections
}
catch (Exception e) {
LOGGER.error("Unexpected exception in housekeeping task", e);
}
}
}
参考文档
git源码:
alibaba/druid pool analysis · Issue #232 · brettwooldridge/HikariCP · GitHub
https://github.com/alibaba/druid
技术博客