前言
主要是突然心血来潮想看看jedis连接池的实现策略,之前看过druid实现,印象中整体实现思路都差不多。这里Jedis源码是2.8.1版本。
为了便于理解,看之前可以先想想池化技术实现通常有什么特点(我是看内容反推结果)。
- 可重复使用:放到池子里面的东西需要可以重复使用,这样才有意义。
- 创建需要一定开销:创建如果开销很小,大部分情况下使用池化意义就不是很大。
- 资源有限:不能创建太多
- 配套监控:为了可以知道池子使用的情况,还需要一些监控数据
此外有些资源还要考虑可用性,而保证池内东西可用,就需要去检查。我们可以在borrow或return的时候看看这个东西还能用不;我们还可以派个人周期检查池子里面东西的可用性。对于资源不可用的情况,我们需要将其销毁。当然有些资源不用考虑,比如说内存块,所以需要实现的时候是可选的。所以需要的操作有:创建,检测可用性,销毁,取,释放等基础操作。
所以简单来说,我们因为不想重复创建一样的东西,而且创建的开销还很大,创建多了还会降低效率,我们就想着囤积以便可以重复利用。
注:本质上还是属于源码笔记,有不清楚的可以提意见,目前我只是本着想到什么就整理什么的思路,发出来只是希望给需要的人一点帮助。
通用对象池
我本来一直以为jedis是自己实现连接池,但是其实并不是:
里面居然用的是一个通用的实现,所有的操作最后都会委托它来操作。这个连接池是apache里面的一个实现:
org.apache.commons.pool2.impl.GenericObjectPool
而且对象名称也很有意思“通用对象池”。
看源码第一步就是整理类图,注意到BaseGenericObjectPool,它承担了GenericObjectPoolMXBean接口方法,都是一些配置的get方法,至于这个接口名MX,和JMX有关系,后面再看。
我们之前说的池所需的操作,在接口(ObjectPool)都有定义,实现就在GenericObjectPool中,所以实际上我们只需要关心GenericObjectPool就行了。
此外,GenericObjectPool中持有的PooledObjectFactory它具有创建,校验,销毁等功能,看到下面,在对象池的主要操作上都有它的影子:
还有一个PoolObject池对象,这是一个包装类,里面会额外附带有些统计信息,还有状态信息(便于监控)。jedis在使用的时候,直接就是:
return new DefaultPooledObject<Jedis>(jedis);
到这里,我们对这个通用对象池有个总体的框架:
- 池对象工厂:它会制造,销毁,校验池对象
- 池对象:持有需要复用的对象,本身具有一系列存活状态(失效,空闲,使用,正在检查等),具有一些统计信息。
- 对象池:存储空闲对象和所有对象,具有获取,释放,关闭等操作,暴露一些统计接口
- 驱逐线程:定时移除和测试空闲对象可用性的逻辑在这里(可选启动,策略可以自定义)
简单来说,我们只需要实现池对象工厂里面的接口就可以使用这个对象池了,这个就是通用二字的含义,我定义了操作的步骤,你实现具体的操作。除了这个还暴露了一些自定义的接口,比如说可以自定义驱逐策略。下面就是介绍核心的操作,便于理解代码。
租借操作
borrow的目的就是为了拿到可用的对象,可用的对象存储在空闲队列里面,所以我们可以从队列里面拿,也可以让工厂给新的对象。
在此基础上,提供了额外的功能:
- 我希望可以等待一段时间,或者阻塞等待;
- 我希望在借之前可以自动检查一下可用性怎么样;
- 我希望在借之前可以检查一下是不是有长时间不归还,或者长时间不使用的对象,废弃(removeAbandoned)。
removeAbandoned这个操作在驱逐线程里面也有,只不过2个地方控制的开关不一样。注意的下面这个判断,推测应该是发现空闲的很少,使用的很多要进行检查:
AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) &&
(getNumActive() > getMaxTotal() - 3) ) {
removeAbandoned(ac);
}
判断对象是否可以分配主要看对象状态是不是空闲状态,所以也存在一种情况:对象正在驱逐检测,这时候状态并非是空闲,而是EVICTION:
@Override
public synchronized boolean allocate() {
if (state == PooledObjectState.IDLE) {
state = PooledObjectState.ALLOCATED;
//...
return true;
} else if (state == PooledObjectState.EVICTION) {
// 只要不被驱逐,最后需要放到队头
state = PooledObjectState.EVICTION_RETURN_TO_HEAD;
return false;
}
// TODO if validating and testOnBorrow == true then pre-allocate for
// performance
return false;
}
在驱逐检测完毕后,如果没有驱逐就会重置状态:
state = PooledObjectState.IDLE;
if (!idleQueue.offerFirst(this)) {
// TODO - Should never happen
}
归还操作
驱逐检测
整个检测是单线程工作,有个Timer去执行。整个流程主要由两个方法构成:evict()和ensureMinIdle()。第一个方法作用是检测,第二个方法是保证空闲数量为minIdle。
外层是一个循环,每次需要检测多少个:
for (int i = 0, m = getNumTests(); i < m; i++) {}
看注释就行了,没啥好说的:
//非空闲状态不作为计数,空闲状态修改状态为EVICTION
if (!underTest.startEvictionTest()) {
// Object was borrowed in another thread
// Don't count this as an eviction test so reduce i;
i--;
continue;
}
if (evictionPolicy.evict(evictionConfig, underTest,
idleObjects.size())) {
//销毁并计数
} else {
if (testWhileIdle) {
boolean active = false;
//...active = activateObject
if (active) {
if (!factory.validateObject(underTest)) {
//销毁并计数
} else {
//passivateObject
}
}
}
//改回状态
if (!underTest.endEvictionTest(idleObjects)) {
// TODO - May need to add code here once additional
// states are used
}
}
void ensureMinIdle() throws Exception {
//minIdle参数<1不处理
//创建
while (idleObjects.size() < minIdleSave) {
PooledObject<T> p = create();
if (p == null) {
// Can't create objects, no reason to think another call to
// create will work. Give up.
break;
}
if (getLifo()) {
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
}
}
池对象状态变化图
分状态是为了避免管理上的冲突,比如说我要拿对象的时候,这个对象不能被其他人使用,例如Evict线程正在使用,再比如已经被废弃了。而且状态转换也会涉及并发问题。在实际源码中,加锁主要针对对象,锁粒度就是池对象级别。
看到空闲状态到已分配状态:
public synchronized boolean allocate() {
//只有空闲状态才会进行转换
if (state == PooledObjectState.IDLE) {
state = PooledObjectState.ALLOCATED;
lastBorrowTime = System.currentTimeMillis();
lastUseTime = lastBorrowTime;
//...
return true;
} else if (state == PooledObjectState.EVICTION) {
// TODO Allocate anyway and ignore eviction test
state = PooledObjectState.EVICTION_RETURN_TO_HEAD;
return false;
}
// TODO if validating and testOnBorrow == true then pre-allocate for
// performance
return false;
}
Jedis连接池
根据上面的分析,可知池的核心就是工厂。在Jedis里面,池对象就是Jedis,其实就是客户端,比如说我们用redis-cli启动也是一个客户端,可以输入命令操作。
redis-cli和redis-server交互是基于tcp,Jedis也是一样:
- 创建:创建tcp连接(授权,给自己标识名字,选择数据库)
- 销毁:断开tcp连接
- 测试:socket发送pong命令获取服务端回应
- activate:重新选库
其实在这里池的重点就是上面的通用对象池,反而Jedis连接池没东西可说。这里就贴贴源码就完事了。这里找到一篇充电宝的demo感觉不错:Java对象池pool2分析PooledObjectFactory。
@Override
public boolean validateObject(PooledObject<Jedis> pooledJedis) {
final BinaryJedis jedis = pooledJedis.getObject();
try {
HostAndPort hostAndPort = this.hostAndPort.get();
String connectionHost = jedis.getClient().getHost();
int connectionPort = jedis.getClient().getPort();
//发送pong命令
return hostAndPort.getHost().equals(connectionHost)
&& hostAndPort.getPort() == connectionPort && jedis.isConnected()
&& jedis.ping().equals("PONG");
} catch (final Exception e) {
return false;
}
}