在我們的項目中多少都會使用緩存,因爲有些數據我們沒有必要每次都去查詢數據庫,特別是高QPS的系統,每次都去查詢數據庫會影響數據庫性能。
業務系統一般調用流程:
我們一般的做法是先從緩存中查詢,如果緩存中查詢到了則直接返回,如果緩存中沒有,則查詢數據庫,如果從數據庫中查詢到了,則先將數據寫入緩存,然後返回數據給調用方。
public Result<Post> queryPostFromCache(Long id) {
// POST:id
final String key = Constant.Cache.POST + Constant.COLON + id;
// 從緩存中查詢
Object post = cacheService.get(key);
if(post != null) {
return Result.success((Post) post);
}
// 從數據庫中查詢
post = postMapper.queryPostById(id);
if(post != null) {
cacheService.set(key, post, Duration.ofHours(1));
}
return Result.success((Post) post);
}
正常情況下,我們查詢的數據都是存在的,比如從文章列表頁面跳轉到文章詳情頁查詢文章詳細信息,此時根據文章ID查詢,文章的ID是真實存在的。不正常的情況是如果請求查詢的文章ID是一條根本不存在的數據,也就是說緩存和數據庫中都不會有值,這會導致請求每次都會到數據庫中查詢。這種查詢不存在數據的現象我們稱之爲緩存穿透
。
緩存穿透
是指用戶查詢的數據在數據庫中沒有,那麼在緩存中也不會有,也就是說查詢的是一條根本不存在的記錄
。這樣就會導致用戶首先在緩存中找不到 ,則每次都要去數據庫再查詢一遍,然後返回空。這樣就相當於進行了兩次無用的查詢。要是有人利用這種不存在的key頻繁攻擊我們的系統,很可能導致數據庫壓力增大,甚至導致數據庫掛掉。
解決方案
1.將未在數據庫中查詢到值的key也寫入緩存,緩存的值爲空對象,同時設置一個較短的過期時間,以防止後面真的有數據。
2.採用布隆過濾器(Bloom Filter),使用一個足夠大的bitmap,用於存儲數據庫中可能訪問的key。布隆過濾器可以理解爲一個不太精確的set集合,當你使用它的contains方法判斷某個對象是否存在時,它可能會誤判。它的特點是當布隆過濾器判斷某個值存在時,這個值可能不存在(誤判),當它判斷某個值不存在時,那就肯定不存在。這個特點可以用在判斷要查詢的key是否存在,如果判斷不存在時,那就肯定不存在,這個時候就不用再查詢數據庫了,直接返回給調用方值不存在。
使用空對象解決緩存穿透問題的示例代碼
public Result<Post> queryPostFromCache(Long id) {
// POST:id
final String key = Constant.Cache.POST + Constant.COLON + id;
// 從緩存中查詢
Object post = cacheService.get(key);
if(post != null) {
// 判斷緩存中的值是否爲默認對象
if(post instanceof NullObjectValue) {
return Result.fail(ResultCode.RESOURCES_NOT_FOUND);
}
return Result.success((Post) post);
}
// 從數據庫中查詢
post = postMapper.queryPostById(id);
if(post != null) {
cacheService.set(key, post, Duration.ofHours(1));
}else {
// 數據庫中沒有對應記錄,給這個key緩存設置一個默認值
cacheService.set(key, new NullObjectValue(), Duration.ofHours(1));
}
return Result.success((Post) post);
}
有關使用布隆過濾器來解決緩存穿透問題的具體實現,我將會在下篇文章中介紹。