爲了應對越來越大的流量,緩存便成爲系統服務必不可少的一部分,但使用緩存就會出現緩存擊穿和緩存穿透的威脅。
一、背景介紹
二、緩存穿透常見的處理方式
2.1 空值緩存
這裏需要強調注意:爲了系統的最終一致性,這些key必須設置過期時間,或者必須存在更新方式,防止這個key的數據後期真實存在,但改key始終爲空,導致數據不一致的情況出現。
public String getById(String key) {
String value = jimdbClient.get(key);
if (value == null) {
value = testMapper.get(key);
if (value == null) {
jimdbClient.set(key, null, 60, TimeUnit.SECONDS, false);
return null;
}
}
}
這種方式的缺點也十分的明顯:如果key數量巨大且分散無任何規律,就會浪費大量緩存空間,並且不能抗住瞬時流量衝擊(尤其是遇到惡意的攻擊的時候,有可能將緩存空間打爆,影響範圍更大),需要額外配置降級開關(查詢數據庫的開關或者限流),這時本方案就顯得沒想象的那麼美好。針對不能抗住瞬時流量的情況,常見的處理方式是使用計數器,對不存在的key進行計數,當某個key在一定時間達到一定的量級,就查詢一次數據庫,按照數據庫的返回值對key進行緩存。未達指定閾值數量之前,按照商定的空值返回。
2.2 布隆過濾器(BloomFilter)
public void put(String rediskey, String key) {
long[] indexs = getIndexs(key);
for (long index : indexs) {
jimdbClient.setBit(rediskey, index,true);
}
}
/**
* 根據key獲取bitmap下標
*/
private long[] getIndexs(String key) {
long hash1 = hash(key);
long hash2 = hash1 >>> 16;
long[] result = new long[numHashFunctions];
for (int i = 0; i < numHashFunctions; i++) {
long combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
result[i] = combinedHash % numBits;
}
return result;
}
/**
* 獲取一個hash值
*/
private long hash(String key) {
Charset charset = Charset.forName("UTF-8");
return Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asLong();
}
這裏需要強調注意的是必須使用高效的hash算法,否則這種方式會嚴重影響系統的性能,建議的算法包括MurmurHash、Fnv的穩定高效的算法。
三、緩存擊穿常見的處理方式
3.1 互斥鎖(mutex key)
public String getById(String key) {
String value = jimdbClient.get(key);
if (value == null) {
boolean nx = jimdbClient.set(lock_key, "test-lock", 2, TimeUnit.SECONDS, false);
if (nx) {
value = testMapper.get(key);
jimdbClient.set(key, value, 60 * 60 * 24, TimeUnit.SECONDS, false);
return value;
} else {
Thread.sleep(100);
return getById(key);
}
}
}
Redis還是善解人意的,從 2.6.12 起,我們可以使用SET命令完成SETNX和EXPIRE的操作,並且這種操作是原子操作,可以完全替代上述的代碼了。
3.2 異步構建緩存
四、緩存雪崩的常見的處理方式
緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機。和緩存擊穿不同的是, 緩存擊穿指併發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。
緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
public String getById(String key) {
Random random = new Random();
int r = random.nextInt(100);
String value = jimdbClient.get(key);
if (value == null) {
boolean nx = jimdbClient.set(lock_key, "test-lock", 2, TimeUnit.SECONDS, false);
if (nx) {
value = testMapper.get(key);
jimdbClient.set(key, value, r, TimeUnit.SECONDS, false);
return value;
} else {
Thread.sleep(100);
return getById(key);
}
}
}
五、總結
綜上所述,針對常見的緩存穿透和緩存擊穿的問題,各自的優缺點如下: