物件池技術和通用實現GenericObjectPool

mushishi發表於2021-07-11

物件池技術其實蠻常見的,比如執行緒池、資料庫連線池

他們的特點是:物件建立代價較高、比較消耗資源、比較耗時;
比如 mysql資料庫連線建立就要先建立 tcp三次握手、傳送使用者名稱/密碼、進行身份校驗、許可權校驗等很多步驟才算是 db連線建立成功;要是每次使用的時候才去建立會比較影響效能,而且也不能無限制的建立太多

所以,這種物件使用完後不立即釋放資源,一般是先放到一個池子裡暫存起來,下次就能直接從池子裡拿出現成可用的物件

物件池需要具備的能力

所以,為了讓這類資源物件的使用方能夠複用資源、快速獲取可用物件,這個池子得具備的能力有哪些?

  1. 首先有個容器的資料結構,能存放多個物件,也有數量上限
  2. 維持一定數量的常駐物件,這個數量如果和 qps * rt 匹配的話,業務處理就都能直接獲取可用物件,不需要消耗物件建立的時間了
  3. 能應對突發流量
  4. 超時獲取,一定時間沒有獲取成功就丟擲異常,不卡死業務執行緒
  5. 具有活性檢測機制, 從容器拿出來的物件得是可用的

1 核心流程

1.1物件獲取流程

1.2 活性檢測

image

2 實現

為了實現前面提到的容器具備的能力,以及物件獲取流程,需要考慮幾個東西:

  1. 容器的資料結構選擇
    用 List、 Map 還是 Queue ?亦或是組合起來用?

  2. 空閒物件要不要單獨用要給集合存一份?方便判斷是否空、阻塞等待?
    比如將空閒物件,用一個blockingqueue存一下,就能利用阻塞佇列的能力實現超時等待

  3. 檢測機制

    • 在什麼時候檢測:常見的有 testOnBorrow 在申請到的時候檢測、testOnReturn在歸還的時候檢測 這兩個對效能有些影響; 單獨開個檢查執行緒,定時去掃描檢查,這個是非同步的 不會有testOnBorrow和testOnReturn的效能影響
    • 檢測哪些物件: 比如空閒超過 500ms 的物件
    • 如何檢查:這個需要根據具體物件的型別來,比如db連線的話一般是傳送 “select 1” 看是否能正常執行

3 一個通用實現 apache commons pool

通過前面的介紹,可以知道物件池技術的核心過程大同小異,可以將物件獲取流程、活性檢測機制等封裝成一個通用的工具,將物件本身的建立、活性檢測邏輯開放給具體的物件實現來完成; apache commons pool 就是這麼個工具, jedis底層的連線池就是直接用的這個

3.1 核心資料結構

  • LinkedBlockingDeque<PooledObject<T>> idleObjects 空閒物件雙向阻塞佇列
  • Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>(); 所有物件的map

apache commons pool 的容器用的 ConcurrentHashMap,並且將空閒的物件用一個雙向阻塞佇列單獨連線起來;
這樣他就能利用這個阻塞佇列本身的特性,達到阻塞獲取的邏輯,如果 idleObjects 是空的,就能 take()/poll(timeout) 阻塞在這裡,等待其他執行緒歸還物件佇列裡

3.2 核心物件定義

  • PooledObject 可池化的物件:包含真實物件、狀態扭轉及其建立時間、取出時間、空閒時間等指標資訊
  • PooledObjectFactory 物件工廠,負責物件的建立、銷燬、檢查等邏輯;它有個預設實現
    DefaultPooledObject 提供了基本的實現,一般只要繼承它重寫物件建立和驗活邏輯就可以了
  • GenericObjectPool 就是物件容器了

3.3 程式碼細節

從池子中獲取物件

