[線上問題] "Redis客戶端連接數一直降不下來"的問題解決
前段時間,上線了新的 Redis緩存(Cache)服務,准備替換掉 Memcached。
為什么要將 Memcached 替換掉?
原因是 業務數據是壓縮后的列表型數據,緩存中保存最新的3000條數據。對於新數據追加操作,需要拆解成[get + unzip + append + zip + set]這5步操作。若列表長度在O(1k)級別的,其耗時至少在50ms+。而在並發環境下,這樣會存在“數據更新覆蓋問題”,因為追加操作不是原子操作。(線上也確實遇到了這個問題)
針對“追加操作不是原子操作”的問題,我們就開始調研有哪些可以解決這個問題同時又滿足業務數據類型的分布式緩存解決方案。
當前,業界常用的一些 key-value分布式緩存系統如下:
- Redis
- Memcached
- Cassandra
- Tokyo Tyrant (Tokyo Cabinet)
參考自:
- 2010年的技術架構建議 – Tim Yang
- From distributed caches to in-memory data grids
- Cassandra vs MongoDB vs CouchDB vs Redis vs Riak vs HBase vs Couchbase vs OrientDB vs Aerospike vs Hypertable vs ElasticSearch vs Accumulo vs VoltDB vs Scalaris comparison
通過對比、篩選分析,我們最終選擇了 Redis。原因有以下幾個:
- Redis 是一個 key-value 的緩存(cache)和存儲(store)系統(現在我們只用它來做緩存,目前還未當作DB用,數據存放在 Cassandra 里)
- 支持豐富的數據結構,List 就專門用於存儲列表型數據,默認按操作時間排序。Sorted Set 可以按分數排序元素,分數是一種廣義概念,可以是時間或評分。其次,其豐富的數據結構為日后擴展提供了很大的方便。
- 提供的所有操作都是原子操作,為並發天然保駕護航。
- 超快的性能,見其官方性能測試《How fast is Redis?》。
- 擁有比較成熟的Java客戶端 - Jedis,像新浪微博都是使用它作為客戶端。(官方推薦的Clients)
啰嗦了一些其它東西,現在言歸正傳。
Redis 服務上線當天,就密切關注 Redis 的一些重要監控指標(clients:客戶端連接數、memory、stats:服務器每秒鍾執行的命令數量、commandstats:一些關鍵命令的執行統計信息、redis.error.log:異常日志)。(參考自《Redis監控方案》)
觀察到下午5點左右,發現“客戶端連接數”一直在增長,最高時都超過了2000個(見下圖),即使減少也就減1~2個。但應用的QPS卻在 10 個左右,而線上應用服務器不超過10台。按理說,服務器肯定不會有這么高的連接數,肯定哪里使用有問題。
現在只能通過逆向思維反向來推測問題:
- Redis服務端監控到的“客戶端連接數”表明所有客戶端總和起來應該有那么多,所以首先到各個應用服務器上確認連接數量;
- 通過“sudo netstat -antp | grep 6379 | wc -l”確認,有一台應用Redis的連接數都超過了1000個,另一台應用則在400左右,其它的都在60上下。(60上下是正常的)
- 第一個問題:為什么不同的機器部署了同一個應用程序,表現出來的行為卻是不一樣?
- 第二個問題:連接數超過1000個的那台,其請求量(140)是比其它機器(200+)要低的(因為它在Nginx中配置的權重低),那它的連接數為什么會這么高?到底發生了什么?
- 對於“第二個問題”,我們通過各個應用的Redis異常日志(redis.error.log)知道發生了什么。最高那台應用的異常操作特別多,共有130+個異常,且存在“關閉集群鏈接時異常導致連接泄漏”問題;另一台較高的應用也存在類似的情況,而其它正常的應用則不超過2個異常,且不存在“連接泄漏”問題。這樣,“第二個問題”算是弄清楚了。(“連接泄漏”問題具體如何修復見《[FAQ] Jedis使用過程中踩過的那些坑》)
- 至此,感覺問題好像已經解決了,但其實沒有。通過連續幾天的觀察,發現最高的時候,它的連接數甚至超過了3000+,這太恐怖了。(當時 leader 還和我說,要不要重啟一下應用)
- 即使應用的QPS是 20個/s,且存在“連接泄漏”問題,連接數也不會超過1000+。但現在連接數居然達到了3000+,這說不通,只有一個可能就是未正確使用Jedis。
- 這時候就繼續反推,Redis的連接數反映了Jedis對象池的池對象數量。線上部署了2台Redis服務器作為一個集群,說明這台應用共持有(3000/2=1500)個池對象。(因為Jedis基於Apache Commons Pool的GenericObjectPool實現)
- 第三個問題:根據應用的QPS,每秒鍾請求需要的Active池對象也不會超過20個,那其余的1480個都是“空閑池對象”。為什么那么多的“空閑池對象”未被釋放?
- 現在就來反思:Jedis的那些配置屬性與對象池管理“空閑池對象”相關,GenericObjectPool背后是怎么管理“空閑池對象”的?
由於在使用Jedis的過程中,就對Apache Commons Pool摸了一次底。對最后的兩個疑惑都比較了解,Jedis的以下這些配置與對象池管理“空閑池對象”相關:
redis.max.idle.num=32768
redis.min.idle.num=30
redis.pool.behaviour=FIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
redis.max.evictable.idle.time.minutes=1440
在上面說“每台應用的Jedis連接數在60個左右是正常的”的理由是:線上共部署了2台Redis服務器,Jedis的“最小空閑池對象個數”配置為30 (redis.min.idle.num=30)。
GenericObjectPool是通過“驅逐者線程Evictor”管理“空閑池對象”的,詳見《Apache Commons Pool之空閑對象的驅逐檢測機制》一文。最下方的5個配置都是與“驅逐者線程Evictor”相關的,表示對象池的空閑隊列行為為FIFO“先進先出”隊列方式,每秒鍾(1)檢測10個空閑池對象,空閑池對象的空閑時間只有超過5分鍾后,才有資格被驅逐檢測,若空閑時間超過一天(1440),將被強制驅逐。
因為“驅逐者線程Evictor”會無限制循環地對“池對象空閑隊列”進行迭代式地驅逐檢測。空閑隊列的行為有兩種方式:LIFO“后進先出”棧方式、FIFO“先進先出”隊列方式,默認使用LIFO。下面通過兩幅圖來展示這兩種方式的實際運作方式:
一、LIFO“后進先出”棧方式
二、FIFO“先進先出”隊列方式
從上面這兩幅圖可以看出,LIFO“后進先出”棧方式 有效地利用了空閑隊列里的熱點池對象資源,隨着流量的下降會使一些池對象長時間未被使用而空閑着,最終它們將被淘汰驅逐;
而 FIFO“先進先出”隊列方式 雖然使空閑隊列里所有池對象都能在一段時間里被使用,看起來它好像分散了資源的請求,但其實這不利於資源的釋放(因為空閑池對象的空閑時間只有超過5分鍾后,才有資格被驅逐檢測,分散資源請求的同時,也導致符合釋放條件的空閑對象也變少了,而每個空閑對象都占用一個redis連接)。
而這也是“客戶端連接數一直降不下來”的根源之一。
redis.pool.behaviour=FIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
按照上述配置,我們可以計算一下,5分鍾里到底有多少個空閑池對象被循環地使用過。
根據應用QPS 10個/s計算,5分鍾里大概有10*5*60=3000個空閑池對象被使用過,正好與上面的“連接數盡然達到了3000+”符合,這樣就說得通了。至此,整個問題終於水落石出了。(從監控圖也可以看出,在21號晚上6點左右修改配置重啟服務后,連接數就比較平穩了)
這里還要解釋一下為什么使用FIFO“先進先出”隊列方式的空閑隊列行為?
因為我們在Jedis的基礎上開發了“故障節點自動摘除,恢復正常的節點自動添加”的功能,本來想使用FIFO“先進先出”隊列方式在節點故障時,對象池能快速更新整個集群信息,沒想到弄巧成拙了。
修復后的Jedis配置如下:
redis.max.idle.num=32768
redis.min.idle.num=30
redis.pool.behaviour=LIFO
redis.time.between.eviction.runs.seconds=1
redis.num.tests.per.eviction.run=10
redis.min.evictable.idle.time.minutes=5
redis.max.evictable.idle.time.minutes=30
綜上所述,這個問題發生有兩方面的原因:
- 未正確使用對象池的空閑隊列行為(LIFO“后進先出”棧方式)
- “關閉集群鏈接時異常導致連接泄漏”問題
http://www.myexception.cn/internet/1849994.html
本文主要剖析 Apache Commons Pool 的“空閑對象的驅逐檢測機制”的實現原理。
以下面3個步驟來循序漸進地深入剖析其實現原理:
- 啟動“空閑對象的驅逐者線程”(startEvictor(...))的2個入口
- 在啟動時,創建一個新的"驅逐者線程"(Evictor),並使用"驅逐者定時器"(EvictionTimer)進行調度
- 進入真正地"空閑池對象"的驅逐檢測操作(evict())
下圖是“空閑對象的驅逐檢測機制”處理流程的時序圖(閱讀代碼時結合着看可以加深理解):
GenericObjectPool.evict() 處理流程的時序圖:
GenericObjectPool.ensureMinIdle()處理流程的時序圖:
一、啟動“空閑對象的驅逐者線程”(startEvictor(...))共有2個入口
1. GenericObjectPool 構造方法
GenericObjectPool(...):初始化"池對象工廠",設置"對象池配置",並啟動"驅逐者線程"。
- /**
- * 使用特定的配置來創建一個新的"通用對象池"實例。
- *
- * @param factory The object factory to be used to create object instances
- * used by this pool (用於創建池對象實例的對象工廠)
- * @param config The configuration to use for this pool instance. (用於該對象池實例的配置信息)
- * The configuration is used by value. Subsequent changes to
- * the configuration object will not be reflected in the
- * pool. (隨后對配置對象的更改將不會反映到池中)
- */
- public GenericObjectPool(PooledObjectFactory<T> factory,
- GenericObjectPoolConfig config) {
- super(config, ONAME_BASE, config.getJmxNamePrefix());
- if (factory == null) {
- jmxUnregister(); // tidy up
- throw new IllegalArgumentException("factory may not be null");
- }
- this.factory = factory;
- this.setConfig(config);
- // 啟動"驅逐者線程"
- startEvictor(this.getTimeBetweenEvictionRunsMillis());
- }
2. BaseGenericObjectPool.setTimeBetweenEvictionRunsMillis(...) - 設置"驅逐者線程"的運行間隔時間
可以動態地更新"驅逐者線程"的運行調度間隔時間。
- /**
- * 設置"空閑對象的驅逐者線程"的運行調度間隔時間。(同時,會立即啟動"驅逐者線程")
- * <p>
- * 如果該值是非正數,則沒有"空閑對象的驅逐者線程"將運行。
- * <p>
- * 默認是 {@code -1},即沒有"空閑對象的驅逐者線程"在后台運行着。
- * <p>
- * 上一層入口:{@link GenericObjectPool#setConfig(GenericObjectPoolConfig)}<br>
- * 頂層入口:{@link GenericObjectPool#GenericObjectPool(PooledObjectFactory, GenericObjectPoolConfig)},
- * 在最后還會調用{@link #startEvictor(long)}來再次啟動"空閑對象的驅逐者線程"。<br>
- * 這樣在初始化時,這里創建的"驅逐者線程"就多余了,會立刻被銷毀掉。<br>
- * 但這里為什么要這樣實現呢?<br>
- * 我的理解是:為了能動態地更新"驅逐者線程"的調度間隔時間。
- *
- * @param timeBetweenEvictionRunsMillis
- * number of milliseconds to sleep between evictor runs ("驅逐者線程"運行的間隔毫秒數)
- *
- * @see #getTimeBetweenEvictionRunsMillis
- */
- public final void setTimeBetweenEvictionRunsMillis(
- long timeBetweenEvictionRunsMillis) {
- this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
- // 啟動"驅逐者線程"
- this.startEvictor(timeBetweenEvictionRunsMillis);
- }
二、startEvictor(long delay) - 啟動“空閑對象的驅逐者線程”
如果有一個"驅逐者線程"(Evictor)運行着,則會先停止它;
然后創建一個新的"驅逐者線程",並使用"驅逐者定時器"(EvictionTimer)進行調度。
- // 空閑對象的驅逐回收策略
- /** 用於初始化"驅逐者線程"的同步對象 */
- final Object evictionLock = new Object();
- /** 空閑對象驅逐者線程 */
- private Evictor evictor = null; // @GuardedBy("evictionLock")
- /** 驅逐檢測對象迭代器 */
- Iterator<PooledObject<T>> evictionIterator = null; // @GuardedBy("evictionLock")
- /**
- * 啟動"空閑對象的驅逐者線程"。
- * <p>
- * 如果有一個"驅逐者線程"({@link Evictor})運行着,則會先停止它;
- * 然后創建一個新的"驅逐者線程",並使用"驅逐者定時器"({@link EvictionTimer})進行調度。
- *
- * <p>This method needs to be final, since it is called from a constructor. (因為它被一個構造器調用)
- * See POOL-195.</p>
- *
- * @param delay time in milliseconds before start and between eviction runs (驅逐者線程運行的開始和間隔時間 毫秒數)
- */
- 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);
- }
- }
- }
2.1 Evictor - "驅逐者線程"實現
Evictor,"空閑對象的驅逐者"定時任務,繼承自 TimerTask。TimerTask 是一個可由定時器(Timer)調度執行一次或重復執行的任務。
核心實現邏輯:
1. evict():執行numTests個空閑池對象的驅逐測試,驅逐那些符合驅逐條件的被檢測對象;
2. ensureMinIdle():試圖確保配置的對象池中可用"空閑池對象"實例的最小數量。
- /**
- * Class loader for evictor thread to use since in a J2EE or similar
- * environment the context class loader for the evictor thread may have
- * visibility of the correct factory. See POOL-161.
- * 驅逐者線程的類加載器
- */
- private final ClassLoader factoryClassLoader;
- // Inner classes
- /**
- * "空閑對象的驅逐者"定時任務,繼承自{@link TimerTask}。
- *
- * @see GenericObjectPool#GenericObjectPool(PooledObjectFactory, GenericObjectPoolConfig)
- * @see GenericKeyedObjectPool#setTimeBetweenEvictionRunsMillis(long)
- */
- class Evictor extends TimerTask {
- /**
- * 運行對象池維護線程。
- * 驅逐對象具有驅逐者的資格,同時保證空閑實例可用的最小數量。
- * 因為調用"驅逐者線程"的定時器是被所有對象池共享的,
- * 但對象池可能存在不同的類加載器中,所以驅逐者必須確保采取的任何行為
- * 都得在與對象池相關的工廠的類加載器下。
- */
- @Override
- public void run() {
- ClassLoader savedClassLoader =
- Thread.currentThread().getContextClassLoader();
- try {
- // Set the class loader for the factory (設置"工廠的類加載器")
- Thread.currentThread().setContextClassLoader(
- factoryClassLoader);
- // Evict from the pool (從"對象池"中驅逐)
- try {
- // 1. 執行numTests個空閑池對象的驅逐測試,驅逐那些符合驅逐條件的被檢測對象
- evict(); // 抽象方法
- } catch(Exception e) {
- swallowException(e);
- } catch(OutOfMemoryError oome) {
- // Log problem but give evictor thread a chance to continue
- // in case error is recoverable
- oome.printStackTrace(System.err);
- }
- // Re-create idle instances. (重新創建"空閑池對象"實例)
- try {
- // 2. 試圖確保配置的對象池中可用"空閑池對象"實例的最小數量
- ensureMinIdle(); // 抽象方法
- } catch (Exception e) {
- swallowException(e);
- }
- } finally {
- // Restore the previous CCL
- Thread.currentThread().setContextClassLoader(savedClassLoader);
- }
- }
- }
2.2 EvictionTimer - "驅逐者定時器"實現
EvictionTimer,提供一個所有"對象池"共享的"空閑對象的驅逐定時器"。此類包裝標准的定時器(Timer),並追蹤有多少個"對象池"使用它。
核心實現邏輯:
schedule(TimerTask task, long delay, long period):添加指定的驅逐任務到這個定時器
- /**
- * 提供一個所有"對象池"共享的"空閑對象的驅逐定時器"。
- *
- * 此類包裝標准的定時器({@link Timer}),並追蹤有多少個對象池使用它。
- *
- * 如果沒有對象池使用這個定時器,它會被取消。這樣可以防止線程一直運行着
- * (這會導致內存泄漏),防止應用程序關閉或重新加載。
- * <p>
- * 此類是包范圍的,以防止其被納入到池框架的公共API中。
- * <p>
- * <font color="red">此類是線程安全的!</font>
- *
- * @since 2.0
- */
- class EvictionTimer {
- /** Timer instance (定時器實例) */
- private static Timer _timer; //@GuardedBy("this")
- /** Static usage count tracker (使用計數追蹤器) */
- private static int _usageCount; //@GuardedBy("this")
- /** Prevent instantiation (防止實例化) */
- private EvictionTimer() {
- // Hide the default constructor
- }
- /**
- * 添加指定的驅逐任務到這個定時器。
- * 任務,通過調用該方法添加的,必須調用{@link #cancel(TimerTask)}來取消這個任務,
- * 以防止內存或消除泄漏。
- *
- * @param task Task to be scheduled (定時調度的任務)
- * @param delay Delay in milliseconds before task is executed (任務執行前的等待時間)
- * @param period Time in milliseconds between executions (執行間隔時間)
- */
- static synchronized void schedule(TimerTask task, long delay, long period) {
- if (null == _timer) {
- // Force the new Timer thread to be created with a context class
- // loader set to the class loader that loaded this library
- ClassLoader ccl = AccessController.doPrivileged(
- new PrivilegedGetTccl());
- try {
- AccessController.doPrivileged(new PrivilegedSetTccl(
- EvictionTimer.class.getClassLoader()));
- _timer = new Timer("commons-pool-EvictionTimer", true);
- } finally {
- AccessController.doPrivileged(new PrivilegedSetTccl(ccl));
- }
- }
- // 增加"使用計數器",並調度"任務"
- _usageCount++;
- _timer.schedule(task, delay, period);
- }
- /**
- * 從定時器中刪除指定的驅逐者任務。
- * <p>
- * Remove the specified eviction task from the timer.
- *
- * @param task Task to be scheduled (定時調度任務)
- */
- static synchronized void cancel(TimerTask task) {
- task.cancel(); // 1. 將任務的狀態標記為"取消(CANCELLED)"狀態
- _usageCount--;
- if (_usageCount == 0) { // 2. 如果沒有對象池使用這個定時器,定時器就會被取消
- _timer.cancel();
- _timer = null;
- }
- }
- /**
- * {@link PrivilegedAction} used to get the ContextClassLoader (獲取"上下文類加載器")
- */
- private static class PrivilegedGetTccl implements PrivilegedAction<ClassLoader> {
- @Override
- public ClassLoader run() {
- return Thread.currentThread().getContextClassLoader();
- }
- }
- /**
- * {@link PrivilegedAction} used to set the ContextClassLoader (設置"上下文類加載器")
- */
- private static class PrivilegedSetTccl implements PrivilegedAction<Void> {
- /** ClassLoader */
- private final ClassLoader cl;
- /**
- * Create a new PrivilegedSetTccl using the given classloader
- * @param cl ClassLoader to use
- */
- PrivilegedSetTccl(ClassLoader cl) {
- this.cl = cl;
- }
- @Override
- public Void run() {
- Thread.currentThread().setContextClassLoader(cl);
- return null;
- }
- }
- }
三、"驅逐者線程"和"驅逐者定時器"都准備就緒,現在真正地開始"空閑池對象"的驅逐檢測操作(evict())
BaseGenericObjectPool.evict():驅逐檢測操作的抽象聲明
- /**
- * 執行{@link numTests}個空閑池對象的驅逐測試,驅逐那些符合驅逐條件的被檢測對象。
- * <p>
- * 如果{@code testWhileIdle}為{@code true},則被檢測的對象在訪問期間是有效的(無效則會被刪除);
- * 否則,僅有那些池對象的空閑時間超過{@code minEvicableIdleTimeMillis}會被刪除。
- *
- * @throws Exception when there is a problem evicting idle objects. (當這是一個有問題的驅逐空閑池對象時,才會拋出Exception異常。)
- */
- public abstract void evict() throws Exception;
GenericObjectPool.evict():"通用對象池"的驅逐檢測操作實現
核心實現邏輯:
1. 確保"對象池"還打開着
2. 獲取"驅逐回收策略"
3. 獲取"驅逐配置"
4. 對所有待檢測的"空閑對象"進行驅逐檢測
4.1 初始化"驅逐檢測對象(空閑池對象)的迭代器"
4.2 將"池對象"標記為"開始驅逐狀態"
4.3 進行真正的"驅逐檢測"操作(EvictionPolicy.evict(...))
4.3.1 如果"池對象"是可驅逐的,則銷毀它
4.3.2 否則,是否允許空閑時進行有效性測試
4.3.2.1 先激活"池對象"
4.3.2.2 使用PooledObjectFactory.validateObject(PooledObject)進行"池對象"的有效性校驗
4.3.2.2.1 如果"池對象"不是有效的,則銷毀它
4.3.2.2.2 否則,還原"池對象"狀態
4.3.2.3 通知"空閑對象隊列",驅逐測試已經結束
5. 是否要移除"被廢棄的池對象"
- /** 池的空閑池對象列表 */
- private final LinkedBlockingDeque<PooledObject<T>> idleObjects =
- new LinkedBlockingDeque<PooledObject<T>>();
- /** 池對象工廠 */
- private final PooledObjectFactory<T> factory;
- // 空閑對象的驅逐回收策略
- /** 用於初始化"驅逐者線程"的同步對象 */
- final Object evictionLock = new Object();
- /** 空閑對象驅逐者線程 */
- private Evictor evictor = null; // @GuardedBy("evictionLock")
- /** 驅逐檢測對象("空閑池對象")的迭代器 */
- Iterator<PooledObject<T>> evictionIterator = null; // @GuardedBy("evictionLock")
- /** 被廢棄的池對象追蹤的配置屬性 */
- private volatile AbandonedConfig abandonedConfig = null;
- /**
- * {@inheritDoc}
- * <p>
- * 按順序對被審查的對象進行連續驅逐檢測,對象是以"從最老到最年輕"的順序循環。
- */
- @Override
- public void evict() throws Exception {
- // 1. 確保"對象池"還打開着
- this.assertOpen();
- if (idleObjects.size() > 0) {
- PooledObject<T> underTest = null; // 測試中的池對象
- // 2. 獲取"驅逐回收策略"
- EvictionPolicy<T> evictionPolicy = this.getEvictionPolicy();
- synchronized (evictionLock) { // 驅逐鎖定
- // 3. 獲取"驅逐配置"
- EvictionConfig evictionConfig = new EvictionConfig(
- this.getMinEvictableIdleTimeMillis(),
- this.getSoftMinEvictableIdleTimeMillis(),
- this.getMinIdle()
- );
- // 4. 對所有待檢測的"空閑對象"進行驅逐檢測
- for (int i = 0, m = this.getNumTests(); i < m; i++) {
- // 4.1 初始化"驅逐檢測對象(空閑池對象)的迭代器"
- if (evictionIterator == null || !evictionIterator.hasNext()) { // 已對所有空閑對象完成一次遍歷
- // 根據"對象池使用行為"賦值驅逐迭代器
- if (this.getLifo()) {
- evictionIterator = idleObjects.descendingIterator();
- } else {
- evictionIterator = idleObjects.iterator();
- }
- }
- if (!evictionIterator.hasNext()) {
- // Pool exhausted, nothing to do here (對象池被耗盡,無可用池對象)
- return;
- }
- try {
- underTest = evictionIterator.next();
- } catch (NoSuchElementException nsee) {
- // Object was borrowed in another thread (池對象被其它請求線程借用了)
- // Don't count this as an eviction test so reduce i;
- i--;
- evictionIterator = null;
- continue;
- }
- // 4.2 將"池對象"標記為"開始驅逐狀態"
- if (!underTest.startEvictionTest()) {
- // Object was borrowed in another thread
- // Don't count this as an eviction test so reduce i;
- i--;
- continue;
- }
- boolean testWhileIdle = this.getTestWhileIdle(); // 是否要在對象空閑時測試有效性
- // 4.3 進行真正的"驅逐檢測"操作(EvictionPolicy.evict(...))
- if (evictionPolicy.evict(evictionConfig, underTest,
- idleObjects.size())) {
- // 4.3.1 如果"池對象"是可驅逐的,則銷毀它
- this.destroy(underTest);
- destroyedByEvictorCount.incrementAndGet();
- } else {
- // 4.3.2 否則,是否允許空閑時進行有效性測試
- if (testWhileIdle) { // 允許空閑時進行有效性測試
- // 4.3.2.1 先激活"池對象"
- boolean active = false;
- try {
- factory.activateObject(underTest);
- active = true;
- } catch (Exception e) {
- this.destroy(underTest);
- destroyedByEvictorCount.incrementAndGet();
- }
- // 4.3.2.2 使用PooledObjectFactory.validateObject(PooledObject)進行"池對象"的有效性校驗
- if (active) {
- if (!factory.validateObject(underTest)) {
- // 4.3.2.2.1 如果"池對象"不是有效的,則銷毀它
- this.destroy(underTest);
- destroyedByEvictorCount.incrementAndGet();
- } else {
- try {
- // 4.3.2.2.2 否則,還原"池對象"狀態
- factory.passivateObject(underTest);
- } catch (Exception e) {
- this.destroy(underTest);
- destroyedByEvictorCount.incrementAndGet();
- }
- }
- }
- }
- // 4.3.2.3 通知"空閑對象隊列",驅逐測試已經結束
- if (!underTest.endEvictionTest(idleObjects)) {
- // TODO - May need to add code here once additional
- // states are used
- }
- }
- }
- }
- }
- // 5. 是否要移除"被廢棄的池對象"
- AbandonedConfig ac = this.abandonedConfig;
- if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
- this.removeAbandoned(ac);
- }
- }
BaseGenericObjectPool.ensureMinIdle():"確保對象池中可用"空閑池對象"實例的最小數量"的抽象聲明
- /**
- * 試圖確保配置的對象池中可用"空閑池對象"實例的最小數量。
- *
- * @throws Exception if an error occurs creating idle instances
- */
- abstract void ensureMinIdle() throws Exception;
GenericObjectPool.ensureMinIdle():"確保對象池中可用"空閑池對象"實例的最小數量"實現
- @Override
- void ensureMinIdle() throws Exception {
- this.ensureIdle(this.getMinIdle(), true);
- }
- /**
- * 返回對象池中維護的空閑對象的最小數量目標。
- * <p>
- * 此設置僅會在{@link #getTimeBetweenEvictionRunsMillis()}的返回值大於0時,
- * 且該值是正整數時才會生效。
- * <p>
- * 默認是 {@code 0},即對象池不維護空閑的池對象。
- *
- * @return The minimum number of objects. (空閑對象的最小數量)
- *
- * @see #setMinIdle(int)
- * @see #setMaxIdle(int)
- * @see #setTimeBetweenEvictionRunsMillis(long)
- */
- @Override
- public int getMinIdle() {
- int maxIdleSave = this.getMaxIdle();
- if (this.minIdle > maxIdleSave) {
- return maxIdleSave;
- } else {
- return minIdle;
- }
- }
- /**
- * 試圖確保對象池中存在的{@code idleCount}個空閑實例。
- * <p>
- * 創建並添加空閑實例,直到空閑實例數量({@link #getNumIdle()})達到{@code idleCount}個,
- * 或者池對象的總數(空閑、檢出、被創建)達到{@link #getMaxTotal()}。
- * 如果{@code always}是false,則不會創建實例,除非線程在等待對象池中的實例檢出。
- *
- * @param idleCount the number of idle instances desired (期望的空閑實例數量)
- * @param always true means create instances even if the pool has no threads waiting
- * (true意味着即使對象池沒有線程等待,也會創建實例)
- * @throws Exception if the factory's makeObject throws
- */
- private void ensureIdle(int idleCount, boolean always) throws Exception {
- if (idleCount < 1 || this.isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
- return;
- }
- while (idleObjects.size() < idleCount) {
- PooledObject<T> p = this.create();
- if (p == null) {
- // Can't create objects (不能創建對象), no reason to think another call to
- // create will work. Give up.
- break;
- }
- // "新的池對象"可以立刻被使用
- if (this.getLifo()) { // LIFO(后進先出)
- idleObjects.addFirst(p);
- } else { // FIFO(先進先出)
- idleObjects.addLast(p);
- }
- }
- }
- /**
- * 嘗試着創建一個新的包裝的池對象。
- *
- * @return The new wrapped pooled object
- *
- * @throws Exception if the object factory's {@code makeObject} fails
- */
- private PooledObject<T> create() throws Exception {
- // 1. 對象池是否被耗盡判斷
- 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 {
- // 2. 使用PooledObjectFactory.makeObject()來制造一個新的池對象
- p = factory.makeObject();
- } catch (Exception e) {
- createCount.decrementAndGet();
- throw e;
- }
- AbandonedConfig ac = this.abandonedConfig;
- if (ac != null && ac.getLogAbandoned()) {
- p.setLogAbandoned(true);
- }
- createdCount.incrementAndGet();
- // 3. 將新創建的池對象追加到"池的所有對象映射表"中
- allObjects.put(p.getObject(), p);
- return p;
- }
3.1 "驅逐回收策略"實現
EvictionConfig:"驅逐回收策略"配置信息
- /**
- * 此類用於將對象池的配置信息傳遞給"驅逐回收策略({@link EvictionPolicy})"實例。
- * <p>
- * <font color="red">此類是不可變的,且是線程安全的。</font>
- *
- * @since 2.0
- */
- public class EvictionConfig {
- // final 字段修飾保證其不可變性
- /** 池對象的最大空閑驅逐時間(當池對象的空閑時間超過該值時,立馬被強制驅逐掉) */
- private final long idleEvictTime;
- /** 池對象的最小空閑驅逐時間(當池對象的空閑時間超過該值時,被納入驅逐對象列表里) */
- private final long idleSoftEvictTime;
- /** 對象池的最小空閑池對象數量 */
- private final int minIdle;
- /**
- * 創建一個新的"驅逐回收策略"配置實例。
- * <p>
- * <font color="red">實例是不可變的。</font>
- *
- * @param poolIdleEvictTime Expected to be provided by (池對象的最大空閑驅逐時間(ms))
- * {@link BaseGenericObjectPool#getMinEvictableIdleTimeMillis()}
- * @param poolIdleSoftEvictTime Expected to be provided by (池對象的最小空閑驅逐時間(ms))
- * {@link BaseGenericObjectPool#getSoftMinEvictableIdleTimeMillis()}
- * @param minIdle Expected to be provided by (對象池的最小空閑池對象數量)
- * {@link GenericObjectPool#getMinIdle()} or
- * {@link GenericKeyedObjectPool#getMinIdlePerKey()}
- */
- public EvictionConfig(long poolIdleEvictTime, long poolIdleSoftEvictTime,
- int minIdle) {
- if (poolIdleEvictTime > 0) {
- idleEvictTime = poolIdleEvictTime;
- } else {
- idleEvictTime = Long.MAX_VALUE;
- }
- if (poolIdleSoftEvictTime > 0) {
- idleSoftEvictTime = poolIdleSoftEvictTime;
- } else {
- idleSoftEvictTime = Long.MAX_VALUE;
- }
- this.minIdle = minIdle;
- }
- /**
- * 獲取"池對象的最大空閑驅逐時間(ms)"。
- * <p>
- * 當池對象的空閑時間超過該值時,立馬被強制驅逐掉。
- * <p>
- * How the evictor behaves based on this value will be determined by the
- * configured {@link EvictionPolicy}.
- *
- * @return The {@code idleEvictTime} in milliseconds
- */
- public long getIdleEvictTime() {
- return idleEvictTime;
- }
- /**
- * 獲取"池對象的最小空閑驅逐時間(ms)"。
- * <p>
- * 當池對象的空閑時間超過該值時,被納入驅逐對象列表里。
- * <p>
- * How the evictor behaves based on this value will be determined by the
- * configured {@link EvictionPolicy}.
- *
- * @return The (@code idleSoftEvictTime} in milliseconds
- */
- public long getIdleSoftEvictTime() {
- return idleSoftEvictTime;
- }
- /**
- * 獲取"對象池的最小空閑池對象數量"。
- * <p>
- * How the evictor behaves based on this value will be determined by the
- * configured {@link EvictionPolicy}.
- *
- * @return The {@code minIdle}
- */
- public int getMinIdle() {
- return minIdle;
- }
- }
EvictionPolicy<T>:"驅逐回收策略"聲明
- /**
- * 為了提供對象池的一個自定義"驅逐回收策略",
- * 使用者必須提供該接口的一個實現(如{@link DefaultEvictionPolicy})。
- *
- * @param <T> the type of objects in the pool (對象池中對象的類型)
- *
- * @since 2.0
- */
- public interface EvictionPolicy<T> {
- /**
- * 一個對象池中的空閑對象是否應該被驅逐,調用此方法來測試。(驅逐行為聲明)
- *
- * @param config The pool configuration settings related to eviction (與驅逐相關的對象池配置設置)
- * @param underTest The pooled object being tested for eviction (正在被驅逐測試的池對象)
- * @param idleCount The current number of idle objects in the pool including
- * the object under test (當前對象池中的空閑對象數,包括測試中的對象)
- * @return <code>true</code> if the object should be evicted, otherwise
- * <code>false</code> (如果池對象應該被驅逐掉,就返回{@code true};否則,返回{@code false}。)
- */
- boolean evict(EvictionConfig config, PooledObject<T> underTest,
- int idleCount);
- }
DefaultEvictionPolicy<T>:提供用在對象池的"驅逐回收策略"的默認實現,繼承自EvictionPolicy<T>
- /**
- * 提供用在對象池的"驅逐回收策略"的默認實現,繼承自{@link EvictionPolicy}。
- * <p>
- * 如果滿足以下條件,對象將被驅逐:
- * <ul>
- * <li>池對象的空閑時間超過{@link GenericObjectPool#getMinEvictableIdleTimeMillis()}
- * <li>對象池中的空閑對象數超過{@link GenericObjectPool#getMinIdle()},且池對象的空閑時間超過{@link GenericObjectPool#getSoftMinEvictableIdleTimeMillis()}
- * </ul>
- * <font color="red">此類是不可變的,且是線程安全的。</font>
- *
- * @param <T> the type of objects in the pool (對象池中對象的類型)
- *
- * @since 2.0
- */
- public class DefaultEvictionPolicy<T> implements EvictionPolicy<T> {
- /**
- * 如果對象池中的空閑對象是否應該被驅逐,調用此方法來測試。(驅逐行為實現)
- */
- @Override
- public boolean evict(EvictionConfig config, PooledObject<T> underTest,
- int idleCount) {
- if ((idleCount > config.getMinIdle() &&
- underTest.getIdleTimeMillis() > config.getIdleSoftEvictTime())
- || underTest.getIdleTimeMillis() > config.getIdleEvictTime()) {
- return true;
- }
- return false;
- }
- }
其他相關實現
- // --- internal attributes (內部屬性) -------------------------------------------------
- /** 對象池中的所有池對象映射表 */
- private final ConcurrentMap<T, PooledObject<T>> allObjects =
- new ConcurrentHashMap<T, PooledObject<T>>();
- /** 池的空閑池對象列表 */
- private final LinkedBlockingDeque<PooledObject<T>> idleObjects =
- new LinkedBlockingDeque<PooledObject<T>>();
- /** 池對象工廠 */
- private final PooledObjectFactory<T> factory;
- /**
- * 計算空閑對象驅逐者一輪測試的對象數量。
- *
- * @return The number of objects to test for validity (要測試其有效性的對象數量)
- */
- private int getNumTests() {
- int numTestsPerEvictionRun = this.getNumTestsPerEvictionRun();
- if (numTestsPerEvictionRun >= 0) {
- return Math.min(numTestsPerEvictionRun, idleObjects.size());
- } else {
- return (int) (Math.ceil(idleObjects.size() /
- Math.abs((double) numTestsPerEvictionRun)));
- }
- }
- /**
- * 銷毀一個包裝的"池對象"。
- *
- * @param toDestory The wrapped pooled object to destroy
- *
- * @throws Exception If the factory fails to destroy the pooled object
- * cleanly
- */
- private void destroy(PooledObject<T> toDestory) throws Exception {
- // 1. 設置這個"池對象"的狀態為"無效(INVALID)"
- toDestory.invalidate();
- // 2. 將這個"池對象"從"空閑對象列表"和"所有對象列表"中移除掉
- idleObjects.remove(toDestory);
- allObjects.remove(toDestory.getObject());
- try {
- // 3. 使用PooledObjectFactory.destroyObject(PooledObject<T> p)來銷毀這個不再需要的池對象
- factory.destroyObject(toDestory);
- } finally {
- destroyedCount.incrementAndGet();
- createCount.decrementAndGet();
- }
- }
- /**
- * 恢復被廢棄的對象,它已被檢測出超過{@code AbandonedConfig#getRemoveAbandonedTimeout()
- * removeAbandonedTimeout}未被使用。
- * <p>
- * <font color="red">注意:需要考慮性能損耗,因為它會對對象池中的所有池對象進行檢測!</font>
- *
- * @param ac The configuration to use to identify abandoned objects
- */
- private void removeAbandoned(AbandonedConfig ac) {
- // 1. Generate a list of abandoned objects to remove (生成一個要被刪除的被廢棄的對象列表)
- final long now = System.currentTimeMillis();
- final long timeout =
- now - (ac.getRemoveAbandonedTimeout() * 1000L);
- List<PooledObject<T>> remove = new ArrayList<PooledObject<T>>();
- Iterator<PooledObject<T>> it = allObjects.values().iterator();
- while (it.hasNext()) {
- PooledObject<T> pooledObject = it.next();
- synchronized (pooledObject) {
- // 從"所有池對象"中挑選出狀態為"使用中"的池對象,且空閑時間已超過了"對象的移除超時時間"
- if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
- pooledObject.getLastUsedTime() <= timeout) {
- // 標記池對象為"被廢棄"狀態,並添加到刪除列表中
- pooledObject.markAbandoned();
- remove.add(pooledObject);
- }
- }
- }
- // 2. Now remove the abandoned objects (移除所有被廢棄的對象)
- Iterator<PooledObject<T>> itr = remove.iterator();
- while (itr.hasNext()) {
- PooledObject<T> pooledObject = itr.next();
- if (ac.getLogAbandoned()) {
- pooledObject.printStackTrace(ac.getLogWriter());
- }
- try {
- this.invalidateObject(pooledObject.getObject());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
綜上所述,真正的"空閑對象的驅逐檢測操作"在 GenericObjectPool.evict() 中實現,其被包裝在"驅逐者定時器任務(Evictor)"中,並由"驅逐定時器(EvictionTimer)"定時調度,而啟動"驅逐者線程"則由 BaseGenericObjectPool.startEvictor(long delay) 實現。
http://bert82503.iteye.com/blog/2171595