Redis(七)快取穿透、快取擊穿、快取雪崩以及分散式鎖
應用問題解決
1 快取穿透
1.1 訪問結構
正常情況下,伺服器接收到瀏覽器發來的web服務請求,會先去訪問redis快取,如果快取中存在資料則直接返回,否則會去查詢資料庫裡面的資料,然後儲存在redis中再進行返回。
1.2 快取擊穿的現象
-
出現大量web請求,應用伺服器壓力變大
-
redis命中率降低:redis中頻繁查不到資料
-
應用伺服器一直在查詢資料庫
1.3 快取擊穿出現的原因
網站被惡意攻擊,主要表現為:
-
redis查詢不到資料庫
-
出現大量非正常url訪問
查詢不到資料並不是真的想要獲取到資料,而是希望藉此增大redis記憶體壓力進而致使伺服器癱瘓。
1.4 快取擊穿解決方案
一個不存在於快取並且註定也不存在於資料庫的資料,由於快取是不命中的時候被動寫的,並且處於容錯的考慮,當儲存層查不到資料就不會寫到快取裡面,這將導致每次在快取中查不到資料就去儲存層查詢,失去了快取的意義。
-
對空值也進行快取:如果查詢到一個數據的結果為空,那麼我們也對這個空值進行快取,不管這個資料是不是存在,設定空結果的過期時間都會很短,最多不超過五分鐘
-
設定可以訪問的名單(白名單):只允許指定的id進行訪問,使用redis 的 bitmaps,將id作為偏移量,然後每次訪問都需要和bitmaps中的id進行比較,如果訪問的id不在bitmaps中,則不允許訪問。
-
布隆過濾器:它底層實現實際上類似於bitmaps,用一個很長的二進位制量(點陣圖)和一系列隨機雜湊函式。布隆過濾器可以檢測一個元素是否存在於一個集合中,優點是空間效率和查詢時間,缺點是由一定的誤識別率和刪除困難。
將所有可能存在的資料雜湊到一個足夠大的bitmaps裡面,一個一定不會存在的資料會被這個bitmaps攔截掉,從而避免了底層系統的查詢壓力。
-
進行實時監控:當發現Redis的命中率開始降低,排查訪問物件和查詢的資料,和運維人員配合,進行設定黑名單攔截。
2 快取擊穿
2.1 快取穿透的現象
-
資料庫訪問壓力瞬間增大
-
redis並沒有出現大量key過期(大量key過期為快取雪崩)
-
redis正常執行
2.2 快取擊穿造成的原因
redis中的某個key過期的時候,突然出現了大量對於該key的web服務請求,導致只能去訪問資料庫,而造成的資料庫壓力瞬間增大。
2.3 快取擊穿解決方案
可以在某個時間點被超高併發訪問的問題,被稱作熱點資料問題,解決方案主要有:
-
預先設定熱門資料:在redis訪問高峰之前,就提前把一些熱門資料放入redis中並增大key的時長
-
實時調整:現場監控熱門資料,實時調整key的時長
-
使用鎖:即在查詢失敗的時候設定一個排它鎖,並開啟一個執行緒查詢資料庫並同步快取,查詢過程中不允許其他執行緒查詢資料庫,查詢成功後則刪除排它鎖。
3 快取雪崩
3.1 快取雪崩的現象
-
key對應的資料在資料庫中,但是在redis中短時間大量key過期
-
資料庫崩潰
3.2 快取雪崩造成的原因
key對應的資料在資料庫中,但是在redis中短時間大量key過期,導致大量請求請求key的時候就會去直接從後端DB載入資料並回設到快取,這時候大併發的請求可能會瞬間把後端DB壓垮。
快取擊穿和快取雪崩的區別在於是否出現大量key過期
3.3 解決方案
-
構建多級快取:nginx 快取 + redis 快取 + 其他快取 (ehcahe等)
-
使用鎖或者佇列:使用鎖或者佇列能夠避免有大量的執行緒對資料庫一次性讀寫,從而避免失效時大量的併發請求落在底層儲存系統上,不適用於高併發地情況。
-
設定過期標誌更新快取:記錄快取是否過期(設定提前量),如果過期會觸發通知另外的執行緒在後臺去更新實際的key快取。
-
將快取過期時間分散開
4 分散式鎖
4.1 簡介
隨著業務發展的需要,原來單體單機部署的系統被演化成為了分散式集群系統,由於分散式系統多執行緒、多程序,並且分佈在不同的系統上,這使得原來單機部署情況下的併發鎖策略失效,單純的JavaAPI並不能提供分散式鎖的能力,為了解決這個問題就需要一個跨JVM的互斥機制來控制共享資源的訪問。
4.2 分散式鎖的主流解決方案
-
基於資料庫實現分散式鎖
-
基於redis
-
基於zookeeper
4.3 設定鎖和過期時間
setnx 對key值新增鎖
新增鎖之後其他程序不能夠進行修改
del 釋放鎖
expire
上面存在的問題是鎖一直沒有釋放則導致資料一直無法訪問,解決方案是對鎖設定過期時間,時間到了會自動釋放
set nx ex
用於實現上面兩條命令的原子操作
4.4 UUID防止誤刪
分散式鎖的程式碼實現
@Autowired
StringRedisTemplate redisTemplate;
@GetMapping("/test")
public String testHandle() {
// 上鎖
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.SECONDS);
if(lock) {
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)) {
return "success";
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 釋放鎖
redisTemplate.delete("lock");
} else {
// 獲取鎖失敗,3s後再次嘗試
try {
Thread.sleep(3000);
testHandle();
} catch (InterruptedException e){
e.printStackTrace();
}
}
return "success";
}
ab壓力測試
[root@hadoop100 ~]# ab -n 1000 -c 100 http://192.168.1.108:8080/test
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 192.168.1.108 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software:
Server Hostname: 192.168.1.108
Server Port: 8080
Document Path: /test
Document Length: 7 bytes
Concurrency Level: 100
Time taken for tests: 199.311 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 139000 bytes
HTML transferred: 7000 bytes
Requests per second: 5.02 [#/sec] (mean)
Time per request: 19931.077 [ms] (mean)
Time per request: 199.311 [ms] (mean, across all concurrent requests)
Transfer rate: 0.68 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 1 3 6.4 1 36
Processing: 1 8360 30440.5 2 198574
Waiting: 1 8360 30440.5 2 198574
Total: 2 8363 30445.3 3 198580
Percentage of the requests served within a certain time (ms)
50% 3
66% 5
75% 9
80% 12
90% 3078
95% 72293
98% 141421
99% 171505
100% 198580 (longest request)
存在的問題:可能導致鎖的誤刪
比如上面我們設定了鎖的自動過期時間為3s,A上鎖執行操作的過程中出現了伺服器卡頓導致操作暫停超過了三秒,鎖被自動釋放了,這時候B搶到了鎖,然後上鎖進行一系列操作,而此時伺服器正常執行了,A又執行了沒有執行的釋放鎖操作,導致實際上是刪除了B的鎖,解決的方法是每次上鎖的時候都給鎖設定UUID,然後釋放鎖的時候檢查是不是之前自己上的鎖,防止誤刪。
@Autowired
StringRedisTemplate redisTemplate;
@GetMapping("/test")
public String testHandle() {
String uuid = UUID.randomUUID().toString();
// 上鎖
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
if(lock) {
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)) {
return "success";
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 防止誤刪
if(uuid.equals(redisTemplate.opsForValue().get("lock"))) {
// 釋放鎖
redisTemplate.delete("lock");
}
} else {
// 獲取鎖失敗,3s後再次嘗試
try {
Thread.sleep(3000);
testHandle();
} catch (InterruptedException e){
e.printStackTrace();
}
}
return "success";
}
4.5 LUA保證原子性
上面使用uuid之後,仍然會存在誤刪的風險:執行緒A在判斷uuid相同後,準備刪除lock,這時候鎖到期自動釋放了,B搶到了鎖然後設定了uuid,最後A刪掉了B的鎖。 這其實就是因為uuid判斷與刪除鎖的操作不是原子性造成的,解決方案是使用lua指令碼,保證在沒有刪除完成之前,別人是不能進行操作的。
4.6 分散式鎖可用需要滿足的條件
-
互斥性
-
不會發生死鎖:即使有客戶端在持有鎖期間沒有主動解鎖,其他客戶端也能正常獲取鎖
-
不能誤刪其他客戶端的鎖
-
加鎖和解鎖必須具有原子性