緩存知識

權(quán)限系統(tǒng)是管理類系統(tǒng)中必不可少的一個(gè)模塊,一個(gè)好的緩存設(shè)計(jì)更是權(quán)限系統(tǒng)的重中之重,今天來聊下如何更好設(shè)計(jì)權(quán)限系統(tǒng)的緩存。
單節(jié)點(diǎn)緩存
權(quán)限校驗(yàn)屬于使用頻率超高的操作,如果每次都去請求db的話,不僅會給db帶來壓力,也會導(dǎo)致用戶響應(yīng)過慢,造成很不好的用戶體驗(yàn),因此把權(quán)限相關(guān)數(shù)據(jù)放到緩存中是很有必要的,偽代碼如下:
private static final FUNCTION_CACHE_KEY = "function_cache_key";
public List<Function> loadFunctions() {
// 優(yōu)先從緩存中取
List<Function> functions = cacheService.get(FUNCTION_CACHE_KEY);
if(functions != null){
return functions;
}
// 緩存中沒有,從數(shù)據(jù)庫中取,并放入緩存
functions = functionDao.loadFunctions();
cacheService.put(FUNCTION_CACHE_KEY, functions);
return functions;
}
推薦使用ehcache作為緩存組件,ehcache是一個(gè)純Java的進(jìn)程內(nèi)緩存框架,支持?jǐn)?shù)據(jù)持久化到磁盤,并且支持多種緩存策略,對于權(quán)限數(shù)據(jù)這種大數(shù)據(jù)量的緩存可以說是非常合適。
集群緩存
ehcache屬于進(jìn)程級緩存,對集群支持不是很友好,雖然可以通過一些方案實(shí)現(xiàn)分布式緩存,但總感覺沒有直接用memcached或redis來的痛快,但直接用memcached或redis的話,會經(jīng)過一次網(wǎng)絡(luò)調(diào)用,而且對于權(quán)限緩存這樣內(nèi)存比較大的數(shù)據(jù),性能沒有ehcache這種進(jìn)程級緩存好。那有沒有一直方案可以兼顧ehcache的性能優(yōu)勢和redis的分布式優(yōu)勢呢?
可以通過ehcache和redis共用的方式來解決這個(gè)問題,大致思路是用ehcache做主緩存,緩存更新通過MQ在集群間進(jìn)行通信,而redis做為二級緩存使用。
具體方案如下:
更新數(shù)據(jù)
把數(shù)據(jù)同時(shí)放入ehcache和redis中,同時(shí)通過MQ通知其它節(jié)點(diǎn)更新自身的緩存,更新的數(shù)據(jù)從redis里面拉取
刪除數(shù)據(jù)
刪除ehcache和redis中數(shù)據(jù),同時(shí)通過MQ通知其它節(jié)點(diǎn)刪除自身的數(shù)據(jù)
其實(shí)對于權(quán)限緩存,一般情況下更新操作并不頻繁,通過MQ做變更通知,redis做二級緩存,這樣就可以在集群環(huán)境下仍舊使用ehcache的高效存儲了
用時(shí)間戳保證級聯(lián)緩存的一致性
在設(shè)計(jì)緩存的時(shí)候,并不是所有的緩存都是從數(shù)據(jù)庫取的,有的緩存是從其它緩存從取的,這樣可以減少使用時(shí)的計(jì)算時(shí)間
數(shù)據(jù)庫 --> 緩存a --> 緩存b
有上面的依賴關(guān)系可以看出,緩存a發(fā)生變更時(shí),緩存b如果不重新從緩存a中重新加載,就會造成緩存臟數(shù)據(jù)。
最直觀的方案是刷新a緩存時(shí),同步刷新b緩存,但從上述依賴關(guān)系可以看到,b依賴a,a并不依賴b,b緩存對于a應(yīng)該是不可見的,所以從邏輯上來說不符合依賴的規(guī)則。
而且上面只是二級關(guān)聯(lián),如果是四級,五級的話,上層緩存的變更帶動(dòng)了太多下級緩存的變更,需要耗費(fèi)很多時(shí)間,因此如果能用延遲刷新或許是更好的方案。
用時(shí)間戳或許是個(gè)不錯(cuò)的辦法,上述例子中,可以給緩存a增加一個(gè)時(shí)間戳,每次a緩存變更,同步更新時(shí)間戳。獲取b的時(shí)候只需要校驗(yàn)下a的時(shí)間戳是否變更,變更了就重新加載b緩存,否則直接返回b。
偽代碼如下:
// 權(quán)限信息緩存key
private static final FUNCTION_CACHE_KEY = "function_cache_key";
// 權(quán)限信息緩存時(shí)間戳
private static final FUNCTION_TIME_STAMP = "function_time_stamp";
// 權(quán)限信息緩存舊的時(shí)間戳
private static final FUNCTION_OLD_TIME_STAMP = "function_old_time_stamp";
// 用戶權(quán)限信息緩存key
private static final USER_FUNCTION_CACHE_KEY = "uer_function_cache_key";

