1坚俗、緩存穿透
1、問題描述
key對(duì)應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在帆啃,每次針對(duì)此key的請(qǐng)求從緩存獲取不到,請(qǐng)求都會(huì)壓到數(shù)據(jù)源,從而可能壓垮數(shù)據(jù)源摊唇。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫都沒有涯鲁,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫巷查。
2、解決方案
一個(gè)一定不存在緩存及查詢不到的數(shù)據(jù)撮竿,由于緩存是不命中時(shí)被動(dòng)寫的吮便,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存幢踏,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢髓需,失去了緩存的意義。
解決方案:
(1)對(duì)空值緩存:如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)是否不存在)房蝉,我們?nèi)匀话堰@個(gè)空結(jié)果(null)進(jìn)行緩存僚匆,設(shè)置空結(jié)果的過期時(shí)間會(huì)很短,最長(zhǎng)不超過五分鐘
(2)設(shè)置可訪問的名單(白名單):
使用bitmaps類型定義一個(gè)可以訪問的名單搭幻,名單id作為bitmaps的偏移量咧擂,每次訪問和bitmap里面的id進(jìn)行比較,如果訪問id不在bitmaps里面檀蹋,進(jìn)行攔截松申,不允許訪問。
(3)采用布隆過濾器:(布隆過濾器(Bloom Filter)是1970年由布隆提出的俯逾。它實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量(位圖)和一系列隨機(jī)映射函數(shù)(哈希函數(shù))贸桶。
布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過一般的算法桌肴,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難皇筛。)
將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmaps中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmaps攔截掉坠七,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力水醋。
(4)進(jìn)行實(shí)時(shí)監(jiān)控:當(dāng)發(fā)現(xiàn)Redis的命中率開始急速降低,需要排查訪問對(duì)象和訪問的數(shù)據(jù)彪置,和運(yùn)維人員配合拄踪,可以設(shè)置黑名單限制服務(wù)
2、緩存擊穿
1拳魁、問題描述
key對(duì)應(yīng)的數(shù)據(jù)存在宫蛆,但在redis中過期,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存耀盗,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮想虎。
2、解決方案
key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問叛拷,是一種非成喑“熱點(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候忿薇,需要考慮一個(gè)問題:緩存被“擊穿”的問題裙椭。
解決問題:
(1)預(yù)先設(shè)置熱門數(shù)據(jù):在redis高峰訪問之前,把一些熱門數(shù)據(jù)提前存入到redis里面署浩,加大這些熱門數(shù)據(jù)key的時(shí)長(zhǎng)
(2)實(shí)時(shí)調(diào)整:現(xiàn)場(chǎng)監(jiān)控哪些數(shù)據(jù)熱門揉燃,實(shí)時(shí)調(diào)整key的過期時(shí)長(zhǎng)
(3)使用鎖:
(1)就是在緩存失效的時(shí)候(判斷拿出來的值為空),不是立即去load db筋栋。
(2)先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX)去set一個(gè)mutex key
(3)當(dāng)操作返回成功時(shí)炊汤,再進(jìn)行l(wèi)oad db的操作,并回設(shè)緩存,最后刪除mutex key弊攘;
(4)當(dāng)操作返回失敗抢腐,證明有線程在load db,當(dāng)前線程睡眠一段時(shí)間再重試整個(gè)get緩存的方法襟交。
3迈倍、緩存雪崩
1、問題描述
key對(duì)應(yīng)的數(shù)據(jù)存在捣域,但在redis中過期啼染,此時(shí)若有大量并發(fā)請(qǐng)求過來,這些請(qǐng)求發(fā)現(xiàn)緩存過期一般都會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存焕梅,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮提完。
緩存雪崩與緩存擊穿的區(qū)別在于這里針對(duì)很多key緩存,前者則是某一個(gè)key
正常訪問
緩存失效瞬間
2丘侠、解決方案
緩存失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非常可怕逐样!
解決方案:
(1)構(gòu)建多級(jí)緩存架構(gòu):nginx緩存 + redis緩存 +其他緩存(ehcache等)
(2)使用鎖或隊(duì)列:
用加鎖或者隊(duì)列的方式保證來保證不會(huì)有大量的線程對(duì)數(shù)據(jù)庫一次性進(jìn)行讀寫蜗字,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上。不適用高并發(fā)情況
(3)設(shè)置過期標(biāo)志更新緩存:
記錄緩存數(shù)據(jù)是否過期(設(shè)置提前量)脂新,如果過期會(huì)觸發(fā)通知另外的線程在后臺(tái)去更新實(shí)際key的緩存挪捕。
(4)將緩存失效時(shí)間分散開:
比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī)争便,這樣每一個(gè)緩存的過期時(shí)間的重復(fù)率就會(huì)降低级零,就很難引發(fā)集體失效的事件。
4、分布式鎖
1奏纪、問題描述
隨著業(yè)務(wù)發(fā)展的需要鉴嗤,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程序调、多進(jìn)程并且分布在不同機(jī)器上醉锅,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力发绢。為了解決這個(gè)問題就需要一種跨JVM的互斥機(jī)制來控制共享資源的訪問硬耍,這就是分布式鎖要解決的問題!
分布式鎖主流的實(shí)現(xiàn)方案:
- 基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖
- 基于緩存(Redis等)
- 基于Zookeeper
每一種分布式鎖解決方案都有各自的優(yōu)缺點(diǎn): - 性能:redis最高
- 可靠性:zookeeper最高
這里边酒,我們就基于redis實(shí)現(xiàn)分布式鎖经柴。
2、解決方案:使用redis實(shí)現(xiàn)分布式鎖
redis:命令
set sku:1:info “OK” NX PX 10000
EX second :設(shè)置鍵的過期時(shí)間為 second 秒墩朦。 SET key value EX second 效果等同于 SETEX key second value 坯认。
PX millisecond :設(shè)置鍵的過期時(shí)間為 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 介杆。
NX :只在鍵不存在時(shí)鹃操,才對(duì)鍵進(jìn)行設(shè)置操作。 SET key value NX 效果等同于 SETNX key value 春哨。
XX :只在鍵已經(jīng)存在時(shí)荆隘,才對(duì)鍵進(jìn)行設(shè)置操作。
- 多個(gè)客戶端同時(shí)獲取鎖(setnx)
- 獲取成功赴背,執(zhí)行業(yè)務(wù)邏輯{從db獲取數(shù)據(jù)椰拒,放入緩存},執(zhí)行完成釋放鎖(del)
- 其他客戶端等待重試
3凰荚、編寫代碼
Redis:set num 0
@GetMapping("testLock")
public void testLock(){
//1獲取鎖燃观,setne
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
//2獲取鎖成功、查詢num的值
if(lock){
Object value = redisTemplate.opsForValue().get("num");
//2.1判斷num為空return
if(StringUtils.isEmpty(value)){
return;
}
//2.2有值就轉(zhuǎn)成成int
int num = Integer.parseInt(value+"");
//2.3把redis的num加1
redisTemplate.opsForValue().set("num", ++num);
//2.4釋放鎖便瑟,del
redisTemplate.delete("lock");
}else{
//3獲取鎖失敗缆毁、每隔0.1秒再獲取
try {
Thread.sleep(100);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
重啟,服務(wù)集群到涂,通過網(wǎng)關(guān)壓力測(cè)試:
ab -n 1000 -c 100 http://192.168.140.1:8080/test/testLock
查看redis中num的值:
基本實(shí)現(xiàn)脊框。
問題:setnx剛好獲取到鎖,業(yè)務(wù)邏輯出現(xiàn)異常践啄,導(dǎo)致鎖無法釋放
解決:設(shè)置過期時(shí)間浇雹,自動(dòng)釋放鎖。
4屿讽、優(yōu)化之設(shè)置鎖的過期時(shí)間
設(shè)置過期時(shí)間有兩種方式:
- 首先想到通過expire設(shè)置過期時(shí)間(缺乏原子性:如果在setnx和expire之間出現(xiàn)異常昭灵,鎖也無法釋放)
-
在set時(shí)指定過期時(shí)間(推薦)
設(shè)置過期時(shí)間:
壓力測(cè)試肯定也沒有問題。自行測(cè)試
問題:可能會(huì)釋放其他服務(wù)器的鎖。
場(chǎng)景:如果業(yè)務(wù)邏輯的執(zhí)行時(shí)間是7s烂完。執(zhí)行流程如下
- index1業(yè)務(wù)邏輯沒執(zhí)行完试疙,3秒后鎖被自動(dòng)釋放。
- index2獲取到鎖窜护,執(zhí)行業(yè)務(wù)邏輯效斑,3秒后鎖被自動(dòng)釋放。
- index3獲取到鎖柱徙,執(zhí)行業(yè)務(wù)邏輯
- index1業(yè)務(wù)邏輯執(zhí)行完成缓屠,開始調(diào)用del釋放鎖,這時(shí)釋放的是index3的鎖护侮,導(dǎo)致index3的業(yè)務(wù)只執(zhí)行1s就被別人釋放敌完。
最終等于沒鎖的情況。
解決:setnx獲取鎖時(shí)羊初,設(shè)置一個(gè)指定的唯一值(例如:uuid)滨溉;釋放前獲取這個(gè)值,判斷是否自己的鎖
5长赞、優(yōu)化之UUID防誤刪
問題:刪除操作缺乏原子性晦攒。
場(chǎng)景:
index1執(zhí)行刪除時(shí),查詢到的lock值確實(shí)和uuid相等
uuid=v1
set(lock,uuid)得哆;index1執(zhí)行刪除前脯颜,lock剛好過期時(shí)間已到,被redis自動(dòng)釋放
在redis中沒有了lock贩据,沒有了鎖栋操。index2獲取了lock
index2線程獲取到了cpu的資源,開始執(zhí)行方法
uuid=v2
set(lock,uuid)饱亮;index1執(zhí)行刪除矾芙,此時(shí)會(huì)把index2的lock刪除
index1 因?yàn)橐呀?jīng)在方法中了,所以不需要重新上鎖近上。index1有執(zhí)行的權(quán)限剔宪。index1已經(jīng)比較完成了,這個(gè)時(shí)候壹无,開始執(zhí)行
刪除的index2的鎖葱绒!
6、優(yōu)化之LUA腳本保證刪除的原子性
@GetMapping("testLockLua")
public void testLockLua() {
//1 聲明一個(gè)uuid ,將做為一個(gè)value 放入我們的key所對(duì)應(yīng)的值中
String uuid = UUID.randomUUID().toString();
//2 定義一個(gè)鎖:lua 腳本可以使用同一把鎖格遭,來實(shí)現(xiàn)刪除!
String skuId = "25"; // 訪問skuId 為25號(hào)的商品 100008348542
String locKey = "lock:" + skuId; // 鎖住的是每個(gè)商品的數(shù)據(jù)
// 3 獲取鎖
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一種: lock 與過期時(shí)間中間不寫任何的代碼留瞳。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//設(shè)置過期時(shí)間
// 如果true
if (lock) {
// 執(zhí)行的業(yè)務(wù)邏輯開始
// 獲取緩存中的num 數(shù)據(jù)
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果說在這出現(xiàn)了異常拒迅! 那么delete 就刪除失敗! 也就是說鎖永遠(yuǎn)存在璧微!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入緩存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua腳本來鎖*/
// 定義lua 腳本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis執(zhí)行l(wèi)ua執(zhí)行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 設(shè)置一下返回值類型 為L(zhǎng)ong
// 因?yàn)閯h除判斷的時(shí)候作箍,返回的0,給其封裝為數(shù)據(jù)類型。如果不封裝那么默認(rèn)返回String 類型前硫,
// 那么返回字符串與0 會(huì)有發(fā)生錯(cuò)誤胞得。
redisScript.setResultType(Long.class);
// 第一個(gè)要是script 腳本 ,第二個(gè)需要判斷的key屹电,第三個(gè)就是key所對(duì)應(yīng)的值阶剑。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他線程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,調(diào)用方法危号。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Lua 腳本詳解:
項(xiàng)目中正確使用:
-
定義key牧愁,key應(yīng)該是為每個(gè)sku定義的,也就是每個(gè)sku有一把鎖外莲。
String locKey ="lock:"+skuId; // 鎖住的是每個(gè)商品的數(shù)據(jù)
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid,3,TimeUnit.SECONDS);
7猪半、總結(jié)
1、加鎖
// 1. 從redis中獲取鎖,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);
2偷线、使用lua釋放鎖
// 2. 釋放鎖 del
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 設(shè)置lua腳本返回的數(shù)據(jù)類型
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 設(shè)置lua腳本返回類型為L(zhǎng)ong
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);
3磨确、重試
Thread.sleep(500);
testLock();
為了確保分布式鎖可用,我們至少要確保鎖的實(shí)現(xiàn)同時(shí)滿足以下四個(gè)條件:
- 互斥性声邦。在任意時(shí)刻乏奥,只有一個(gè)客戶端能持有鎖。
- 不會(huì)發(fā)生死鎖翔忽。即使有一個(gè)客戶端在持有鎖的期間崩潰而沒有主動(dòng)解鎖英融,也能保證后續(xù)其他客戶端能加鎖。
- 解鈴還須系鈴人歇式。加鎖和解鎖必須是同一個(gè)客戶端驶悟,客戶端自己不能把別人加的鎖給解了。
- 加鎖和解鎖必須具有原子性材失。