T borrowObject(final long borrowMaxWaitMillis) {

    //省略一些程式碼 ...
    PooledObject<T> p = null;

    // Get local copy of current config so it is consistent for entire
    // method execution
    final boolean blockWhenExhausted = getBlockWhenExhausted();

    boolean create;
    final long waitTime = System.currentTimeMillis();

    while (p == null) {
        create = false;
        // 空閒佇列 隊首如果是空的,則建立一個新的物件 
        // 建立的邏輯裡會校驗是否超過最大連線數,然後利用 PooledObjectFactory建立物件
        p = idleObjects.pollFirst();
        if (p == null) {
            p = create();
            if (p != null) {
                create = true;
            }
        }

        // 阻塞從 idleObject 空閒阻塞佇列獲取物件
        if (blockWhenExhausted) {
            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");
            }
        } else {
            if (p == null) {
                throw new NoSuchElementException("Pool exhausted");
            }
        }

        // 狀態轉換為已分配 ALLOCATE,記錄借出時間等資訊
        if (!p.allocate()) {
            p = null;
        }

        if (p != null) {
            try {
                // 允許 PooledObjectFactory 在成功獲取到物件後做一些事,
                // 比如jedis連線池獲取到連線後會執行 select db 切換db
                factory.activateObject(p);
            } catch (final Exception e) {
                try {
                    destroy(p);
                } catch (final Exception e1) {
                    // Ignore - activation failure is more important
                }
                p = null;
                if (create) {
                    final NoSuchElementException nsee = new NoSuchElementException(
                            "Unable to activate object");
                    nsee.initCause(e);
                    throw nsee;
                }
            }
            // 如果 testOnBorrow=true, 或者 testOnCreate=true + 此次物件是新建的 
            // 則會去校驗物件的有效性 PooledObjectFactory#validateObject()
            if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                boolean validate = false;
                Throwable validationThrowable = null;
                try {
                    validate = factory.validateObject(p);
                } catch (final Throwable t) {
                    PoolUtils.checkRethrow(t);
                    validationThrowable = t;
                }
                // 如果物件有效性校驗失敗,則銷燬掉
                if (!validate) {
                    try {
                        destroy(p);
                        destroyedByBorrowValidationCount.incrementAndGet();
                    } catch (final Exception e) {
                        // Ignore - validation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to validate object");
                        nsee.initCause(validationThrowable);
                        throw nsee;
                    }
                }
            }
        }
    }

    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

    return p.getObject();
}

歸還物件

public void returnObject(final T obj) {
    // 校驗下物件是否還存在
    final PooledObject<T> p = allObjects.get(new IdentityWrapper<>(obj));

    if (p == null) {
        if (!isAbandonedConfig()) {
            throw new IllegalStateException(
                    "Returned object not currently part of this pool");
        }
        return; // Object was abandoned and removed
    }

    // 狀態標記為 “歸還中” 
    synchronized(p) {
        final PooledObjectState state = p.getState();
        if (state != PooledObjectState.ALLOCATED) {
            throw new IllegalStateException(
                    "Object has already been returned to this pool or is invalid");
        }
        p.markReturning(); // Keep from being marked abandoned
    }

    final long activeTime = p.getActiveTimeMillis();

    // 如果 testOnReturn=true,則在歸回時校驗物件是否還有效,如果無效了就銷燬掉
    if (getTestOnReturn()) {
        if (!factory.validateObject(p)) {
            try {
                destroy(p);
            } catch (final Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (final Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }
    }

    try {
        factory.passivateObject(p);
    } catch (final Exception e1) {
        swallowException(e1);
        try {
            destroy(p);
        } catch (final Exception e) {
            swallowException(e);
        }
        try {
            ensureIdle(1, false);
        } catch (final Exception e) {
            swallowException(e);
        }
        updateStatsReturn(activeTime);
        return;
    }

    if (!p.deallocate()) {
        throw new IllegalStateException(
                "Object has already been returned to this pool or is invalid");
    }

   // 如果此時物件池已經關閉了, 或者當前空閒物件數量大於maxIdle(最大空閒數量)則直接銷燬掉
    final int maxIdleSave = getMaxIdle();
    if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
        try {
            destroy(p);
        } catch (final Exception e) {
            swallowException(e);
        }
    } else {
        if (getLifo()) {
            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);
}

開啟定期檢查任務

final void startEvictor(final long delay) {
    synchronized (evictionLock) {
        // 關閉前已有的清理任務
        if (null != evictor) {
            EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
            evictor = null;
            evictionIterator = null;
        }
    
        // 間隔時間大於0的話(預設為-1),才建立定時清理任務Evictor
        // Evictor 是一個 Runable任務, 它會檢查空閒佇列裡的物件數量是否超過 maxIdle,空閒時長是否超過 minEvictableTimeMillis
        if (delay > 0) {
            evictor = new Evictor(); 
            EvictionTimer.schedule(evictor, delay, delay);
        }
    }
}

總結

apache commons pool 的物件池實現,比較通用,在效能要求不是太苛刻的情況下可以直接使用;
但是預設的物件實現在狀態扭轉等地方是用 synchronized 加鎖來處理併發的,如果對效能要求比較高的話,需要考慮自定義其他實現方式,比如用 cas + retry 或 threadlocal 等方式減少併發衝突

相關文章