// 加載所有的權(quán)限信息
public List<Function> loadFunctions() {
// 優(yōu)先從緩存中取
List<Function> functions = cacheService.get(FUNCTION_CACHE_KEY);
if(functions != null){
return functions;
}
// 緩存中沒有,從數(shù)據(jù)庫中取,并放入緩存
functions = functionDao.loadFunctions();
cacheService.put(FUNCTION_CACHE_KEY, functions);
// 同步更新時(shí)間戳
String timeStamp = String.valueOf(System.currentTimeMillis());
cacheService.put(FUNCTION_TIME_STAMP, timeStamp);
return functions;
}

// 根據(jù)用戶id加載用戶的權(quán)限信息
public List<Function> loadUserFunctions(Long userId) {
List<Function> functions = loadFunctions();
// 加載緩存中用戶權(quán)限信息
List<Function> userFunctions = cacheService.get(USER_FUNCTION_CACHE_KEY + userId);
String newTimeStamp= cacheService.get(FUNCTION_TIME_STAMP);
String oldTimeStamp= cacheService.get(FUNCTION_OLD_TIME_STAMP);
// 如果緩存中沒有用戶權(quán)限信息,或者時(shí)間戳不相等,重新從權(quán)限信息里面加載用戶權(quán)限信息
if(userFunctions == null || newTimeStamp != oldTimeStamp){
userFunctions = getUserFunctions(functions, userId);
// 把用戶權(quán)限信息放入緩存
cacheService.put(USER_FUNCTION_CACHE_KEY + userId, functions);
// 把當(dāng)前時(shí)間戳放入緩存
cacheService.put(FUNCTION_OLD_TIME_STAMP, newTimeStamp);
return userFunctions;
}
return userFunctions;
}
需要說明的是,上述代碼只是作為示例,真正開發(fā)時(shí)用戶的權(quán)限信息一般有更好的處理方式,并不一定是上面示例中每個(gè)用戶都單獨(dú)放一份緩存。
因?yàn)樯厦婢彺嬷皇嵌壖壜?lián),如果級數(shù)更多,同樣可以用時(shí)間戳來進(jìn)行延遲加載
數(shù)據(jù)庫 --> 緩存a --> 緩存b --> 緩存c --> 緩存d
獲取緩存d時(shí),可以校驗(yàn) 緩存a時(shí)間戳 + 緩存b時(shí)間戳 + 緩存c時(shí)間戳,abc任何一個(gè)時(shí)間戳發(fā)生變化,緩存d都需要重新加載,思路和上面的差不多,這里就不多贅述了。
guava 的妙用
對于權(quán)限校驗(yàn)中使用頻率高,但校驗(yàn)邏輯又不常變化的地方可以再加一層緩存。
例如一般都權(quán)限系統(tǒng)都有對外的接口,可以直接匿名訪問,校驗(yàn)代碼如下
// ant風(fēng)格 url 匹配器
private AntPathMatcher matcher = new AntPathMatcher();
// 可以訪問的匿名url集合,通常采用ant風(fēng)格,例如 /open/api/**
// 匿名url通常寫在配置文件中,并且在bean初始化時(shí)加載到該集合中
private Set<String> anonymousUrlPatterns = new HashSet<String>();

