典型緩存案例
當我們使用redis做緩存時一般步驟如下
- 請求進來時候首先查詢redis判斷是否存在緩存且緩存是否過期
- 若已經存在不過期的緩存則直接獲取返回
- 若緩存不存在或已過期則重新查詢數據庫并將該數據存到redis中
代碼可以如下表示:
@Autowired
private RedisTemplate redisTemplate;
public List<String> getValueBySql(String key){
System.out.println("這里模擬從數據庫中獲取數據");
return new ArrayList<>();
}
public List<String> getCache(String key){
List<String> resultList = (List<String>)redisTemplate.opsForValue().get(key);
if(resultList == null || CollectionUtils.isEmpty(resultList)){
//若緩存不存在則從數據庫獲取并設置時間
resultList = getValueBySql(key);
redisTemplate.opsForValue().set(key, resultList, 1000, TimeUnit.SECONDS);
return resultList;
}else{
return resultList;
}
}
緩存擊穿
什么是緩存擊穿?
如上面的經典緩存流程,在整個流程中我們需要先查詢redis,在redis沒有的時候再去查數據庫最后再將數據庫返回的數據存到redis中。如果有一些非常經常被訪問的數據,例如一分鐘內有超高的訪問請求。試想一下剛某個熱點數據key在這個時刻過期。下一時刻有好幾個請求同時來請求key,這時候由于redisTemplate.opsForValue().get(key)為空,所有的數據必將直接訪問數據庫,這個時候大并發的請求可能會瞬間把后端DB壓垮
解決方案1: 使用synchronized+雙檢查機制
此方法適用于單機模式
/***
* synchronized + 雙重檢查機制
* @param key
* @return
*/
public List<String> getCacheSave(String key){
List<String> resultList = (List<String>)redisTemplate.opsForValue().get(key);
if(resultList == null || CollectionUtils.isEmpty(resultList)){
//采用synchronized保證一次只有一個請求進入到這個代碼塊
synchronized (this){
resultList = (List<String>)redisTemplate.opsForValue().get(key);
if(CollectionUtils.isEmpty(resultList)){
return resultList;
}
resultList = getValueBySql(key);
redisTemplate.opsForValue().set(key, resultList, 1000, TimeUnit.SECONDS);
return resultList;
}
}else{
return resultList;
}
}
- 上面代碼第一個判斷保證在緩存有數據時,讓查詢緩存的請求不必排隊,減小了同步的粒度
- synchronized (this)保證查詢數據庫是同步操作,同一時刻只能有一個請求查詢數據庫
- 第二個判斷保證所有在redis有緩存時,其他請求無需在查意思數據庫。若沒有這個判斷,其他已經等待synchronized 解鎖的請求會在請求一次數據庫
解決方案2:采用互斥鎖
適用于分布式模式
使用分布式鎖的方式。如圖,使用分布式鎖保證只有一個線程查詢數據庫,其他線程采用重試的方式進行獲取
代碼參考如下
/***
*
* @param key
* @param retryCount 重試次數
* @return
* @throws InterruptedException
*/
public List<String> getCacheSave2(String key,int retryCount) throws InterruptedException {
List<String> resultList = (List<String>)redisTemplate.opsForValue().get(key);
if(CollectionUtils.isEmpty(resultList)){
final String mutexKey = key + "_lock";
boolean isLock = (Boolean) redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
//只在鍵key不存在的情況下,將鍵key的值設置為value,若鍵key已經存在,則 SETNX 命令不做任何動作
//命令在設置成功時返回 1 , 設置失敗時返回 0
return connection.setNX(mutexKey.getBytes(),"1".getBytes());
}
});
if(isLock){
//設置成1秒過期
redisTemplate.expire(mutexKey, 1000, TimeUnit.MILLISECONDS);
resultList = getValueBySql(key);
redisTemplate.opsForValue().set(key, resultList, 1000, TimeUnit.SECONDS);
redisTemplate.delete(mutexKey);
}else{
//線程休息50毫秒后重試
Thread.sleep(50);
retryCount--;
System.out.println("=====進行重試,當前次數:" + retryCount);
if(retryCount == 0){
System.out.println("====這里發郵件或者記錄下獲取不到數據的日志,并為key設置一個空置防止重復獲取");
List<String> list = Lists.newArrayList("no find");
redisTemplate.opsForValue().set(key, list, 1000, TimeUnit.SECONDS);
return list;
}
return getCacheSave2(key,retryCount);
}
}
return resultList;
}
解決方案3:提前設置鎖
這是網上看到的方案
https://carlosfu.iteye.com/blog/2269687
感覺還是采用分布式鎖的方式,只不過是每次獲取的時候先獲取一下key的過期時間,如果過期時間快到了就提前重新設置下超時時間,并從數據庫中獲取最新的數據覆蓋
解決方案:資源保護
采用netflix的hystrix,可以做資源的隔離保護主線程池(不懂,后面學習下)
緩存雪崩
什么是緩存雪崩?
緩存雪崩是指在我們設置緩存時采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發到DB,DB瞬時壓力過重雪崩。
解決方案:在設置過期時間時加隨機值保證不同時失效
緩存失效時的雪崩效應對底層系統的沖擊非??膳?。大多數系統設計者考慮用加鎖或者隊列的方式保證緩存的單線程(進程)寫,從而避免失效時大量的并發請求落到底層存儲系統上。這里分享一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發集體失效的事件
緩存擊穿
例如上面的經典流程,如果我輸入一個不在我們規劃范圍的key,也就是說這個key永遠也查不到數據,則按照流程每次都要先去查數據庫,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
解決方案1:設置白名單
設置key的白名單,只有在白名單的key才能允許查詢(如果key的數量很多或key不是事先知道的情況下這種方式就不太好用)。或者更高級點用布隆過濾器記錄所有可能的key,每次請求時進行攔截