首先來討論兩個問題,分別是緩存穿透和緩存雪崩
一、什么是緩存穿透?如何避免?
一般的緩存系統,都是按照key去查詢緩存,如果不存在對應的value,就應該去后端系統查找(比如DB)。一些惡意的請求會故意查詢不存在的key,請求量很大,就會對后端系統造成很大的壓力。這就叫做緩存穿透。(只對於登錄的惡意用戶群體)
如何避免:
1:網上有好多說法是分段自增設置緩存的失效時間,避免在同一時間段造成大量的緩存失效。
我想說的是系統默認永久,你去動它干什么?而且在需要緩存的業務中,新增、修改、刪除都有對應的緩存策略,你動它干什么?甚至內存,哪些該舍棄,哪些該保留,redis都有自己的一套方案。如果考慮Redis機器宕機,可考慮主從復制方案。
二、什么是緩存雪崩?何如避免?
當緩存服務器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,會給后端系統帶來很大壓力。導致系統崩潰。
如何避免:
1:在緩存失效后,通過加鎖或者隊列來控制讀數據庫寫緩存的線程數量。比如對某個key只允許一個線程查詢數據和寫緩存,其他線程等待。
首先不存在服務器重啟,大量的緩存更新,從業務上講,有查詢才有緩存。其次,redis有持久化機制,默認是RDB,當然設置成AOF更好。我覺得沒必要,不存在,去查一遍並更新就好了嘛。
綜上,我們需要做到是先查緩存,沒有就去查數據庫再更新到緩存,並且保證只能有一個線程執行查詢數據庫並更新到緩存這個任務就可以了。至於沒有查詢的業務,你加載到緩存干什么?
於是redis分布式鎖就誕生,原理是第一個請求的資源加鎖,執行查詢DB並更新緩存的操作。其他資源看到有鎖就等待,然后執行查詢。於是乎,需要三個方法,分別是:加鎖、解鎖、輪詢。
①、加鎖
使用redis命令 set key value NX EX max-lock-time 實現加鎖
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final String SUCCESS = "OK"; /** * 加鎖操作 * @param key 鎖標識 * @param value 客戶端標識 * @param timeOut 過期時間 */ public Boolean lock(String key,String value,Long timeOut){ String var1 = jedis.set(key,value,"NX","EX",timeOut); if(LOCK_SUCCESS.equals(var1)){ return true; } return false; }
解讀:
加鎖操作:jedis.set(key,value,"NX","EX",timeOut)【保證加鎖的原子操作】
key就是redis的key值作為鎖的標識,value在這里作為客戶端的標識,只有key-value都比配才有刪除鎖的權利【保證安全性】
通過timeOut設置過期時間保證不會出現死鎖【避免死鎖】
NX,EX什么意思?
NX:只有這個key不存才的時候才會進行操作,if not exists;
EX:設置key的過期時間為秒,具體時間由第5個參數決定
②、解鎖
使用redis命令 EVAL 實現解鎖
Jedis jedis = new Jedis("127.0.0.1", 6379); private static final Long UNLOCK_SUCCESS = 1L; /** * 解鎖操作 * @param key 鎖標識 * @param value 客戶端標識 * @return */ public static Boolean unLock(String key,String value){ String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end"; Object var2 = jedis.eval(luaScript,Collections.singletonList(key), Collections.singletonList(value)); if (UNLOCK_SUCCESS == var2) { return true; } return false; }
解讀:
-
luaScript 這個字符串是個lua腳本,代表的意思是如果根據key拿到的value跟傳入的value相同就執行del,否則就返回0【保證安全性】
-
jedis.eval(String,list,list);這個命令就是去執行lua腳本,KEYS的集合就是第二個參數,ARGV的集合就是第三參數【保證解鎖的原子操作】
③、輪詢
試想一下如果在業務中去拿鎖如果沒有拿到是應該阻塞着一直等待還是直接返回,這個問題其實可以寫一個重試機制,根據重試次數和重試時間做一個循環去拿鎖,當然這個重試的次數和時間設多少合適,是需要根據自身業務去衡量的。
/** * 重試機制 * @param key 鎖標識 * @param value 客戶端標識 * @param timeOut 過期時間 * @param retry 重試次數 * @param sleepTime 重試間隔時間 * @return */ public Boolean lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime){ Boolean flag = false; try { for (int i=0;i<retry;i++){ flag = lock(key,value,timeOut); if(flag){ break; } Thread.sleep(sleepTime); } }catch (Exception e){ e.printStackTrace(); } return flag; }