功能02-商鋪查詢緩存02知識(shí)補(bǔ)充(1)緩存穿透https://blog.csdn.net/qq_45637260/article/details/125866738緩存穿透(cachepe
功能02-商鋪查詢緩存02知識(shí)補(bǔ)充(1)緩存穿透
https://blog.csdn.net/qq_45637260/article/details/125866738
緩存穿透(cache penetration)是指用戶訪問(wèn)的數(shù)據(jù)既不在緩存當(dāng)中,也不在數(shù)據(jù)庫(kù)中。出于容錯(cuò)的考慮,如果從底層數(shù)據(jù)庫(kù)查詢不到數(shù)據(jù),則不寫(xiě)入緩存。這就導(dǎo)致每次請(qǐng)求都會(huì)到底層數(shù)據(jù)庫(kù)進(jìn)行查詢,緩存也失去了意義。當(dāng)高并發(fā)或有人利用不存在的Key頻繁攻擊時(shí),數(shù)據(jù)庫(kù)的壓力驟增,甚至崩潰,這就是緩存穿透問(wèn)題。
簡(jiǎn)單地說(shuō),緩存穿透是指用戶請(qǐng)求的數(shù)據(jù)在緩存和數(shù)據(jù)庫(kù)中都不存在,則每次請(qǐng)求都會(huì)打到數(shù)據(jù)庫(kù)中,給數(shù)據(jù)庫(kù)帶來(lái)巨大壓力。
【資料圖】
常見(jiàn)的兩種解決方案
(1)緩存空對(duì)象:是指在持久層沒(méi)有命中的情況下,對(duì)key進(jìn)行set (key,null)。
緩存空對(duì)象會(huì)有兩個(gè)問(wèn)題:
value為null 不代表不占用內(nèi)存空間,空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內(nèi)存空間,比較有效的方法是針對(duì)這類數(shù)據(jù)設(shè)置一個(gè)較短的過(guò)期時(shí)間,讓其自動(dòng)剔除。
緩存層和存儲(chǔ)層的數(shù)據(jù)會(huì)有一段時(shí)間窗口的不一致,可能會(huì)對(duì)業(yè)務(wù)有一定影響。例如過(guò)期時(shí)間設(shè)置為5分鐘,如果此時(shí)存儲(chǔ)層添加了這個(gè)數(shù)據(jù),那此段時(shí)間就會(huì)出現(xiàn)緩存層和存儲(chǔ)層數(shù)據(jù)的不一致,此時(shí)可以利用消息系統(tǒng)或者其他方式清除掉緩存層中的空對(duì)象。
(2)布隆過(guò)濾器:
在訪問(wèn)緩存層和存儲(chǔ)層之前,將存在的key用布隆過(guò)濾器提前保存起來(lái),做第一層攔截,當(dāng)收到一個(gè)對(duì)key請(qǐng)求時(shí),先用布隆過(guò)濾器驗(yàn)證是key否存在,如果存在再進(jìn)入緩存層、存儲(chǔ)層。
可以使用bitmap做布隆過(guò)濾器。這種方法適用于數(shù)據(jù)命中不高、數(shù)據(jù)相對(duì)固定、實(shí)時(shí)性低的應(yīng)用場(chǎng)景,代碼維護(hù)較為復(fù)雜,但是緩存空間占用少。
布隆過(guò)濾器實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量和一系列隨機(jī)映射函數(shù)。布隆過(guò)濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都遠(yuǎn)遠(yuǎn)超過(guò)一般的算法,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難。
布隆過(guò)濾器攔截的算法描述:
初始狀態(tài)時(shí),BloomFilter是一個(gè)長(zhǎng)度為m的位數(shù)組,每一位都置為0。添加元素x時(shí),x使用k個(gè)hash函數(shù)得到k個(gè)hash值,對(duì)m取余,對(duì)應(yīng)的bit位設(shè)置為1。
判斷y是否屬于這個(gè)集合,對(duì)y使用k個(gè)哈希函數(shù)得到k個(gè)哈希值,對(duì)m取余,所有對(duì)應(yīng)的位置都是1,則認(rèn)為y屬于該集合(哈希沖突,可能存在誤判),否則就認(rèn)為y不屬于該集合。可以通過(guò)增加哈希函數(shù)和增加二進(jìn)制位數(shù)組的長(zhǎng)度來(lái)降低錯(cuò)報(bào)率
兩種方案的比較:
| 緩存穿透的方案 | 使用場(chǎng)景 | 維護(hù)成本 |
|---|
| 緩存空對(duì)象 | 1.數(shù)據(jù)命中率不高 2.數(shù)據(jù)頻繁變化實(shí)時(shí)性高 | 1.代碼維護(hù)簡(jiǎn)單 2.需要過(guò)多的緩存空間 3.數(shù)據(jù)不一致 |
| 布隆過(guò)濾器 | 1.數(shù)據(jù)命中不高 2.數(shù)據(jù)相對(duì)固定實(shí)時(shí)性低 | 1.代碼維護(hù)復(fù)雜 2.緩存空間占用少 |
緩存穿透的解決方案還有:
(2)緩存雪崩
緩存雪崩
在使用緩存時(shí),通常會(huì)對(duì)緩存設(shè)置過(guò)期時(shí)間,一方面目的是保持緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)的一致性,另一方面是減少冷緩存占用過(guò)多的內(nèi)存空間。但當(dāng)緩存中大量熱點(diǎn)緩存采用了相同的實(shí)效時(shí)間,就會(huì)導(dǎo)致緩存在某一個(gè)時(shí)刻同時(shí)實(shí)效,請(qǐng)求全部轉(zhuǎn)發(fā)到數(shù)據(jù)庫(kù),從而導(dǎo)致數(shù)據(jù)庫(kù)壓力驟增,甚至宕機(jī)。從而形成一系列的連鎖反應(yīng),造成系統(tǒng)崩潰等情況,這就是緩存雪崩(Cache Avalanche)。
簡(jiǎn)單地說(shuō),緩存雪崩是指在同一時(shí)間段大量的熱點(diǎn)key同時(shí)失效,或者Redis服務(wù)宕機(jī),導(dǎo)致大量請(qǐng)求到達(dá)數(shù)據(jù)庫(kù),給數(shù)據(jù)庫(kù)帶來(lái)巨大壓力。
解決方案
給不同的key的TTL添加隨機(jī)值(比如隨機(jī)1-5分鐘),讓key均勻地失效利用redis集群提高服務(wù)的可用性(提高高可用性)給緩存業(yè)務(wù)添加熔斷、降級(jí)、限流策略給業(yè)務(wù)添加多級(jí)緩存(3)緩存擊穿
緩存擊穿
如果有一個(gè)熱點(diǎn)key,在不停的扛著大并發(fā),在這個(gè)key失效的瞬間,持續(xù)的大并發(fā)請(qǐng)求就會(huì)擊破緩存,直接請(qǐng)求到數(shù)據(jù)庫(kù),好像蠻力擊穿一樣。這種情況就是緩存擊穿(Cache Breakdown)。
緩存擊穿問(wèn)題也叫做熱點(diǎn)key問(wèn)題,簡(jiǎn)單來(lái)說(shuō),就是一個(gè)被高并發(fā)訪問(wèn)并且緩存重建業(yè)務(wù)較復(fù)雜的key突然失效了,無(wú)數(shù)的請(qǐng)求訪問(wèn)在瞬間給數(shù)據(jù)庫(kù)帶來(lái)巨大的沖擊。
從定義上可以看出,緩存擊穿和緩存雪崩很類似,只不過(guò)是緩存擊穿是一個(gè)熱點(diǎn)key失效,而緩存雪崩是大量熱點(diǎn)key失效。因此,可以將緩存擊穿看作是緩存雪崩的一個(gè)子集。
解決方案
方案一:使用互斥鎖(Mutex Key),只讓一個(gè)線程構(gòu)建緩存,其他線程等待構(gòu)建緩存執(zhí)行完畢,重新從緩存中獲取數(shù)據(jù)。單機(jī)通過(guò)synchronized或lock來(lái)處理,分布式環(huán)境采用分布式鎖。
方案二:邏輯過(guò)期。熱點(diǎn)數(shù)據(jù)不設(shè)置過(guò)期時(shí)間,只在value中設(shè)置邏輯上的過(guò)期時(shí)間。后臺(tái)異步更新緩存,適用于不嚴(yán)格要求緩存一致性的場(chǎng)景。
兩種方案的對(duì)比:
3.功能02-商鋪查詢緩存3.4查詢商鋪id的緩存穿透問(wèn)題3.4.3需求分析
解決查詢商鋪查詢可能存在的緩存穿透問(wèn)題:當(dāng)訪問(wèn)不存在的店鋪時(shí),請(qǐng)求會(huì)直接打到數(shù)據(jù)庫(kù)上,并且redis緩存永遠(yuǎn)不會(huì)生效。
這里使用緩存空對(duì)象的方式來(lái)解決。
3.4.4代碼實(shí)現(xiàn)
(1)修改ShopServiceImpl.java的queryById方法
@Overridepublic Result queryById(Long id) { String key = CACHE_SHOP_KEY + id; //1.從redis中查詢商鋪緩存 String shopJson = stringRedisTemplate.opsForValue().get(key); //2.判斷緩存是否命中 if (StrUtil.isNotBlank(shopJson)) { //2.1若命中,直接返回商鋪信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } //判斷命中的是否是redis的空值 if (shopJson != null) { return Result.fail("店鋪不存在!"); } //2.2未命中,根據(jù)id查詢數(shù)據(jù)庫(kù),判斷商鋪是否存在數(shù)據(jù)庫(kù)中 Shop shop = getById(id); if (shop == null) { //2.2.1不存在,防止緩存穿透,將空值存入redis,TTL設(shè)置為2min stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回錯(cuò)誤信息 return Result.fail("店鋪不存在!"); } //2.2.2存在,則將商鋪數(shù)據(jù)寫(xiě)入redis中 stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop);}
(2)測(cè)試,訪問(wèn)一個(gè)緩存和數(shù)據(jù)庫(kù)都不存在的數(shù)據(jù):
可以看到redis已經(jīng)緩存了一個(gè)空值
之后再訪問(wèn)該數(shù)據(jù),只要redis的空值對(duì)沒(méi)有過(guò)期,就不會(huì)訪問(wèn)到數(shù)據(jù)庫(kù),從而起到保護(hù)數(shù)據(jù)庫(kù)的作用。
3.5查詢商鋪id的緩存擊穿問(wèn)題
當(dāng)查詢店鋪id時(shí),可能會(huì)出現(xiàn)該店鋪id對(duì)應(yīng)的緩存失效,從而大量請(qǐng)求發(fā)送到數(shù)據(jù)庫(kù)的情況,這里使用兩種方案分別解決該問(wèn)題。
3.5.1基于互斥鎖方案解決3.5.1.1需求分析
修改根據(jù)id查詢商鋪的業(yè)務(wù),基于互斥鎖方式來(lái)解決緩存擊穿問(wèn)題。
如下,當(dāng)出現(xiàn)緩存擊穿問(wèn)題,首先需要判斷當(dāng)前的線程是否能夠獲取鎖:
若可以,則進(jìn)行緩存重建(將數(shù)據(jù)庫(kù)數(shù)據(jù)重新寫(xiě)入緩存中),然后釋放鎖。如果不能,則線程等待一段時(shí)間,然后再判斷緩存是否能命中。如果未命中,則重復(fù)獲取鎖的流程,直到緩存命中,或者獲得鎖,重建緩存。
根據(jù)redis的setnx命令,當(dāng)setnx設(shè)置某個(gè)key之后,如果該key存在,則其他線程無(wú)法設(shè)置該key。
我們可以根據(jù)這個(gè)特性,作為一個(gè)lock的邏輯標(biāo)志,當(dāng)一個(gè)線程setnx某個(gè)key后,代表獲取了“鎖”。當(dāng)刪除這個(gè)key時(shí),代表釋放“鎖”,這樣其他線程就可以重新獲取“鎖”。此外,可以對(duì)該key設(shè)置一個(gè)有效期,防止刪除key失敗,產(chǎn)生“死鎖”。
3.5.1.2代碼實(shí)現(xiàn)
(1)修改 ShopServiceImpl.java
package com.hmdp.service.impl;import .../** * 服務(wù)實(shí)現(xiàn)類 * * @author 李 * @version 1.0 */@Servicepublic class ShopServiceImpl extends ServiceImpl implements IShopService { @Resource StringRedisTemplate stringRedisTemplate; @Override public Result queryById(Long id) { Shop shop = queryWithMutex(id); if (shop == null) { return Result.fail("店鋪不存在!"); } return Result.ok(shop); } //緩存穿透(存儲(chǔ)空對(duì)象)+緩存擊穿解決(互斥鎖解決) public Shop queryWithMutex(Long id) { String key = CACHE_SHOP_KEY + id; //從redis中查詢商鋪緩存 String shopJson = stringRedisTemplate.opsForValue().get(key); //判斷緩存是否命中 if (StrUtil.isNotBlank(shopJson)) { //命中,直接返回商鋪信息 return JSONUtil.toBean(shopJson, Shop.class); } //判斷命中的是否是redis的空值(緩存擊穿解決) if (shopJson != null) { return null; } //未命中,嘗試獲取互斥鎖 String lockKey = "lock:shop:" + id; boolean isLock = false; Shop shop = null; try { //獲取互斥鎖 isLock = tryLock(lockKey); //判斷是否獲取成功 if (!isLock) {//失敗 //等待并重試 Thread.sleep(50); //直到緩存命中,或者獲取到鎖 return queryWithMutex(id); } //獲取鎖成功,開(kāi)始重建緩存 //根據(jù)id查詢數(shù)據(jù)庫(kù),判斷商鋪是否存在數(shù)據(jù)庫(kù)中 shop = getById(id); //模擬重建緩存的延遲----------- Thread.sleep(200); if (shop == null) { //不存在,防止緩存穿透,將空值存入redis,TTL設(shè)置為2min stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); //返回錯(cuò)誤信息 return null; } //存在,則將商鋪數(shù)據(jù)寫(xiě)入redis中 stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { //釋放互斥鎖 unLock(lockKey); } //返回從緩存或數(shù)據(jù)庫(kù)中查到的數(shù)據(jù) return shop; } //緩存穿透方案// public Shop queryWithPassThrough(Long id) {// String key = CACHE_SHOP_KEY + id;// //1.從redis中查詢商鋪緩存// String shopJson = stringRedisTemplate.opsForValue().get(key);// //2.判斷緩存是否命中// if (StrUtil.isNotBlank(shopJson)) {// //2.1若命中,直接返回商鋪信息// return JSONUtil.toBean(shopJson, Shop.class);// }// //判斷命中的是否是redis的空值// if (shopJson != null) {// return null;// }// //2.2未命中,根據(jù)id查詢數(shù)據(jù)庫(kù),判斷商鋪是否存在數(shù)據(jù)庫(kù)中// Shop shop = getById(id);// if (shop == null) {// //2.2.1不存在,防止緩存穿透,將空值存入redis,TTL設(shè)置為2min// stringRedisTemplate.opsForValue().set(key, "",// CACHE_NULL_TTL, TimeUnit.MINUTES);// //返回錯(cuò)誤信息// return null;// }// //2.2.2存在,則將商鋪數(shù)據(jù)寫(xiě)入redis中// stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),// CACHE_SHOP_TTL, TimeUnit.MINUTES);// return shop;// } private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue() .setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); } @Override @Transactional public Result update(Shop shop) { Long id = shop.getId(); if (id == null) { return Result.fail("店鋪id不能為空"); } //1.更新數(shù)據(jù)庫(kù) updateById(shop); //2.刪除redis緩存 stringRedisTemplate.delete(CACHE_SHOP_KEY + id); return Result.ok(); }}
(2)使用jemeter模擬高并發(fā)的情況:
5秒發(fā)起1000個(gè)請(qǐng)求線程:
模擬http請(qǐng)求:
全部請(qǐng)求成功,獲取到數(shù)據(jù):
在服務(wù)器的控制臺(tái)中可以看到:對(duì)于數(shù)據(jù)庫(kù)的請(qǐng)求只觸發(fā)了一次,證明在高并發(fā)的場(chǎng)景下,只有一個(gè)線程對(duì)數(shù)據(jù)庫(kù)發(fā)起請(qǐng)求,并對(duì)redis對(duì)應(yīng)的緩存重新設(shè)置。
3.5.2基于邏輯過(guò)期方案解決
關(guān)鍵詞: