1. 什么是連接池
一般在程序中如果要和其他的系統(tǒng)創(chuàng)建連接進行交互并且連接的創(chuàng)建代價比較"昂貴"就需要用到連接池. 那怎么樣才算是昂貴呢? 簡單說來就是創(chuàng)建連接的時間接近甚至超過交互的時間. 所以連接池就是一個創(chuàng)建連接管理連接, 對連接進行緩存的技術(shù). 最常見的連接池就是數(shù)據(jù)庫連接池. 更加具體的關(guān)于連接池的介紹, 請移步連接池
2. Jedis的連接池
既然連接池的作用就是管理連接, 那Jedis
的連接池也不例外, 它的作用就是緩存Jedis
和redis server
之間的連接. Jedis
連接池的作用具體來說分為以下幾個部分:
2.1 創(chuàng)建保存連接的容器
當初始化一個JedisPool
的時候會創(chuàng)建一個LinkedBlockingDeque<PooledObject<T>> idleObjects
隊列, 這個隊列用于存放已經(jīng)和redis server
建立連接********并且已經(jīng)使用過********的連接對象(實際上存放的是DefaultPooledObject<Jedis>
對象, 后面會看到).
同時還會創(chuàng)建一個Map<IdentityWrapper<T>, PooledObject<T>> allObjects
對象, 這個Map
用于沒有可用的連接時新創(chuàng)建出來的連接.
2.2 發(fā)起請求獲取連接
當發(fā)起請求從連接池中獲取一個連接的時候, 連接池會先從idleObjects
隊列中獲取連接, 如果獲取不到則開始創(chuàng)建一個新的連接, 創(chuàng)建新的連接是首先判斷當前已經(jīng)存在的連接是否已經(jīng)大于連接池可容納的連接數(shù)量, 如果是則不予創(chuàng)建, 以阻塞等的方式從idleObjects
中等待獲取可用連接(默認是阻塞不超時等待即等待直到有可用的連接, 但是也可以配置超時時間). 如果可以創(chuàng)建, 則創(chuàng)建一個新的連接放入到allObjects
對象中, 同時將連接返回.
2.3 連接使用完畢關(guān)閉連接
當使用完連接調(diào)用連接關(guān)閉的時候, 連接池會將歸還連接從allObjects
中拿出來放入到idleObjects
中, 所以下一次再獲取連接將從idleObjects
直接獲取.
但是這里要特別注意, 從allObjects
中拿出來放入到idleObjects
中時, 并沒有將連接從allObjects
中刪除, 也就是說allObjects
和idleObjects
中的連接實際上是指向同一個對象實例的. 為什么要這么做, 而不是直接刪除allObjects
中的連接呢? 因為JedisPool
會在指定的時間內(nèi)對連接池中空閑對象進行刪除, 這樣可以減少資源的占用, 這個是JedisPool
的單獨線程自動完成的操作. 所以說, 如果有個連接創(chuàng)建出來長時間沒有使用是會被自動銷毀的, 而不是一直連接著占用資源.
3. 源碼閱讀
下面針對于上面提到的Jedis Pool
的關(guān)鍵點來看看具體的代碼實現(xiàn)
3.1 連接池的創(chuàng)建
JedisPool pool = new JedisPool(new JedisPoolConfig(), hnp.getHost(), hnp.getPort(), 2000);
調(diào)用上面的代碼就可以創(chuàng)建一個連接池了, 其中最關(guān)鍵的部分就是JedisPoolConfig
對象的創(chuàng)建
// JedisPoolConfig.java
public class JedisPoolConfig extends GenericObjectPoolConfig {
public JedisPoolConfig() {
...
// 連接空閑的最小時間, 達到此值后空閑連接將會被移除. 負值(-1)表示不移除
setMinEvictableIdleTimeMillis(60000);
// "空閑鏈接"檢測線程, 檢測的周期, 毫秒數(shù). 如果為負值, 表示不運行“檢測線程”. 默認為-1
setTimeBetweenEvictionRunsMillis(1000);
...
}
}
在創(chuàng)建連接池的同時會創(chuàng)建idleObjects
對象
// GenericObjectPool.java
public GenericObjectPool(PooledObjectFactory<T> factory,
GenericObjectPoolConfig config) {
...other code...
// 創(chuàng)建idleObjects對象
idleObjects = new LinkedBlockingDeque<PooledObject<T>>(config.getFairness());
setConfig(config);
startEvictor(getTimeBetweenEvictionRunsMillis());
}
在上面的代碼中創(chuàng)建了idleObjects
對象, 但是還有一句很關(guān)鍵的代碼startEvictor(getTimeBetweenEvictionRunsMillis());
.
沒錯,這就是啟動自動回收空閑連接的代碼!
// BaseGenericObjectPool.java
final void startEvictor(long delay) {
synchronized (evictionLock) {
if (null != evictor) {
EvictionTimer.cancel(evictor);
evictor = null;
evictionIterator = null;
}
if (delay > 0) {
evictor = new Evictor();
EvictionTimer.schedule(evictor, delay, delay);
}
}
}
至于保存剛創(chuàng)建出來的連接的allObjects
對象是GenericObjectPool
的成員變量
private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects =
new ConcurrentHashMap<IdentityWrapper<T>, PooledObject<T>>();
3.2 獲取一個連接
Jedis jedis = pool.getResource();
調(diào)用上面的代碼就可以獲取一個連接了
獲取連接最關(guān)鍵的就是borrowObject
// GenericObjectPool.java
public T borrowObject(long borrowMaxWaitMillis) throws Exception {
assertOpen();
...other code...
PooledObject<T> p = null;
// 沒有空閑連接的時候是否阻塞的等待連接
boolean blockWhenExhausted = getBlockWhenExhausted();
boolean create;
long waitTime = System.currentTimeMillis();
while (p == null) {
create = false;
// 沒有空閑連接時等待
if (blockWhenExhausted) {
// 先從idleObjects隊列中獲取
p = idleObjects.pollFirst();
if (p == null) {
// 如果隊列中沒有空閑的連接, 則創(chuàng)建一個連接
p = create();
if (p != null) {
create = true;
}
}
// 如果連接創(chuàng)建失敗, 則繼續(xù)從idleObjects中阻塞的獲取連接
if (p == null) {
if (borrowMaxWaitMillis < 0) {
// 無限制的等待, 不會超時
p = idleObjects.takeFirst();
} else {
// 有超時時間的等待
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
if (!p.allocate()) {
p = null;
}
} else { // 沒有空閑連接時直接拋出異常
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) {
create = true;
}
}
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
if (!p.allocate()) {
p = null;
}
}
...other code...
}
updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
// 返回連接
return p.getObject();
}
上面是獲取連接時的一個完整的流程, 包括有空閑連接時直接返回, 沒有空閑連接時創(chuàng)建空閑連接, 連接創(chuàng)建失敗后是繼續(xù)阻塞等待(包括是否超時)還是直接拋出異常. 下面看一下是如何創(chuàng)建一個連接的
// GenericObjectPool.java
private PooledObject<T> create() throws Exception {
// 判斷當前已經(jīng)創(chuàng)建的連接是否已經(jīng)超過設置的最大連接數(shù)(默認是8)
int localMaxTotal = getMaxTotal();
long newCreateCount = createCount.incrementAndGet();
if (localMaxTotal > -1 && newCreateCount > localMaxTotal ||
newCreateCount > Integer.MAX_VALUE) {
createCount.decrementAndGet();
return null;
}
final PooledObject<T> p;
try {
// 創(chuàng)建一個到redis server的連接
p = factory.makeObject();
} catch (Exception e) {
createCount.decrementAndGet();
throw e;
}
...other code...
createdCount.incrementAndGet();
// 將新創(chuàng)建的連接放入到allObjects中
allObjects.put(new IdentityWrapper<T>(p.getObject()), p);
// 返回新創(chuàng)建的連接
return p;
}
上面就是創(chuàng)建一個到redis server
連接的過程. 但是上面沒有深究是如何與redis server
創(chuàng)建連接的, 因為這次介紹的主題是JedisPool
, 所以客戶端與redis server
創(chuàng)建連接的具體細節(jié)會在之后的文章中介紹.
3.3 關(guān)閉一個連接
在使用完一個連接之后就要將一個連接關(guān)閉. 其實上面創(chuàng)建一個連接之后還有一個比較重要的步驟
@Override
public Jedis getResource() {
// 獲取連接
Jedis jedis = super.getResource();
// 將連接池放入到連接中, 這里這么做的目的其實就是為關(guān)閉連接的時候作準備的
jedis.setDataSource(this);
return jedis;
}
調(diào)用下面的代碼就可以關(guān)閉一個連接了
jedis.close();
我們知道連接池的作用就是為了緩存連接而生的, 所以這里的關(guān)閉連接肯定不能是直接和redis server
斷開連接, 所以讓我們看看這里的關(guān)閉連接到底是做了什么操作從而實現(xiàn)連接的復用
// Jedis.java
public void close() {
// 如果連接池存在就調(diào)用連接池的返回資源(這里的資源就是連接)的方法
if (dataSource != null) {
if (client.isBroken()) {
this.dataSource.returnBrokenResource(this);
} else {
this.dataSource.returnResource(this);
}
} else { // 如果連接池不存在就直接關(guān)閉連接
client.close();
}
}
所以當關(guān)閉一個連接的時候如果連接存在其實是將資源還給了連接池. 其中最核心的方法就是returnObject
// GenericObjectPool.java
@Override
public void returnObject(T obj) {
// 從allObjects中獲取要歸還的連接
PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
...other code...
int maxIdleSave = getMaxIdle();
// 如果idleObjects隊列中連接的數(shù)據(jù)已經(jīng)>=允許的最大連接數(shù)或者連接池已經(jīng)關(guān)閉就直接銷毀這個連接
if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
try {
destroy(p);
} catch (Exception e) {
swallowException(e);
}
} else { // 將連接放入到idleObjects隊列中, 一旦將連接放入到idleObjects中如果連接長時間不被使用就會被自動回收
if (getLifo()) { // 默認是使用last in first out機制
idleObjects.addFirst(p);
} else {
idleObjects.addLast(p);
}
if (isClosed()) {
// Pool closed while object was being added to idle objects.
// Make sure the returned object is destroyed rather than left
// in the idle object pool (which would effectively be a leak)
clear();
}
}
updateStatsReturn(activeTime);
}
4. 總結(jié)
- 使用了這么久的連接池自從看了
Jedis Pool
的源碼之后才對連接池有了一個直觀的認識, 之后可以看看數(shù)據(jù)庫的連接池, 比較一下兩個對于連接池實現(xiàn)的異同 -
Jedis
的連接池使用上是對apache common pool2
的一個實現(xiàn), 有了Jedis Pool
這個例子以后要是要實現(xiàn)自己的連接池也方便許多