1 緩存雪崩
緩存雪崩產生的原因
緩存雪崩通俗簡單的理解就是:由於原有緩存失效(或者數據未加載到緩存中),新緩存未到期間(緩存正常從Redis中獲取,如下圖)所有原本應該訪問緩存的請求都去查詢數據庫了,而對數據庫CPU和內存造成巨大壓力,嚴重的會造成數據庫宕機,造成系統的崩潰。
緩存失效的時候如下圖:
緩存失效時的雪崩效應對底層系統的沖擊非常可怕!那有什么辦法來解決這個問題呢?基本解決思路如下:
第一,大多數系統設計者考慮用加鎖或者隊列的方式保證來保證不會有大量的線程對數據庫一次性進行讀寫,避免緩存失效時對數據庫造成太大的壓力,雖然能夠在一定的程度上緩解了數據庫的壓力但是與此同時又降低了系統的吞吐量。
第二,分析用戶的行為,盡量讓緩存失效的時間均勻分布。
第三,如果是因為某台緩存服務器宕機,可以考慮做主備,比如:redis主備,但是雙緩存涉及到更新事務的問題,update可能讀到臟數據,需要好好解決。
解決方案
1:在緩存失效后,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
@RequestMapping("/getUsers") public Users getByUsers(Long id) { // 1.先查詢redis String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; String userJson = redisService.getString(key); if (!StringUtils.isEmpty(userJson)) { Users users = JSONObject.parseObject(userJson, Users.class); return users; } Users user = null; try { lock.lock(); // 查詢db user = userMapper.getUser(id); redisService.setSet(key, JSONObject.toJSONString(user)); } catch (Exception e) { } finally { lock.unlock(); // 釋放鎖 } return user; }
注意:加鎖排隊只是為了減輕數據庫的壓力,並沒有提高系統吞吐量。假設在高並發下,緩存重建期間key是鎖着的,這是過來1000個請求999個都在阻塞的。同樣會導致用戶等待超時,這是個治標不治本的方法。
2:不同的key,設置不同的過期時間,讓緩存失效的時間點盡量均勻。
3:做二級緩存,A1為原始緩存,A2為拷貝緩存,A1失效時,可以訪問A2,A1緩存失效時間設置為短期,A2設置為長期(此點為補充)
2 緩存穿透
緩存穿透是指用戶查詢數據,在數據庫沒有,自然在緩存中也不會有。這樣就導致用戶查詢的時候,在緩存中找不到,每次都要去數據庫再查詢一遍,然后返回空。這樣請求就繞過緩存直接查數據庫,這也是經常提的緩存命中率問題。
解決的辦法就是:如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續訪問數據庫,這種辦法最簡單粗暴。
@Service public class UserAvalanService { @Autowired private UserMapper userMapper; @Autowired private RedisService redisService; private Lock lock = new ReentrantLock(); private String SIGN_KEY = "${NULL}"; public Users getByUsers(Long id) { // 1.先查詢redis String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; String userJson = redisService.getString(key); if (!StringUtils.isEmpty(userJson)) { Users users = JSONObject.parseObject(userJson, Users.class); return users; } Users user = null; try { lock.lock(); // 查詢db user = userMapper.getUser(id); redisService.setSet(key, JSONObject.toJSONString(user)); lock.unlock(); } catch (Exception e) { } finally { lock.unlock(); // 釋放鎖 } return user; } public String getByUsers2(Long id) { // 1.先查詢redis String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id; String userName = redisService.getString(key); if (!StringUtils.isEmpty(userName)) { return userName; } System.out.println("######開始發送數據庫DB請求########"); Users user = userMapper.getUser(id); String value = null; if (user == null) { // 標識為null value = SIGN_KEY; } else { value = user.getName(); } redisService.setString(key, value); return value; } }
把空結果,也給緩存起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當查詢的值為空時引起的緩存穿透。同時也可以單獨設置個緩存區域存儲空值,對要查詢的key進行預先校驗,然后再放行給后面的正常緩存處理邏輯。
注意:再給對應的ip存放真值的時候,需要先清除對應的之前的空緩存。
3 熱點key
熱點key:某個key訪問非常頻繁,當key失效的時候有大量線程來構建緩存,導致負載增加,系統崩潰。解決辦法:
1.使用鎖,單機用synchronized,lock等,分布式用分布式鎖。
2.緩存過期時間不設置,而是設置在key對應的value里。如果檢測到存的時間超過過期時間則異步更新緩存。
3.在value設置一個比過期時間t0小的過期時間值t1,當t1過期的時候,延長t1並做更新緩存操作。