十四、Redis應(yīng)用問題解決

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)方案:

  1. 基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖
  2. 基于緩存(Redis等)
  3. 基于Zookeeper
    每一種分布式鎖解決方案都有各自的優(yōu)缺點(diǎn):
  4. 性能:redis最高
  5. 可靠性: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è)置操作。

  1. 多個(gè)客戶端同時(shí)獲取鎖(setnx)
  2. 獲取成功赴背,執(zhí)行業(yè)務(wù)邏輯{從db獲取數(shù)據(jù)椰拒,放入緩存},執(zhí)行完成釋放鎖(del)
  3. 其他客戶端等待重試

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í)間有兩種方式:

  1. 首先想到通過expire設(shè)置過期時(shí)間(缺乏原子性:如果在setnx和expire之間出現(xiàn)異常昭灵,鎖也無法釋放)
  2. 在set時(shí)指定過期時(shí)間(推薦)


設(shè)置過期時(shí)間:


壓力測(cè)試肯定也沒有問題。自行測(cè)試
問題:可能會(huì)釋放其他服務(wù)器的鎖。

場(chǎng)景:如果業(yè)務(wù)邏輯的執(zhí)行時(shí)間是7s烂完。執(zhí)行流程如下

  1. index1業(yè)務(wù)邏輯沒執(zhí)行完试疙,3秒后鎖被自動(dòng)釋放。
  2. index2獲取到鎖窜护,執(zhí)行業(yè)務(wù)邏輯效斑,3秒后鎖被自動(dòng)釋放。
  3. index3獲取到鎖柱徙,執(zhí)行業(yè)務(wù)邏輯
  4. 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)景:

  1. index1執(zhí)行刪除時(shí),查詢到的lock值確實(shí)和uuid相等
    uuid=v1
    set(lock,uuid)得哆;

  2. index1執(zhí)行刪除前脯颜,lock剛好過期時(shí)間已到,被redis自動(dòng)釋放
    在redis中沒有了lock贩据,沒有了鎖栋操。

  3. index2獲取了lock
    index2線程獲取到了cpu的資源,開始執(zhí)行方法
    uuid=v2
    set(lock,uuid)饱亮;

  4. 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)目中正確使用:

  1. 定義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è)客戶端驶悟,客戶端自己不能把別人加的鎖給解了。
  • 加鎖和解鎖必須具有原子性材失。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痕鳍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子龙巨,更是在濱河造成了極大的恐慌笼呆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旨别,死亡現(xiàn)場(chǎng)離奇詭異诗赌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)秸弛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門铭若,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洪碳,“玉大人,你說我怎么就攤上這事叼屠⊥纾” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵镜雨,是天一觀的道長(zhǎng)嫂侍。 經(jīng)常有香客問我,道長(zhǎng)荚坞,這世上最難降的妖魔是什么挑宠? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮西剥,結(jié)果婚禮上痹栖,老公的妹妹穿的比我還像新娘。我一直安慰自己瞭空,他們只是感情好揪阿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咆畏,像睡著了一般南捂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旧找,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天溺健,我揣著相機(jī)與錄音,去河邊找鬼钮蛛。 笑死鞭缭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的魏颓。 我是一名探鬼主播岭辣,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼甸饱!你這毒婦竟也來了沦童?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤叹话,失蹤者是張志新(化名)和其女友劉穎偷遗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體驼壶,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡氏豌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了热凹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泵喘。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瞭吃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出涣旨,到底是詐尸還是另有隱情,我是刑警寧澤股冗,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布霹陡,位于F島的核電站,受9級(jí)特大地震影響止状,放射性物質(zhì)發(fā)生泄漏烹棉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一怯疤、第九天 我趴在偏房一處隱蔽的房頂上張望浆洗。 院中可真熱鬧,春花似錦集峦、人聲如沸伏社。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摘昌。三九已至,卻和暖如春高蜂,著一層夾襖步出監(jiān)牢的瞬間聪黎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工备恤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稿饰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓露泊,卻偏偏與公主長(zhǎng)得像喉镰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滤淳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容