1,領域模型設計
該設計方案定義了三個基礎接口: Cache,Cleanable和CacheManager;和一個默認實現類DefaultCacheManager。
- Cache接口抽象了非內存緩存所能提供的基礎操作,Cache接口隔離了外部緩存的具體實現方案,可以是Redis/Codis等任意形式的緩存方案;
- Cleanable接口定義了被緩存對象(value)的基本屬性,所以需要被緩存的對象都必須實現Cleanable接口;
- CacheManager抽象了該緩存方案支持的功能,直接提供給客戶端調用,屏蔽了具體緩存實現方案,允許提供多個解決方案;
- DefaultCacheManager是CacehManager的實現類,支持使用本地內存和外部緩存的實現。
2,Cache interface
該接口的定義沒有多少爭議,就是實現序列化對象通過key進行存取。
3,Cleanable interface
前文已說明,所有需要被緩存的對象都必須實現該接口。接口定義了,緩存對象必須是可清理的,有過期時間: expiredTime,創建時間: createdTime;如果使用的是本地內存實現,則會通過isValid方法方便的檢查對象是否過期,否則使用timeout方法來設置外部緩存時長。
4,CacheManager interface
CacheManager接口是真正面向客戶端使用的,緩存的value是Cleanable對象,key允許是任何可序列化值(常用String);特別地,需要攜帶一個枚舉類ModuleType。在微服務或分布式的架構系統中,無論是在項目初期或中后期,都可能公用同一個外部緩存服務器,因此為了避免緩存數據沖突和方便數據追蹤,都需要對Key進行模塊分割。
5,DefaultCacheManager
public class DefaultCacheManager implements CacheManager, Runnable { private static ConcurrentMap<String, Cleanable> dataMap = new ConcurrentHashMap<String, Cleanable>(); private final Long cleanPeriod; private final boolean enabledRedisCache; private final ScheduledExecutorService validationService; private final Cache cacheClient; public DefaultCacheManager() { this(1000L, null); } public DefaultCacheManager(Cache cacheClient) { this(null, cacheClient); } public DefaultCacheManager(Long cleanPeriod) { this(cleanPeriod, null); } /** * @param cleanPeriod * @param cacheClient */ private DefaultCacheManager(Long cleanPeriod, Cache cacheClient) { this.enabledRedisCache = cacheClient != null ? true : false; this.cacheClient = cacheClient; this.cleanPeriod = cleanPeriod; if(!enabledRedisCache) { /** * 在本地完成expired清理動作 */ this.validationService = Executors.newSingleThreadScheduledExecutor(); this.validationService.scheduleAtFixedRate(this, this.cleanPeriod, this.cleanPeriod, TimeUnit.MILLISECONDS); } else this.validationService = null; } @Override public boolean exist(Serializable srcKey, ModuleType keyType) { String key = keyType.key(srcKey); if(enabledRedisCache) { return cacheClient.getValue(key) != null; } Cleanable c = dataMap.get(key); if (c == null) return false; if (!c.isValid()) { dataMap.remove(key); return false; } return true; } @Override public void put(Serializable srcKey, ModuleType keyType, Cleanable value) { String key = keyType.key(srcKey); if(enabledRedisCache) { cacheClient.setValue(key, value, value.timeout(), TimeUnit.SECONDS); return; } dataMap.put(key, value); } @Override public Cleanable get(Serializable srcKey, ModuleType keyType) { String key = keyType.key(srcKey); Cleanable value; if(enabledRedisCache) { value = ((Cleanable) cacheClient.getValue(key)); } else { value = dataMap.get(key); } return value; } @Override public void remove(Serializable srcKey, ModuleType keyType) { String key = keyType.key(srcKey); if(enabledRedisCache) { cacheClient.removeValue(key); } else { dataMap.remove(key); } } public void run() { /** * 清理過期對象 */ Iterator<String> iter = dataMap.keySet().iterator(); while (iter.hasNext()){ String key = iter.next(); // 由於並發緣故 // 可能已經把該{@param key}對應的對象, 清理掉了 Cleanable c = dataMap.get(key); if(c != null && !c.isValid()){ iter.remove(); } } } } |
DefaultCacheManager類是支持內存緩存和外部緩存的默認實現類。在此,可能會有疑惑,像Redis這樣的外部緩存,應用已經非常普遍,為什么還要做內存緩存?我個人有三個考慮:1)即便是開源項目,如Eurka Server也仍然使用內存緩存,具體原因可查閱該項目的源碼設計分析;2)對於微服務或分布式架構的系統,在項目早期或對於某些服務而言,需緩存數據量小或不需要共享緩存數據;3)追求高響應時間,不想額外依賴外部服務,提高系統可靠性。
6,一個實現Cleanable接口的案例
/** |
如上代碼所示,Piece類是一個可緩存對象的實現類,其意義用於緩存所有請求下載過該資源片的設備節點列表。
a, 仔細看Piece類的屬性有一個明顯的特征,即所有屬性都用final關鍵字修飾了;而實際上,所有Cleanable接口的實現類(或會被緩存的對象)的字段,除非必須的,都應當使用final關鍵字修飾。因為我們服務處在並發環境下,緩存對象應當盡可能做到線程安全,final修飾的作用就是消除JMM的重排序,保證創建的對象是線程安全的;而如果被緩存對象存在非final修飾的屬性,則在發生修改時,若要確保數據的一致性,則必須加鎖。
b, 另外,可以注意到Piece類有兩個構造方法,一個是公開訪問,一個是不可公開調用但可以通過反射工具調用的;所以,所有Cleanable的實現類都必須實現一個@JsonCreator注解的保留構造方法,如此才能實現自動的序列化和反序列化。