一種通用的簡易緩存設計方案


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接口的案例

/**
* 資源片請求設備列表緩存對象
*/
public class Piece implements Cleanable {

public final Integer id;
public final Long groupId;
public final Long resourceId;
/**
* 擁有該資源片設備節點隊列,不允許隊列中出現重復設備(因為一些異常情況,設備節點會重復請求);
* 需要保證請求節點有序的添加,因此可以使用非線程安全的數組實現
*/
public final List<DownloadNode> nodes;
final LocalDateTime createdTime;
//默認60分鍾后過期
final LocalDateTime expiredTime;

public Piece(Long groupId, Long resourceId, Integer id) {
this.groupId = groupId;
this.resourceId = resourceId;
this.id = id;
nodes = new ArrayList<>();
this.createdTime = LocalDateTime.now();
this.expiredTime = this.createdTime.plusMinutes(60);
}

@JsonCreator
Piece(@JsonProperty("id") Integer id, @JsonProperty("groupId") Long groupId,
@JsonProperty("resourceId") Long resourceId, @JsonProperty("nodes") List<DownloadNode> nodes,
@JsonProperty("createdTime") String createdTime, @JsonProperty("expiredTime") String expiredTime) {
this.id = id;
this.groupId = groupId;
this.resourceId = resourceId;
this.nodes = nodes;
this.createdTime = LocalDateTime.parse(createdTime);
this.expiredTime = LocalDateTime.parse(expiredTime);
}

public String CachedId() {
return "piece_" + groupId + "_" + resourceId + "_" + id;
}

/**
* 將擁有該資源片的節點加入隊列
* @param node
*/
public void add(DownloadNode node) {
this.nodes.add(node);
}

/**
*
* 獲取可共享資源片的節點
*
* 采用從第一個節點開始遞減式分配。如當前序列有如下分配情況:
* 1, 0; 則當請求分配連接后變為 1,0。
* 2, 1,0; 則當請求分配連接后變為 2,0,0。
* 3, 2,0,0; 則當請求分配連接后變為 2,1,0,0
* 4, 2,1,0,0; 。。。。變為3,1,0,0,0
* 5, 3,1,0,0,0; 。。。。變為3,2,0,0,0,0
* 6, 3,2,0,0,0,0; 。。。。變為3,2,1,0,0,0,0
* 以此類推,遞減式分配。也即是,
* a,如果索引n節點的conn和索引n+1的conn的差值為2則將n+1節點返回;
* b,否則,將索引為0的節點返回。
*
* @return 可共享資源片節點
*/
public DownloadNode getPieceNode() {
for(int i=0;i<nodes.size()-1;i++) {
DownloadNode n = nodes.get(i);
DownloadNode n_1 = nodes.get(i+1);
int val = n.conns - n_1.conns;
if(val == 2) {
return n_1;
}
}
return nodes.get(0);
}

public boolean hasRequest(DownloadNode dn) {
return nodes.contains(dn);
}

public void remove(DownloadNode dn) {
this.nodes.remove(dn);
}

@Override
public LocalDateTime expiredTime() {
return this.expiredTime;
}

@Override
public LocalDateTime createdTime() {
return this.createdTime;
}

//for json mapper

public Integer getId() {
return id;
}

public Long getGroupId() {
return groupId;
}

public Long getResourceId() {
return resourceId;
}

public List<DownloadNode> getNodes() {
return nodes;
}

public String getCreatedTime() {
return createdTime.toString();
}

public String getExpiredTime() {
return expiredTime.toString();
}
}

如上代碼所示,Piece類是一個可緩存對象的實現類,其意義用於緩存所有請求下載過該資源片的設備節點列表。

a, 仔細看Piece類的屬性有一個明顯的特征,即所有屬性都用final關鍵字修飾了;而實際上,所有Cleanable接口的實現類(或會被緩存的對象)的字段,除非必須的,都應當使用final關鍵字修飾。因為我們服務處在並發環境下,緩存對象應當盡可能做到線程安全,final修飾的作用就是消除JMM的重排序,保證創建的對象是線程安全的;而如果被緩存對象存在非final修飾的屬性,則在發生修改時,若要確保數據的一致性,則必須加鎖

b, 另外,可以注意到Piece類有兩個構造方法,一個是公開訪問,一個是不可公開調用但可以通過反射工具調用的;所以,所有Cleanable的實現類都必須實現一個@JsonCreator注解的保留構造方法,如此才能實現自動的序列化和反序列化。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM