Jedis源碼閱讀之連接池

1. 什么是連接池

一般在程序中如果要和其他的系統(tǒng)創(chuàng)建連接進行交互并且連接的創(chuàng)建代價比較"昂貴"就需要用到連接池. 那怎么樣才算是昂貴呢? 簡單說來就是創(chuàng)建連接的時間接近甚至超過交互的時間. 所以連接池就是一個創(chuàng)建連接管理連接, 對連接進行緩存的技術(shù). 最常見的連接池就是數(shù)據(jù)庫連接池. 更加具體的關(guān)于連接池的介紹, 請移步連接池

2. Jedis的連接池

既然連接池的作用就是管理連接, 那Jedis的連接池也不例外, 它的作用就是緩存Jedisredis 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中刪除, 也就是說allObjectsidleObjects中的連接實際上是指向同一個對象實例的. 為什么要這么做, 而不是直接刪除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)自己的連接池也方便許多
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狱庇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矾踱,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機济炎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辐真,“玉大人须尚,你說我怎么就攤上這事∈淘郏” “怎么了耐床?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長楔脯。 經(jīng)常有香客問我撩轰,道長,這世上最難降的妖魔是什么昧廷? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任堪嫂,我火速辦了婚禮,結(jié)果婚禮上木柬,老公的妹妹穿的比我還像新娘皆串。我一直安慰自己,他們只是感情好眉枕,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布恶复。 她就那樣靜靜地躺著怜森,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谤牡。 梳的紋絲不亂的頭發(fā)上副硅,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音拓哟,去河邊找鬼想许。 笑死,一個胖子當著我的面吹牛断序,可吹牛的內(nèi)容都是我干的流纹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼违诗,長吁一口氣:“原來是場噩夢啊……” “哼漱凝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诸迟,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤茸炒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后阵苇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體壁公,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年绅项,在試婚紗的時候發(fā)現(xiàn)自己被綠了紊册。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡快耿,死狀恐怖囊陡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掀亥,我是刑警寧澤撞反,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站搪花,受9級特大地震影響遏片,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撮竿,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一丁稀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧倚聚,春花似錦、人聲如沸凿可。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惨驶,卻和暖如春白热,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粗卜。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工屋确, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人续扔。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓攻臀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親纱昧。 傳聞我的和親對象是個殘疾皇子刨啸,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)识脆,斷路器设联,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 1 Redis介紹1.1 什么是NoSql為了解決高并發(fā)离例、高可擴展、高可用悉稠、大數(shù)據(jù)存儲問題而產(chǎn)生的數(shù)據(jù)庫解決方...
    克魯?shù)吕?/span>閱讀 5,266評論 0 36
  • 1.1 資料 宫蛆,最好的入門小冊子,可以先于一切文檔之前看偎球,免費洒扎。 作者Antirez的博客,Antirez維護的R...
    JefferyLcm閱讀 17,032評論 1 51
  • 心的尖端劃過什么 手的風與眼的黑 誰說的傳說 我會在時光中愛你 愿這隔了千年的癡情 在透過你眼角看到的明月中流連 ...
    Longczx閱讀 213評論 2 3