Storm常見模式------TimeCacheMap
Storm中使用一種叫做TimeCacheMap的數據結構,用於在內存中保存近期活躍的對象,它的實現非常地高效,而且可以自動刪除過期不再活躍的對象。
TimeCacheMap使用多個桶buckets來縮小鎖的粒度,以此換取高併發讀寫性能。下面我們來看看TimeCacheMap內部是如何實現的。
1. 實現原理
桶鏈表:鏈表中每個元素是一個HashMap,用於保存key,value格式的數據。
private LinkedList<HashMap<K,
V>> _buckets; |
鎖對象:用於對TimeCacheMap進行get/put等操作時上鎖保證原子性。
private final Object
_lock = new Object(); |
後臺清理線程:負責超時後清理數據。
超時回調接口:用於超時後進行函數回調,做一些其他處理。
public static interface ExpiredCallback<K,
V> { |
public void expire(K
key, V val); |
private ExpiredCallback
_callback; |
有了以上數據結構,下面來看看構造函數的具體實現:
1、 首先,初始化指定個數的bucket,以鏈式鏈表形式存儲,每個bucket中放入空的HashMap;
2、 然後,設置清理線程,處理流程爲:
a) 休眠expirationMillis / (numBuckets-1)毫秒時間(即:expirationSecs / (numBuckets-1)秒);
b) 對_lock對象上鎖,然後從buckets鏈表中移除最後一個元素;
c) 向buckets鏈表頭部新加入一個空的HashMap桶,解除_lock對象鎖;
d) 如果設置了callback函數,則進行回調。
public TimeCacheMap( int expirationSecs, int numBuckets,
ExpiredCallback<K, V> callback) { |
throw new IllegalArgumentException( "numBuckets
must be >= 2" ); |
_buckets
= new LinkedList<HashMap<K,
V>>(); |
for ( int i= 0 ;
i<numBuckets; i++) { |
_buckets.add( new HashMap<K,
V>()); |
final long expirationMillis
= expirationSecs * 1000L; |
final long sleepTime
= expirationMillis / (numBuckets- 1 ); |
_cleaner
= new Thread( new Runnable()
{ |
dead
= _buckets.removeLast(); |
_buckets.addFirst( new HashMap<K,
V>()); |
for (Entry<K,
V> entry: dead.entrySet()) { |
_callback.expire(entry.getKey(),
entry.getValue()); |
} catch (InterruptedException
ex) { |
_cleaner.setDaemon( true ); |
構造函數需要傳遞三個參數:expirationSecs:超時的時間,單位爲秒;numBuckets:桶的個數;callback:超時回調函數。
爲了方便使用,還提供了以下三種形式的構造函數,使用時可以根據需要選擇:
private static final int DEFAULT_NUM_BUCKETS
= 3 ; |
public TimeCacheMap( int expirationSecs,
ExpiredCallback<K, V> callback) { |
this (expirationSecs,
DEFAULT_NUM_BUCKETS, callback); |
public TimeCacheMap( int expirationSecs, int numBuckets)
{ |
this (expirationSecs,
numBuckets, null ); |
public TimeCacheMap( int expirationSecs)
{ |
this (expirationSecs,
DEFAULT_NUM_BUCKETS); |
2. 性能分析
get操作:遍歷各個bucket,如果存在指定的key則返回,時間複雜度爲O(numBuckets)
for (HashMap<K,
V> bucket: _buckets) { |
if (bucket.containsKey(key))
{ |
put操作:將key,value放到_buckets的第一個桶中,然後遍歷其他numBuckets-1個桶,從HashMap中移除其中鍵爲key的記錄,時間複雜度爲O(numBuckets)
public void put(K
key, V value) { |
Iterator<HashMap<K,
V>> it = _buckets.iterator(); |
HashMap<K,
V> bucket = it.next(); |
remove操作:遍歷各個bucket,如果存在以key爲鍵的記錄,直接刪除,時間複雜度爲O(numBuckets)
public Object
remove(K key) { |
for (HashMap<K,
V> bucket: _buckets) { |
if (bucket.containsKey(key))
{ |
return bucket.remove(key); |
containsKey操作:遍歷各個bucket,如果存在指定的key則返回true,否則返回false,時間複雜度爲O(numBuckets)
public boolean containsKey(K
key) { |
for (HashMap<K,
V> bucket: _buckets) { |
if (bucket.containsKey(key))
{ |
size操作:遍歷各個bucket,累加各個bucket的HashMap的大小,時間複雜度爲O (numBuckets)
for (HashMap<K,
V> bucket: _buckets) { |
3. 超時時間
經過上面對put操作和_cleaner線程的分析,我們已經知道:
a) put操作將數據放到_buckets的第一個桶中,然後遍歷其他numBuckets-1個桶,從HashMap中移除其中鍵爲key的記錄;
b) _cleaner線程每隔expirationSecs / (numBuckets-1)秒會把_buckets中最後一個桶中的數據從TimeCacheMap中移除掉。
因此,假設_cleaner線程剛剛清理數據,put函數調用發生將key放入桶中,那麼一條數據的超時時間爲:
expirationSecs / (numBuckets-1) * numBuckets = expirationSecs * (1 + 1 / (numBuckets-1))
然而,假設put函數調用剛剛執行結束,_cleaner線程就開始清理數據,那麼一條數據的超時時間爲:
expirationSecs / (numBuckets-1) * numBuckets - expirationSecs / (numBuckets-1) = expirationSecs
4. 總結
1、 TimeCacheMap的高效之處在於鎖的粒度小,O(1)時間內完成鎖操作,因此,大部分時間內都可以進行get和put操作。
2、 get,put,remove,containsKey和size操作都可以在O(numBuckets)時間內完成,其中numBuckets是桶的個數,默認爲3。
3、 未更新數據的超時時間在expirationSecs和expirationSecs * (1 + 1 / (numBuckets-1))之間。
參考:http://www.cnblogs.com/panfeng412/archive/2012/06/26/storm-common-patterns-of-timecachemap.html