// 判斷url是否能匿名訪問
public boolean couldAnonymous(String url) {
for (String patternUrl : anonymousUrlPatterns) {
if (matcher.match(patternUrl, url)) {
isMatch = true;
break;
}
}
return isMatch;
}
可以看到,每一次url訪問都會校驗(yàn),可以通過加一層緩存來優(yōu)化性能
用分布式緩存感覺有點(diǎn)大材小用,ehcache又有點(diǎn)太重量級,ConcurrentHashMap又不支持緩存策略,思來想去guava貌似是最好的選擇,改造完后的代碼如下:
// ant風(fēng)格 url 匹配器
private AntPathMatcher matcher = new AntPathMatcher();
// 可以訪問的匿名url集合,通常采用ant風(fēng)格,例如 /open/api/**
// 匿名url通常寫在配置文件中,并且在bean初始化時(shí)加載到該集合中
private Set<String> anonymousUrlPatterns = new HashSet<String>();

// 匿名url訪問權(quán)限緩存
private static Cache<String, Boolean> anonymousUrlCache = CacheBuilder.newBuilder()
.maximumSize(5000)
.initialCapacity(1000)
.expireAfterAccess(1, TimeUnit.DAYS) // 設(shè)置cache中的的對象多久沒有被訪問后過期
.build();

// 判斷url 是否能匿名訪問
public boolean couldAnonymous(String url) {
// 先從緩存中取,有的話直接返回
Boolean couldAnonymousAccess = anonymousUrlCache.getIfPresent(url);
if (couldAnonymousAccess != null) {
return couldAnonymousAccess;
}
boolean isMatch = false;
for (String patternUrl : anonymousUrlPatterns) {
if (matcher.match(patternUrl, url)) {
isMatch = true;
break;
}
}
// 匹配結(jié)果放入緩存
anonymousUrlCache.put(url, isMatch);
return isMatch;
}
localStorage 緩存
localStorage 是 HTML5支持的新特性,可以把一些數(shù)據(jù)緩存放在客戶端,減輕服務(wù)器的壓力,例如可以把菜單數(shù)據(jù)放到客戶端,菜單數(shù)據(jù)是否過期通過時(shí)間戳來判斷,偽代碼如下:
var timestamp = localStorage.getItem("timestamp" + userId);
// 請求后臺獲取菜單接口,帶上時(shí)間戳參數(shù) timestamp
// 后臺校驗(yàn)時(shí)間戳是否變更,如果變更,返回新的菜單數(shù)據(jù)和新的時(shí)間戳,否則不需要返回菜單數(shù)據(jù),仍舊返回舊的時(shí)間戳即可
// 后臺接口返回?cái)?shù)據(jù)格式 result = {menus:{},timestamp:""}
var newTimestamp = result.timestamp;
// 時(shí)間戳變更,把新的菜單數(shù)據(jù)和新的時(shí)間戳 放入 localStorage
if (newTimestamp != timestamp) {
localStorage.setItem("menus" + userId, JSON.stringify(result.menus));
localStorage.setItem("timestamp" + userId, newTimestamp);
}
有人擔(dān)心把緩存放在localStorage中如果被修改會造成安全問題,其實(shí)這個(gè)擔(dān)心是沒必要的,因?yàn)闄?quán)限校驗(yàn)是在服務(wù)器端做的,localStorage中的緩存只做展示使用,因此修改localStorage時(shí)沒有任何意義的。
總結(jié)
在不同的情況下,上述場景分別用了ehcache,redis,guava,localStorage做緩存,更加說明了沒有最好的技術(shù),只有最適合的技術(shù)。通過引入時(shí)間戳這種版本號的機(jī)制,解決了緩存更新問題。最終的目的只有一個(gè),保證緩存數(shù)據(jù)一致性的同時(shí),把性能做的極致,用戶體驗(yàn)做到最好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容