大型網站中都會用到分布式緩存,現在經常使用的成熟可靠的分布式緩存產品有Memcached、Redis、Velocity等等。開發中我們在設計實現緩存層的時候,通常會按照業務模塊,定義一些有意義的緩存鍵。比如,在一個非常典型的電子商務網站中,我們會緩存常用的字典表,如省市區縣、商品分類、商品等等,一種常用的定義緩存鍵的方式如下:
/// <summary> /// 緩存鍵管理 /// </summary> public class CacheKeyManager { /// <summary> /// 區域 /// </summary> public static readonly string Area = "{0}_area_{1}"; /// <summary> /// 商品 /// </summary> public static readonly string Product = "{0}_product_{1}"; /// <summary> /// 商品分類 /// </summary> public static readonly string ProductCatagory = "{0}_productcatagory_{1}"; }
對於字符串中的第一項{0},我們通常會傳入一個用於標識業務的有意義的字符串,比如命名空間、業務部門名稱等;第二項{1}則通常對應於標識字典表數據的唯一性的值(如主鍵等)。這樣拼接字符串定義緩存的key可以降低不同業務部門不同開發人員命名相同緩存鍵的可能,這一點在一個業務部門眾多而部署的分布式緩存系統只有一套的環境下顯得尤為重要。
實際開發使用緩存鍵的時候,最終拼接的字符串可能如下面這種形式:
/// <summary> /// 區域 /// </summary> public static readonly string Area = "namespace_area_areaid"; /// <summary> /// 商品 /// </summary> public static readonly string Product = "namespace_product_productid"; /// <summary> /// 商品分類 /// </summary> public static readonly string ProductCatagory = "namespace_productcatagory_productcatagoryid";
正如你所看到的,namespace是對應的命名空間,由不同業務部門的開發自行定義;xxxid則用於標識字典表數據的唯一性。
緩存的數據通常都是短時間內相對穩定不會發生變化的,但互聯網業務是瞬息萬變的,業務人員的要求通常也不那么通情達理,不變的數據往往也會隨時需要改變並立刻在生產環境中體現出來。於是,我們在前台站點用上分布式緩存之后,還需要在后台業務系統中寫不少代碼,用於在改變數據的時候,及時更新緩存。
但在后台代碼中,更新緩存往往都是費力不討好的事情,一個考慮不到就會出現數據不一致的問題。比如現在需要更新一個商品分類的屬性,和商品分類相關的很多緩存鍵必須都要考慮到,比如可能按照商品分類主鍵或者分類名稱緩存了一個商品分類,或者按照某種查詢規則緩存了一個商品分類列表或字典,或者商品分類屬性的修改直接導致商品信息級聯的修改,你又不得不考慮到緩存的商品信息……所以,在后台清理緩存的操作通常不是那么穩定可靠讓人放心。
針對上面描述的這種復雜多變的應用場景,有人可能會說適當時候直接讓運維介入,對於常用分布式緩存產品,上面這種情形只要一個命令行就把緩存及時清理了。可實際情況是,可能只有一個開發小組的緩存數據需要改變,卻把整個公司的緩存數據都清理了,這樣顯然非常不合理。
求人不如求己,下面就是本文要介紹的一種相對靈活控制緩存版本並“及時”清理緩存的方法,分如下幾步:
1、在自己的業務系統中新建一張數據表(比如叫CacheVersion),只有一個字符串Version字段,初始化一條記錄,且只有一條記錄;
2、將CacheVersion表數據寫入分布式緩存系統,按照自己所在業務定義Version對應的緩存鍵,如xxx_cacheversion,對於你所在的業務部門,這個緩存鍵字符串可以認為是個常量;
3、重新定義業務需要的緩存鍵,如下所示:
/// <summary> /// 區域 /// </summary> public static readonly string Area = "{0}_area_{1}_{2}";
其中{2}對應的就是讀出的CacheVersion對應的那條數據,CacheVersion那條記錄的獲取也是先從分布式緩存中取,如沒有再讀數據庫,最后拼好的字符串是如下這個形式:
/// <summary> /// 區域 /// </summary> public static readonly string Area = "namespace_area_areaid_version";
4、后台業務數據發生改變,直接更新業務數據對應表記錄,不需要寫任何代碼用於更新緩存;
5、在一個獨立的管理模塊,如業務需更新緩存數據,則更新CacheVersion表的唯一記錄,並重置分布式緩存中的CacheVersion對應數據。
上面的5步思路其實很簡單,就是由原來的維護多個緩存鍵改為集中維護一個CacheVersion,“及時”過期策略其實就是換一個緩存版本而已。到這里分布式緩存數據的版本控制就大功告成了。
當然,這種方式也有缺點,主要包括:
1、新增了一個數據表CacheVersion需要維護,Version字段需考慮數據唯一的生成策略;
2、多了一個CacheVersion操作模塊;
3、前台站點緩存鍵字符串構造多了一個Version參數,讀取Version也需要先從分布式緩存中取,如沒有再讀數據庫,多了網絡傳輸和IO;
4、每次更新版本,依賴這個版本的分布式緩存都會“被過期”,如在並發訪問高峰,可能得不償失。
最后歡迎大家討論:你是如何控制和實現緩存有效期過期策略的?CacheVersion這張表是不是可以不需要?如果是你來控制緩存版本,緩存Version該如何生成?