Redis 必知必會(huì)

1、Redis 的過期鍵是如何刪除的?

按官方的解釋,有主動(dòng)和被動(dòng)兩種策略

策略 優(yōu)勢(shì) 劣勢(shì)
主動(dòng)刪除 減少了對(duì)CPU和內(nèi)存的影響 難以確定操作執(zhí)行的時(shí)長(zhǎng)和頻率
被動(dòng)刪除 CPU友好 內(nèi)存不友好
  • redis刪除過期鍵采用了惰性刪除和定期刪除相結(jié)合的策略,惰性刪除則是在每次GET/SET操作時(shí)去刪,定期刪除,則是在時(shí)間事件中,從整個(gè)key空間隨機(jī)取樣,直到過期鍵比率小于25%,如果同時(shí)有大量key過期的話,極可能導(dǎo)致主線程阻塞。一般可以通過做散列來優(yōu)化處理。
  • 在每一個(gè)定期刪除循環(huán)中,Redis 會(huì)遍歷 DB。如果這個(gè) DB 完全沒有設(shè)置了過期時(shí)間的 key,那就直接跳過。否則就針對(duì)這個(gè) DB 抽一批 key,如果 key 已經(jīng)過期,就直接刪除。 如果在這一批 key 里面,過期的比例太低,那么就會(huì)中斷循環(huán),遍歷下一個(gè) DB。如果執(zhí)行時(shí)間超過了閾值,也會(huì)中斷。不過這個(gè)中斷是整個(gè)中斷,下一次定期刪除的時(shí)候會(huì)從當(dāng)前 DB 的下一個(gè)繼續(xù)遍歷。 總的來說,Redis 是通過控制執(zhí)行定期刪除循環(huán)時(shí)間來控制開銷,這樣可以在服務(wù)正常請(qǐng)求和清理過期 key 之間取得平衡。
  • 具體參考另一篇詳細(xì)介紹:Redis 的過期鍵是如何刪除的
2、Redis 的淘汰策略有哪些?

當(dāng)Redis的內(nèi)存空間已經(jīng)用滿時(shí),Redis將根據(jù)配置的淘汰策略(maxmemory-policy),進(jìn)行相應(yīng)的動(dòng)作。某司Redis的淘汰策略共分為以下六種,默認(rèn)no-eviction:

  • no-eviction:不刪除策略,當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:直接返回錯(cuò)誤。
  • allkeys-lru:當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:在所有的key中,挑選最近最少使用(LRU)的key淘汰
  • volatile-lru:當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:在設(shè)置了expire(過期時(shí)間)的key中,挑選最近最少使用(LRU)的key淘汰
  • allkeys-random:當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:在所有的key中,隨機(jī)淘汰部分key
  • volatile-random:當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:在設(shè)置了expire(過期時(shí)間)的key中,隨機(jī)淘汰部分key
  • volatile-ttl:當(dāng)達(dá)到最大內(nèi)存限制時(shí),如果還需要更多的內(nèi)存:在設(shè)置了expire(過期時(shí)間)的key中,挑選TTL(time to live,剩余時(shí)間)短的key淘汰
3、常見的緩存模式有哪些,優(yōu)缺點(diǎn)?
3.1 Cache Aside

這種模式通常是平時(shí)應(yīng)用最廣泛的一種模式,沒有單獨(dú)的緩存維護(hù)組件,緩存和db的讀寫操作由應(yīng)用方負(fù)責(zé),對(duì)于讀寫請(qǐng)求分別為請(qǐng)求讀:先讀緩存,若命中則返回。若沒有命中,從數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)寫入緩存并返回請(qǐng)求寫:先更新數(shù)據(jù)庫(kù),然后將緩存中的數(shù)據(jù)失效掉(注意是失效而不是更新)

通常在應(yīng)用中,寫緩存和寫入數(shù)據(jù)庫(kù)是兩個(gè)獨(dú)立的事務(wù),選擇先更新緩存還是先更新數(shù)據(jù)庫(kù)在高并發(fā)的情況下,都有可能會(huì)產(chǎn)生數(shù)據(jù)不一致,如以下情況,注:拋開因?yàn)槿鐚憯?shù)據(jù)庫(kù)失敗或?qū)懢彺媸≡斐刹灰恢碌囊蛩亍?br> 問題1:為什么不是先刪緩存,再更新數(shù)據(jù)庫(kù)?
回答:這種情況,當(dāng)同時(shí)2個(gè)并發(fā)的讀和寫請(qǐng)求容易導(dǎo)致臟數(shù)據(jù)。試想同時(shí)有讀寫2個(gè)請(qǐng)求。1. 寫請(qǐng)求A首先刪除了緩存,并刪除成功,這時(shí)還未開始更新數(shù)據(jù)庫(kù)。2. 讀請(qǐng)求B查詢緩存未命中,然后查詢數(shù)據(jù)庫(kù),查詢出了舊數(shù)據(jù)并將舊數(shù)據(jù)寫入緩存。3. 寫請(qǐng)求A繼續(xù)將新數(shù)據(jù)寫入數(shù)據(jù)庫(kù)。4. 此時(shí)緩存中的數(shù)據(jù)就出現(xiàn)了不一致,并且一直臟下去。

問題2:為什么更新操作是將cache失效,而不是更新?
回答:這種情況會(huì)造成2方面的問題,
1.同時(shí)2個(gè)并發(fā)的寫請(qǐng)求時(shí)可能會(huì)導(dǎo)致臟數(shù)據(jù)。2.違背數(shù)據(jù)懶加載。

  • 同時(shí)2個(gè)并發(fā)的寫請(qǐng)求時(shí)可能會(huì)導(dǎo)致臟數(shù)據(jù)
    • 寫請(qǐng)求A先更新了數(shù)據(jù)庫(kù)。
    • 之后寫請(qǐng)求B成功更新了數(shù)據(jù)庫(kù),并成功更新了緩存。
    • 寫請(qǐng)求A最后更新了緩存,此時(shí)寫請(qǐng)求A的數(shù)據(jù)已經(jīng)是臟數(shù)據(jù),造成了不一致,并且會(huì)一致臟下去。
  • 違背數(shù)據(jù)懶加載,避免不必要的計(jì)算消耗:有些緩存值是需要經(jīng)過復(fù)雜的計(jì)算才能得出,如果每次更新數(shù)據(jù)的時(shí)候都更新緩存,但是后續(xù)在一段時(shí)間內(nèi)并沒有讀取該緩存數(shù)據(jù),這樣就白白浪費(fèi)了大量的計(jì)算性能,完全可以后續(xù)由讀請(qǐng)求的時(shí)候,再去計(jì)算即可,這樣更符合數(shù)據(jù)懶加載,降低計(jì)算開銷。

Cache Aside Pattern模式也會(huì)出現(xiàn)不一致的問題實(shí)際上先更新db,再失效cache這種模式理論上也可能出現(xiàn)問題,只是相對(duì)于以上的更新順序,出現(xiàn)不一致的幾率會(huì)更小。

  • 讀請(qǐng)求A首先讀取緩存未命中,這個(gè)時(shí)候去讀數(shù)據(jù)庫(kù)成功查詢到數(shù)據(jù)。
  • 寫請(qǐng)求B進(jìn)來更新數(shù)據(jù)庫(kù)成功,并刪除緩存的數(shù)據(jù)成功。
  • 最后請(qǐng)求A再將查詢的數(shù)據(jù)寫入到緩存中,而此時(shí)請(qǐng)求A寫入的數(shù)據(jù)已經(jīng)是臟數(shù)據(jù),造成了數(shù)據(jù)不一致。

之所以建議用這種更新順序,因?yàn)槔碚撋显斐刹灰恢碌膸茁蕰?huì)比較小,要達(dá)到不一致需要讀請(qǐng)求要先與寫請(qǐng)求查詢,然后后與寫請(qǐng)求返回,通常來說數(shù)據(jù)庫(kù)的查詢的耗時(shí)會(huì)小于數(shù)據(jù)庫(kù)寫入的耗時(shí),所以這種問題出現(xiàn)概率會(huì)比較小。

3.2 Read/Write Through Pattern

Cache Aside Pattern模式中由應(yīng)用方維護(hù)數(shù)據(jù)庫(kù)和緩存的讀寫,導(dǎo)致應(yīng)用方數(shù)據(jù)庫(kù)和緩存的維護(hù)設(shè)計(jì)侵入代碼,數(shù)據(jù)層的耦合增大,代碼復(fù)雜性增加。而Read/Write Through Pattern模式彌補(bǔ)了這一問題,調(diào)用方無需管理緩存和數(shù)據(jù)庫(kù)調(diào)用,通過在設(shè)計(jì)中多抽象出一層緩存管理組件來負(fù)責(zé)和緩存和數(shù)據(jù)庫(kù)讀寫維護(hù),并且緩存和數(shù)據(jù)庫(kù)的讀寫維護(hù)是同步的。調(diào)用方直接和緩存管理組件打交道,緩存和數(shù)據(jù)庫(kù)對(duì)調(diào)用方是透明的視為一個(gè)整體。通過分離出緩存管理組件,解耦業(yè)務(wù)代碼。

Read Through:應(yīng)用向緩存管理組件發(fā)送查詢請(qǐng)求,由緩存管理組件查詢緩存,若緩存未命中,查詢數(shù)據(jù)庫(kù),并將查詢的數(shù)據(jù)寫入緩存,并返回給應(yīng)用。

Write Through:Write Through 套路和Read Through相仿,當(dāng)更新數(shù)據(jù)的時(shí)候,將請(qǐng)求發(fā)送給緩存管理組件,由緩存管理組件同步更數(shù)據(jù)庫(kù)和緩存數(shù)據(jù)。

3.3 Write Behind Caching Pattern

Write Behind模式和Write Through模式整個(gè)架構(gòu)是一樣的,最核心的一點(diǎn)在于write through在緩存數(shù)據(jù)庫(kù)中的更新是同步的,而Write Behind是異步的。每次的請(qǐng)求寫都是直接更新緩存然后就成功返回,并沒有同步把數(shù)據(jù)更新到數(shù)據(jù)庫(kù)。而把更新到數(shù)據(jù)庫(kù)的過程稱為flush,觸發(fā)flush的條件可自定義,如定時(shí)或達(dá)到一定容量閾值時(shí)進(jìn)行flush操作。并且可以實(shí)現(xiàn)批量寫,合并寫等策略,也有效減少了更新數(shù)據(jù)的頻率,這種模式最大的好處就是讀寫響應(yīng)非??欤掏铝恳矔?huì)明顯提升,因?yàn)槎际歉鷆ache交互。當(dāng)然這種模式也有其他的問題。例如:數(shù)據(jù)不是強(qiáng)一致性的,因?yàn)檫x擇了把最新的數(shù)據(jù)放在緩存里,如果緩存在flush到數(shù)據(jù)庫(kù)之前宕機(jī)了就會(huì)丟失數(shù)據(jù),另外實(shí)現(xiàn)也是最復(fù)雜的。

幾種模式的優(yōu)缺點(diǎn):

模式 優(yōu)點(diǎn) 缺點(diǎn)
Cache Aside 1.實(shí)現(xiàn)簡(jiǎn)單 1.需要調(diào)用方維護(hù)緩存和db的更新邏輯
2.代碼侵入大
Read/Write Through 1.引入緩存管理組件,緩存和數(shù)據(jù)庫(kù)的維護(hù)對(duì)應(yīng)用方式透明的
2.應(yīng)用代碼入侵小,邏輯更清晰
1.引入緩存管理組件,實(shí)現(xiàn)更復(fù)雜
Write Behind Caching 1.讀寫直接和緩存打交道,異步批量更新數(shù)據(jù)庫(kù),性能最好
2.緩存和數(shù)據(jù)庫(kù)對(duì)應(yīng)用方透明
1.實(shí)現(xiàn)最復(fù)雜
2.數(shù)據(jù)丟失的風(fēng)險(xiǎn)
3.一致性最弱
4、如何保證緩存一致性問題?
  • 不一致的原因:部分失敗或并發(fā)導(dǎo)致的
  • 互聯(lián)網(wǎng)場(chǎng)景下,一般追求的最終一致性
  • 方案 1:重試機(jī)制
  • 方案 2:重試+binlog

通過引入Canal和緩存管理組件,將緩存更新的維護(hù)和業(yè)務(wù)代碼解耦。另一個(gè)原因是,現(xiàn)在的數(shù)據(jù)庫(kù)通常是主從架構(gòu)來提升整體的查詢qps,因數(shù)據(jù)庫(kù)主從同步的延遲,刪除緩存后,如果此時(shí)從數(shù)據(jù)庫(kù)還未同步完成,新來的請(qǐng)求發(fā)現(xiàn)緩存失效了,從從庫(kù)里查詢了已經(jīng)過期的數(shù)據(jù)放到緩存中,也會(huì)造成數(shù)據(jù)的不一致。而通過訂閱binlog的同步的延遲性,使刪除緩存的時(shí)序延后,進(jìn)一步降低不一致的幾率。

5、如何解決緩存穿透、擊穿、雪崩、大Key、熱Key問題?
5.1 緩存穿透

概念:如果大量的非法請(qǐng)求都去查詢壓根數(shù)據(jù)庫(kù)中根本就不存在的數(shù)據(jù),也就是緩存和數(shù)據(jù)庫(kù)都查詢不到這條數(shù)據(jù),但是請(qǐng)求每次都會(huì)打到數(shù)據(jù)庫(kù)上面去,緩存就形同虛設(shè),緩存命中率為0,這種情況我們稱之為緩存穿透。

解決方案:

  • 業(yè)務(wù)非法參數(shù)校驗(yàn)
    在上層業(yè)務(wù)上做非法參數(shù)校驗(yàn),盡量避免非法參數(shù)的請(qǐng)求case打到cache層。
  • 緩存空對(duì)象
    因?yàn)槊看尾榫彺娑疾淮嬖冢缓蠡厮莸絛b去查詢也不存在。因此可以把這種不存在的key也緩存起來,設(shè)置標(biāo)識(shí)空的標(biāo)識(shí)值,如“##”,那么就無法穿透到db層,但是要記到設(shè)置過期時(shí)間。這種方式的好處在于實(shí)現(xiàn)簡(jiǎn)單,但是會(huì)占用緩存空間,如果空數(shù)據(jù)的命中率不高,而且遇到的比較多非法請(qǐng)求時(shí),會(huì)增加緩存空間的壓力。
public Object getCache(final String key) {
        Object value = redis.get(key);
        if (value != null) {
            if (value.equals("##")) {
                return null;
            }
            return value;
        }
        Object valueFromDb = getValueFromDb(key);
        if (value == null) {
            valueFromDb = "##"; //"##"緩存標(biāo)識(shí)為空
        }
        redis.set(key, valueFromDb, t);
        return valueFromDb;
    }
  • 布隆過濾器
    在緩存前加一層布隆過濾器,利用布隆過濾器bitset存儲(chǔ)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)庫(kù)中所有值,查詢緩存前,先查詢布隆過濾器,若一定不存在就返回,不用再回溯流量到緩存服務(wù),過程如下:


private final BloomFilter<String> bloomFilter =
            BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1024 * 1024 * 32);

    //查詢布隆過濾器中是否存在
    public boolean contains(String cacheKey) {
        if (StringUtils.isEmpty(cacheKey)) {
            return true;
        }
        boolean exists = bloomFilter.mightContain(cacheKey);
        if (!exists) {
            bloomFilter.put(cacheKey);
        }
        return exists;
    }

    //模擬初始化布隆過濾器,從db填充所有數(shù)據(jù)
    public void initBF() {
        int offset = 0;
        int limit = 200;
        while (true) {
            List<String> dataFromDb = listFromDb(offset, limit);
            if (CollectionUtils.isEmpty(dataFromDb)) {
                break;
            }
            for (String s : dataFromDb) {
                bloomFilter.put(s);
            }
            offset += limit;
        }
    }
5.2 緩存擊穿
  • 概念:當(dāng)某一個(gè)熱點(diǎn)key失效的時(shí)候,很多請(qǐng)求這一時(shí)間都查不到緩存,然后全部請(qǐng)求并發(fā)打到了數(shù)據(jù)庫(kù)去查詢數(shù)據(jù)構(gòu)建緩存,造成數(shù)據(jù)庫(kù)壓力非常大甚至宕機(jī)。
  • 解決方案
    1.互斥鎖:
    因?yàn)槭峭粫r(shí)間很多請(qǐng)求并發(fā)的訪問數(shù)據(jù)庫(kù),把這個(gè)動(dòng)作設(shè)置一個(gè)分布式鎖,只有一個(gè)請(qǐng)求能去db訪問,其他請(qǐng)求重試等待,解決了全部請(qǐng)求全部查詢數(shù)據(jù)庫(kù)的問題。這種方案相當(dāng)于把數(shù)據(jù)庫(kù)的訪問壓力轉(zhuǎn)到了分布式鎖的壓力上來,有一定的弊端,但是最簡(jiǎn)單實(shí)用。
public Object getCache(final String key) {
        Object value = redis.get(key);
        //緩存值過期
        if (value == null) {    
        //加mutexKey的互斥鎖
            if (redis.setnx(mutexKey, 1, time)) {  
                value = db.get(key);
                redis.set(key, value, time);
                redis.delete(mutexKey);
            } else {
                sleep(50); 
                //重試
                return get(key);  
            }
        }
        return value;
    }

2.軟過期+互斥鎖:
軟過期指對(duì)緩存的值里存儲(chǔ)邏輯過期時(shí)間t1,這個(gè)時(shí)間比實(shí)際要過期的時(shí)間t2小(t1<t2),業(yè)務(wù)取值時(shí)候,校驗(yàn)t1是否過期,在發(fā)現(xiàn)了數(shù)據(jù)邏輯時(shí)間過期的時(shí)候,也是引入一把互斥鎖,首先將t1時(shí)間延長(zhǎng)t1=t1+t并設(shè)置到緩存中去,接著去db查詢新數(shù)據(jù),其他線程這時(shí)看到延長(zhǎng)了的過期時(shí)間,就會(huì)繼續(xù)使用舊數(shù)據(jù),等線程獲取最新數(shù)據(jù)后再更新緩存。
這種方案相比第一種進(jìn)一步減少了讀請(qǐng)求線程阻塞的時(shí)間,第一種方案阻塞時(shí)間block time從數(shù)據(jù)庫(kù)查詢并設(shè)置到緩存中的整個(gè)時(shí)間段。第二種方案阻塞時(shí)間block time:t1=t1+t并設(shè)置到緩存中的時(shí)間段。

public Object getCache(final String key) {
        Object value = redis.get(key);
        if (value != null) {
            //檢驗(yàn)緩存里的邏輯過期時(shí)間
            if (value.getTimeout() <= currentTimeMillis()) {
                if (redis.setnx(mutexKey,time)) {
                    //立即延長(zhǎng)邏輯過期時(shí)間,減少阻塞時(shí)間
                    value.setTimeout(value.getTimeout() + t1);
                    redis.set(key, value, time);
                    
                    value = db.get(key);
                    //獲取最新db數(shù)據(jù),并重新設(shè)置新的邏輯過期時(shí)間,覆蓋舊數(shù)據(jù)
                    value.setTimeout(value.getTimeout() + t2);
                    redis.set(key, value, time);
                    redis.delete(mutexKey);
                } else {
                    sleep(500);
                    get(key);
                }
            }
        
        } else {
            //緩存不存在的情況和上面一樣
            if (redis.setnx(mutexKey,time)) {
                value = db.get(key);
                redis.set(key, value,time1);
                redis.delete(mutexKey);
            } else {
                sleep(500);
                get(key);
            }
        }
         return value;
    }

3.靜態(tài)數(shù)據(jù):lazy expiration
這里靜態(tài)數(shù)據(jù)的含義是指redis不set expire過期時(shí)間,對(duì)redis來說認(rèn)為數(shù)據(jù)是不過期的是靜態(tài)的但實(shí)際和上面的軟過期是一樣的,通過value里設(shè)置邏輯過期時(shí)間,再拿到值判斷值過期之后,后臺(tái)新起異步線程更新緩存,這種方式性能最好

public Object getCache(String key) {
        Object value = redis.get(key);
        if (value.getTimeout() <= System.currentTimeMillis()) {
            // 另起一條線程異步更新緩存
            executorService.execute(new Runnable() {
                public void run() {
                    if (redis.setnx(mutexKey, "1")) {
                        redis.expire(mutexKey, 3 * 60);
                        String dbValue = db.get(key);
                        redis.set(key, dbValue);
                        redis.delete(mutexKey);
                    }
                }
            });
        }
        return value;
    }
方法 優(yōu)點(diǎn) 缺點(diǎn)
互斥鎖 1.簡(jiǎn)單易用
2.一致性保證
1.存在線程阻塞的風(fēng)險(xiǎn)
2.數(shù)據(jù)庫(kù)訪問的壓力轉(zhuǎn)到分布式鎖上來
軟過期+互斥鎖 1.相比互斥鎖方案,降低線程阻塞的時(shí)間 1.代碼更復(fù)雜
2.邏輯過期時(shí)間會(huì)占用一定的內(nèi)存空間
靜態(tài)數(shù)據(jù) 1.數(shù)據(jù)不過期,異步構(gòu)建性能最好
2.基本杜絕熱點(diǎn)key重建問題
1.不能保證一致性
2.代碼復(fù)雜性增加
3.邏輯過期時(shí)間會(huì)占用一定的內(nèi)存空間
5.3 緩存雪崩
  • 概念:緩存層擋在db層前面,抗住了非常多的流量,在分布式系統(tǒng)中,“everything will fails”,緩存作為一種資源,當(dāng)cache crash后,流量集中涌入下層數(shù)據(jù)庫(kù),稱之為緩存雪崩。造成這種問題通常有2種:
    • 業(yè)務(wù)層面:大量的緩存key同時(shí)失效,失效請(qǐng)求全部回源到數(shù)據(jù)庫(kù),造成數(shù)據(jù)庫(kù)壓力過大崩潰。
    • 系統(tǒng)層面:緩存服務(wù)宕機(jī)。
  • 解決方案:
    1.分散過期時(shí)間:業(yè)務(wù)層面的原因,主要是緩存key過期時(shí)間一致,造成同一時(shí)間,大量緩存key同時(shí)失效。針對(duì)這種問題的解決方案,主要是防止緩存在同一時(shí)間一期過期,如在設(shè)置的過期時(shí)間的基礎(chǔ)上增加t1-t2的隨機(jī)值,使緩存失效時(shí)間比較均勻
    2.提前演練壓測(cè):提前做好系統(tǒng)的演練壓測(cè),發(fā)現(xiàn)性能瓶頸,預(yù)估合適的系統(tǒng)存儲(chǔ)和計(jì)算容量。
    3.cache高可用+后端數(shù)據(jù)庫(kù)限流:1. 緩存作為一種系統(tǒng)資源,且通常充當(dāng)關(guān)鍵路徑關(guān)鍵資源,應(yīng)盡可能提升緩存的可用性,如redis的sentinel和cluster機(jī)制等;2. 采用了雙緩存熱備份方案來進(jìn)可能提升緩存資源的可用性;3. 后端數(shù)據(jù)庫(kù)限流,緩存層宕機(jī),流量集中打到數(shù)據(jù)庫(kù),會(huì)再次讓db崩潰。為保護(hù)這種情況下的db,在db層加入限流。
5.4 熱 Key
  • 概念:用戶的消費(fèi)速度遠(yuǎn)遠(yuǎn)大于生產(chǎn)速度,例如電商平臺(tái)上線某個(gè)熱門促銷商品,微博大量轉(zhuǎn)發(fā)的熱門新聞等,這些數(shù)據(jù)往往查詢量非常大。其實(shí)緩存擊穿也是一種熱點(diǎn)key問題,但是這里要討論的方面不一樣,緩存擊穿主要側(cè)重的是熱key失效后大量并發(fā)查詢涌向數(shù)據(jù)庫(kù)照成的壓力,而這里的熱key側(cè)重的是熱key的訪問壓力已經(jīng)大到超過redis性能極限,相對(duì)于緩存擊穿的熱key,這里也可叫巨熱數(shù)據(jù)。
    分布式緩存組件,通常會(huì)進(jìn)行分片切分,例如squirrel的cluster機(jī)制,查詢某個(gè)key,會(huì)通過key的hash值計(jì)算出對(duì)應(yīng)的slot,路由到某個(gè)分片的所屬機(jī)器上。熱key出現(xiàn)時(shí),所有熱點(diǎn)訪問的請(qǐng)求都會(huì)路由到同一個(gè)redis server,該節(jié)點(diǎn)的負(fù)載嚴(yán)重加劇,并且這種現(xiàn)象通常不是馬上加機(jī)器就能解決,因?yàn)橥粋€(gè)請(qǐng)求key還是會(huì)落到同一個(gè)新機(jī)器上,瓶頸依然存在。并且如果這個(gè)key還是大key ,甚至可能達(dá)到物理網(wǎng)卡極限,服務(wù)被打垮宕機(jī),造成雪崩,成為系統(tǒng)瓶頸和風(fēng)險(xiǎn)。因此熱點(diǎn)key會(huì)有以下問題。
    • 流量集中,達(dá)到物理網(wǎng)卡上限。
    • 請(qǐng)求過多,緩存分片服務(wù)被打垮。
    • 緩存分片打垮,重建再次被打垮,引起業(yè)務(wù)雪崩。
  • 解決方案
    1.多級(jí)緩存:本地緩存->Redis->DB
    2.多副本:當(dāng)發(fā)現(xiàn)某個(gè)熱key的時(shí)候,增加熱key所在節(jié)點(diǎn)的從副本,這種情況對(duì)讀多寫少的情況比較有效。但是也增加了多副本同步不一致的風(fēng)險(xiǎn)。
    3.遷移熱key:當(dāng)發(fā)現(xiàn)某個(gè)slot里熱key的時(shí)候,將該slot的單獨(dú)遷移到新的節(jié)點(diǎn),和集群其他節(jié)點(diǎn)隔離,避免影響集群節(jié)點(diǎn)其他業(yè)務(wù)。
5.5 大 Key
  • 概念:業(yè)務(wù)場(chǎng)景中經(jīng)常會(huì)有各種大value多value的情況, 比如:1. 單個(gè)string 類型 key 存的value很大, 超過 1MB。2. hash, set,zset,list 中存儲(chǔ)過多的元素,超過 10K。3. 一個(gè)集群存儲(chǔ)了上億的key,key本身過多也帶來了更多的空間占用(如無例外,文章中所提及的hash,set等數(shù)據(jù)結(jié)構(gòu)均指redis中的數(shù)據(jù)結(jié)構(gòu))由于redis是單線程運(yùn)行的,如果一次操作的value很大會(huì)對(duì)整個(gè)redis的響應(yīng)時(shí)間造成負(fù)面影響,所以,業(yè)務(wù)上能拆則拆,下面舉幾個(gè)典型的分拆方案。

  • string 類型大key處理方式
    1:該對(duì)象需要每次都整存整取
    可以嘗試將對(duì)象分拆成幾個(gè)key-value
    使用multiGet獲取值,這樣分拆的意義在于分拆單次操作的壓力,將操作壓力平攤到多個(gè)redis實(shí)例中,降低對(duì)單個(gè)redis的IO影響和 CPU 的影響
    2:該對(duì)象每次只需要存取部分?jǐn)?shù)據(jù)
    以像第一種做法一樣,分拆成幾個(gè)key-value,也可以將這個(gè)存儲(chǔ)在一個(gè)hash中,每個(gè)field代表一個(gè)具體的屬性,
    使用hget,hmget來獲取部分的value,使用hset,hmset來更新部分屬性

  • 集合類型大 key處理方式
    類似于場(chǎng)景一種的第一個(gè)做法,可以將這些元素分拆。以hash為例,原先的正常存取流程是 hget(hashKey, field) ; hset(hashKey, field, value)
    現(xiàn)在,固定一個(gè)桶的數(shù)量,比如 10000, 每次存取的時(shí)候,先在本地計(jì)算field的hash值,模除 10000, 確定了該field落在哪個(gè)key上。newHashKey = hashKey + ( hash(field) % 10000); hset (newHashKey, field, value) ; hget(newHashKey, field)
    set, zset, list 也可以類似上述做法。
    但有些不適合的場(chǎng)景,比如,要保證 lpop 的數(shù)據(jù)的確是最早push到list中去的,這個(gè)就需要一些附加的屬性,或者是在 key的拼接上做一些工作(比如list按照時(shí)間來分拆)。

6、如何保證 Redis 高性能?
  • Redis 的高性能源自兩方面,一方面是 Redis 處理命令的時(shí)候,都是純內(nèi)存操作。另外一方面,在 Linux 系統(tǒng)上 Redis 采用了 epoll 和 Reactor 結(jié)合的 IO 模型,非常高效。
  • 為了保證性能最好,Redis 使用的是基于 epoll 的 Reactor 模式。 Reactor 模式可以看成是一個(gè)分發(fā)器 + 一堆處理器。Reactor 模式會(huì)發(fā)起 epoll 之類的系統(tǒng)調(diào)用,如果是讀寫事件,那么就交給 Handler 處理;如果是連接事件,就交給 Acceptor 處理。
  • Redis 是單線程模型,所以 Reactor、Handler 和 Acceptor 其實(shí)都是這個(gè)線程。 整個(gè)過程是這樣的: Redis 中的 Reactor 調(diào)用 epoll,拿到符合條件的文件描述符。假如說 Redis 拿到了可讀寫的描述符,就會(huì)執(zhí)行對(duì)應(yīng)的讀寫操作。如果 Redis 拿到了創(chuàng)建連接的文件描述符,就會(huì)完成連接的初始化,然后準(zhǔn)備監(jiān)聽這個(gè)連接上的讀寫事件。

  • Redis 在 6.0 引入多線程,整個(gè) Redis 在多線程模式下,可以看作是單線程 Reactor、單線程 Acceptor 和多線程 Handler 的 Reactor 模式。只不過 Redis 的主線程同時(shí)扮演了 Reactor 中分發(fā)事件的角色,也扮演了接收請(qǐng)求的角色。同時(shí)多線程 Handler 在 Redis 里面僅僅是讀寫數(shù)據(jù),命令的執(zhí)行還是依賴于主線程來進(jìn)行的。

7、如何保證 Redis分布式鎖的高可用和高性能?

參考:分布式系統(tǒng)互斥性與冪等性問題的分析與解決
Cerberus在分布式場(chǎng)景下提供互斥原語,能夠?qū)崿F(xiàn)對(duì)并發(fā)場(chǎng)景下共享“資源”的保護(hù)。分布式鎖主要解決兩類問題。
? 互斥保護(hù):加鎖是為了避免Race Condition導(dǎo)致邏輯錯(cuò)誤。例如直接使用分布式鎖實(shí)現(xiàn)防重,冪等機(jī)制。此時(shí)如果鎖出現(xiàn)錯(cuò)誤會(huì)引起嚴(yán)重后果,因此對(duì)鎖的正確性要求高。
? 高效去重:加鎖是為了避免不必要的重復(fù)處理。例如防止冪等任務(wù)被多個(gè)執(zhí)行者搶占。此時(shí)對(duì)鎖的正確性要求不高;
其適用的最常見業(yè)務(wù)場(chǎng)景是:
a. 避免資源搶占,資源搶占可能導(dǎo)致重復(fù)執(zhí)行的問題。例如運(yùn)行定時(shí)任務(wù)前加鎖,處理結(jié)束后解鎖。
b. 防止并發(fā)寫入,并發(fā)更新可能導(dǎo)致ABA問題。例如分發(fā)優(yōu)惠券時(shí),更新余量前加鎖避免超量發(fā)券。
c. 選主/降級(jí)服務(wù),對(duì)某個(gè)Node加鎖等效為該Node成為Master。

Cerberus如何處理高一致性要求的場(chǎng)景?
目前Cerberus實(shí)現(xiàn)了基于ZooKeeper的Lock-Engine,用于在一致性要求高的場(chǎng)景下提供分布式鎖服務(wù);
Cerberus如何處理高性能要求的場(chǎng)景?
目前Cerberus實(shí)現(xiàn)了基于Redis的Lock-Engine,用于處理優(yōu)先考慮高吞吐量需求的業(yè)務(wù)場(chǎng)景。

鎖使用 Demo:

@Autowired
private IDistributedLockManager distributedLockManager;

@Override
public void lock(String lockName) {
    Lock lock = distributedLockManager.getReentrantLock(lockName);
    String name = Thread.currentThread().getName();
    // 獲取鎖
    logger.info("Thread={} try to acquire lock", name);
    lock.lock();
    try {
        // 處理任務(wù)
        logger.info("Thread={} do something...", name);
    } finally {
        // 釋放鎖
        lock.unlock();
        logger.info("Thread={} unlocked", name);
    }
}

@Override
public void tryLock(String lockName) {
    Lock lock = distributedLockManager.getReentrantLock(lockName);
    String name = Thread.currentThread().getName();
    // 獲取鎖
    logger.info("Thread={} try to acquire lock", name);
    if (lock.tryLock()) {
        try {
            // 處理任務(wù)
            logger.info("Thread={} do something...", name);
        }finally {
            lock.unlock();// 釋放鎖
            logger.info("Thread={} unlocked", name);
        }
    } else {
        // 如果不能獲取鎖,則直接做其他事情
        logger.info("Thread={} do other things...", name);
    }
}

@Override
public void tryLock(String lockName, long time, TimeUnit timeUnit) throws InterruptedException {
    Lock lock = distributedLockManager.getReentrantLock(lockName);
    String name = Thread.currentThread().getName();
    // 獲取鎖
    logger.info("Thread={} try to acquire lock", name);
    if (lock.tryLock(time,timeUnit)) {
        try {
            // 處理任務(wù)
            logger.info("Thread={} do something...", name);
        }finally {
            lock.unlock();// 釋放鎖
            logger.info("Thread={} unlocked", name);
        }
    } else {
        // 如果不能獲取鎖,則直接做其他事情
        logger.info("Thread={} do other things...", name);
    }
}

@Override
public void lockInterruptibly(String lockName) throws InterruptedException {
    Lock lock = distributedLockManager.getReentrantLock(lockName);
    String name = Thread.currentThread().getName();
    // 獲取鎖
    logger.info("Thread={} try to acquire lock", name);
    lock.lockInterruptibly();
    try {
        // 處理任務(wù)
        logger.info("Thread={} do something...", name);
    } finally {
        // 釋放鎖
        lock.unlock();
        logger.info("Thread={} unlocked", name);
    }
}

接口說明:


package com.meituan.hotel.dlm.lock;

public interface Lock {
  //阻塞接口,如果此線程無法獲得鎖會(huì)一直阻塞直到獲得鎖,不響應(yīng)中斷
    void lock() throws CerberusDLMException;
  
  //與lock()的區(qū)別在于lockInterruptibly()可以響應(yīng)中斷
    void lockInterruptibly() throws InterruptedException, CerberusDLMException;

    //非阻塞接口,如果此線程可以獲得鎖返回true并持有鎖,否則返回false
    boolean tryLock() throws CerberusDLMException;

  /**
    * 阻塞接口,如果此線程無法獲得鎖會(huì)一直阻塞,有3種情況結(jié)束阻塞
    * 1. 超過timeout限制的時(shí)長(zhǎng)仍未獲得鎖,返回false;
    * 2. 此線程被中斷,拋出InterruptedException異常;
    * 3. timeout限制的時(shí)長(zhǎng)內(nèi)獲取到鎖,返回true;
 */
    boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException, CerberusDLMException;

  //解鎖
    void unlock();

  //返回鎖名
    String getName();
}
public interface IDistributedLockManager {

    // 獲得相應(yīng)的可重入鎖
    Lock getReentrantLock(String lockName);

    Lock getReentrantLock(String lockName, int expireTime);

    Lock getReentrantLock(String lockName, int expireTime, int retry);

    Lock getNewMultiLock(Lock[] locks);

    Lock getNewMultiLock(String[] locks);

    Lock getNewMultiLock(String[] locks, int expireTime);
}

核心原理:

  • 加鎖
    private RenewalWatchdog watchdog;  

    /**
     * 線程本地變量, 用于鎖重入檢查
     */
    private ThreadLocal<LockHolder> locks = new ThreadLocal<>();

    public void lock() {
        try {
            // 鎖重入檢查
            if (reentrant()) {
                t.setStatus(Transaction.SUCCESS);
                return;
            }

            // 自旋, 直到獲取鎖
            while (true) {
                try {
                    if (squirrelProcessor.add(key, uuid, expireTime, retry)) {
                        lockAtOutermost();

                        log.info("Try acquire lock success. key: {}, uuid: {}", key, uuid);
                        t.setStatus(Transaction.SUCCESS);
                        return;
                    } else {
                        log.debug("Try acquire lock failed. key: {}, uuid: {}", key, uuid);
                    }
                } catch (Exception e) {
                    log.error("Redis try acquire lock failed, key: {}", key, e);
                    t.setStatus(e);
                    sliTx.setStatus(e);

                    throw new RuntimeException(e);
                }
            }
        }
    }

  protected void lockAtOutermost() {
        locks.set(new LockHolder(key.toString()));

        watchdog.watch(this);
    }

    /**
     * 鎖重入檢查
     *
     * @return 重入則返回 true; 否則返回 false
     */
    private boolean reentrant() {
        try {
            if (LockHolder.checkReentrancy(locks)) {
                // reentrant, refresh lease time
//                return squirrelProcessor.compareAndSet(key, this.uuid, this.uuid, expireTime, retry);
                watchdog.watch(this);

                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException("Redis refresh lease time failed, key: " + key, e);
        }
    }
    public boolean add(StoreKey key, String value, int leaseTime, int retry) throws Exception {
        try{
            Boolean result;
            try {
                result = redisStoreClient.add(key, value, leaseTime);
            } catch (Exception e) {
                result = exceptionHandler(SquirrelMethodEnum.ADD, e, key, value, null, leaseTime, retry);
                if (false == result) {
                    // 有可能客戶端超時(shí),但服務(wù)端已成功添加(key,value)
                    if (redisStoreClient.exists(key)) {
                        // 若key存在,獲取value1,并判斷value1是否與value相等,相等則返回true
                        String getValue = redisStoreClient.get(key);
                        t.setStatus(Transaction.SUCCESS);
                        return StringUtils.equals(value, getValue);
                    }
                }
            }

            t.setStatus(Transaction.SUCCESS);
            return result;
        }
    }

        /**
     * 處理squirrel的操作拋出的異常,根據(jù)重試次數(shù)進(jìn)行回調(diào)
     *
     * @param type
     * @param squirrelException
     * @param key
     * @param oldValue
     * @param newValue
     * @param expireTime
     * @param retry
     * @return
     * @throws Exception
     */
    private boolean exceptionHandler(SquirrelMethodEnum type, Exception squirrelException, StoreKey key, String oldValue, String newValue,
            int expireTime, int retry) throws Exception {
        if (Thread.interrupted())
            throw new InterruptedException();
        boolean result = false;
        if (retry > 0) {
            retry--;
            Thread.sleep(100);
            LOGGER.info(type.getField() + " error; Retry:" + (retry + 1) + "; Key:" + key + "; exception:" + squirrelException.toString());
            switch (type) {
            case ADD:
                result = this.add(key, oldValue, expireTime, retry);
                break;
            case COMPARE_AND_DELETE:
                result = this.compareAndDelete(key, oldValue, retry);
                break;
            case COMPARE_AND_SET:
                result = this.compareAndSet(key, oldValue, newValue, expireTime, retry);
                break;
            default:
                break;
            }
        } else {
            LOGGER.error(type.getField() + " error; Key:" + key, squirrelException);
            throw squirrelException;
        }

        return result;
    }
    @Override
    public Boolean add(StoreKey key, final Object value, final int expire) {
        return setnx(key, value, expire);
    }

    @Override
    public Boolean setnx(StoreKey key, final Object value, final int expireInSeconds) {
        final StoreCategoryConfig categoryConfig = categoryConfigManager.findCacheKeyType(key.getCategory());
        final String finalKey = categoryConfig.getFinalKey(key);

        return new MonitorCommand(new Method(Method.Command.WRITE, "setnx", finalKey).expire(expireInSeconds)
                , storeType, categoryConfig) {
            @Override
            public Object excute() throws Exception {
                byte[] str = transcoder.encodeToBytes(value);

                if (expireInSeconds > 0) {
                    String result = clientManager.getClient().set(SafeEncoder.encode(finalKey), str, "NX", "EX", expireInSeconds);

                    return OK_STR.equals(result);
                } else {
                    return 1 == clientManager.getClient().setnx(SafeEncoder.encode(finalKey), str);
                }
            }
        }.run();
    }
  • 嘗試加鎖
    public boolean tryLock() {
        try{
            // 鎖重入檢查
            if (reentrant()) {
                t.setStatus(Transaction.SUCCESS);
                return true;
            }

            try {
                if (squirrelProcessor.add(key, uuid, expireTime, retry)) {
                    lockAtOutermost();

                    log.info("Try acquire lock success. key: {}, uuid: {}", key, uuid);
                    t.setStatus(Transaction.SUCCESS);
                    return true;
                } else {
                    log.debug("Try acquire lock failed. key: {}, uuid: {}", key, uuid);
                    t.setStatus(Transaction.SUCCESS);
                    return false;
                }
            } catch (Exception e) {
                log.error("Redis try acquire lock failed, key: {}", key, e);

                throw new RuntimeException(e);
            }
        }
    }
    /**
     * 一直阻塞直到獲取鎖, 除非超過等待時(shí)間, 或者線程被中斷;
     * 獲取鎖后, 持有的鎖超過指定的時(shí)間后自動(dòng)過期
     *
     * @param waitTime  等待時(shí)間
     * @param unit      時(shí)間單位
     * @return 獲取鎖成功返回 true; 否則返回 false
     * @throws InterruptedException 如果當(dāng)前線程被中斷, 拋出該異常
     */
    @Override
    public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {

        try {
            Preconditions.checkNotNull(unit, "時(shí)間單位不能為空");

            // 鎖重入檢查
            if (reentrant()) {
                t.setStatus(Transaction.SUCCESS);
                return true;
            }

            long timeout = System.nanoTime() + unit.toNanos(waitTime);
            if (timeout < 0) {
                timeout = Long.MAX_VALUE;
            }
            while (true) {
                try {
                    if (squirrelProcessor.add(key, uuid, expireTime, retry)) {
                        lockAtOutermost();

                        log.info("Try acquire lock success. key: {}, uuid: {}", key, uuid);
                        t.setStatus(Transaction.SUCCESS);
                        return true;
                    }
                } catch (InterruptedException e) {
                    t.setStatus("thread interrupted");
                    throw new InterruptedException();
                } catch (Exception e) {
                    log.error("Redis try acquire lock failed, key: {}", key, e);

                    throw new RuntimeException(e);
                }

                if (System.nanoTime() >= timeout) {
                    log.debug("Try acquire lock timeout. key: {}, uuid: {}", key, uuid);
                    t.setStatus(Transaction.SUCCESS);
                    return false;
                } else {
                    log.debug("Try acquire lock failed, will try again. key: {}, uuid: {}", key, uuid);
                }

                if (Thread.interrupted()) {
                    t.setStatus("thread interrupted");
                    throw new InterruptedException();
                }
            }

        }
    }
    /**
     * 一直阻塞到獲得鎖, 除非線程被中斷;
     * 獲取鎖后, 持有的鎖超過指定的時(shí)間后自動(dòng)過期
     *
     * @throws InterruptedException 如果當(dāng)前線程被中斷, 拋出該異常
     */
    @Override
    public void lockInterruptibly() throws InterruptedException {
        tryLock(Long.MAX_VALUE, TimeUnit.DAYS);
    }
  • 解鎖
    public void unlock() {
        try{

            LockHolder lockHolder = this.locks.get();
            if (lockHolder == null) {
                t.setStatus(Transaction.SUCCESS);
                throw new IllegalMonitorStateException("Attempting to unlock without first obtaining that lock on this thread");
            }

            int lockCounts = lockHolder.decrementLock();
            try {
                if (lockCounts == 0) {
//                    locks.remove();
                    unlockAtOutermost();

                    if (squirrelProcessor.compareAndDelete(key, uuid, retry)) {
                        log.info("Release lock success. key: {}, uuid: {}", key, uuid);
                    } else {
                        log.debug("Release lock failed. key: {}, uuid: {}", key, uuid);
                    }
                }
            } catch (Exception e) {
                log.error("Redis try release lock failed. key: {}", key, e);
                t.setStatus(e);
                throw new RuntimeException(e);
            }
            t.setStatus(Transaction.SUCCESS);
        }
    }
    public boolean compareAndDelete(StoreKey key, String value, int retry) throws Exception {
        try{
            Boolean result;
            try {
                result = redisStoreClient.compareAndDelete(key, value);
            } catch (Exception e) {
                result = exceptionHandler(SquirrelMethodEnum.COMPARE_AND_DELETE, e, key, value, null, -1, retry);
            }

            t.setStatus(Transaction.SUCCESS);
            return result;
        }
    }

    public Boolean compareAndDelete(StoreKey key, final Object expect) {
        checkNotNull(key, STORE_KEY_IS_NULL);
        checkNotNull(expect, STORE_VALUE_IS_NULL);
        final StoreCategoryConfig categoryConfig = categoryConfigManager.findCacheKeyType(key.getCategory());
        final String finalKey = categoryConfig.getFinalKey(key);

        return new MonitorCommand(new Method(Method.Command.WRITE, "compareAndDelete", finalKey)
                , storeType, categoryConfig) {
            @Override
            public Object excute() throws Exception {

                return clientManager.getClient().compareAndDelete(SafeEncoder.encode(finalKey),
                        transcoder.encodeToBytes(expect));
            }
        }.run();
    }
  
    // jedis
    public Boolean compareAndDelete(final byte[] key, final byte[] expect) {
        return new JedisClusterCommand<Boolean>(connectionHandler, maxRedirections, Protocol.Command.SET) {
            @Override
            public Boolean execute(Jedis connection) {
                return connection.cad(key, expect) == 1;
            }
        }.runBinary(key);
    }
  • 工具類:
public class LockHolder {
    private final String lockNode;
    private final AtomicInteger numLocks = new AtomicInteger(1);
    private final int lockTime;

    public LockHolder(String lockNode) {
        this.lockNode = lockNode;
        this.lockTime = -1;
    }

    public LockHolder(int lockTime) {
        this.lockTime = lockTime;
        this.lockNode = "";
    }

    public void incrementLock() {
        numLocks.incrementAndGet();
    }

    public int decrementLock() {
        return numLocks.decrementAndGet();
    }

    public String getLockNode() {
        return lockNode;
    }

    public int getLockTime() {
        return lockTime;
    }

    public static boolean checkReentrancy(ThreadLocal<LockHolder> locks) {
        LockHolder local = locks.get();
        if(local!=null){
            local.incrementLock();
            return true;
        }
        return false;
    }
}
@Slf4j
public abstract class RenewalWatchdog<T extends Lock> {
    protected static final long SHUT_DOWN_WAITING_MILI = 5000;

    /**
     * 刷新間隔, nanosecond
     */
    protected long renewalIntervalNano;

    /**
     * 續(xù)租步長(zhǎng), nanosecond
     */
    protected long renewalStepNano;

    protected ScheduledExecutorService renewalScheduler = new ScheduledThreadPoolExecutor(1);

    protected ThreadPoolExecutor renewalExecutor = createProcessorExecutor();

    protected final Map<String, LockTimer<T>> LOCK_TIMER_MAP;

    /**
     * @param intervalNano nanosecond
     * @param stepSizeNano nanosecond
     * @param mapInitSize map init size
     */
    public RenewalWatchdog(long intervalNano, long stepSizeNano, int mapInitSize) {
        log.info("New RenewalWatchdog initiated. {}, {}, {}", intervalNano, stepSizeNano, mapInitSize);

        this.renewalIntervalNano = intervalNano;
        this.renewalStepNano = stepSizeNano;

        this.LOCK_TIMER_MAP = new ConcurrentHashMap<>(mapInitSize);

        renewalScheduler.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    if (CollectionUtils.isEmpty(LOCK_TIMER_MAP)) {
                        log.info("LOCK_TIMER_MAP empty, no lock need to be renewed.");
                        return;
                    }

                    int counter = 0;
                    for (LockTimer<T> item : LOCK_TIMER_MAP.values()) {
                        log.debug("trying to renew lock, timer: ", item);

                        CatUtils.logSquirrelWatchDogEvent("LOCK_Renewal");

                        if (renewal(item)) {
                            counter++;
                        }
                    }

                    log.info("{} locks' lease renewed.", counter);
                } catch (Exception e) {
                    log.warn("renewal failed.", e);
                }
            }
        }, this.renewalIntervalNano, this.renewalIntervalNano, TimeUnit.NANOSECONDS);
    }

    /**
     * @param lock
     * @return
     */
    public void watch(T lock) {
        log.debug("watch lock {}", lock);

        LOCK_TIMER_MAP.put(lock.getName(), createLockTimer(lock));
    }

    public void unWatch(T lock) {
        if (lock == null) {
            log.warn("lock cannot be null.");

            return;
        }

        log.debug("unwatch lock {}", lock);

        LOCK_TIMER_MAP.remove(lock.getName());
    }

    public void destroy() {
        renewalScheduler.shutdown();
        renewalExecutor.shutdown();

        try {
            if (renewalScheduler.awaitTermination(SHUT_DOWN_WAITING_MILI, TimeUnit.MILLISECONDS)) {
                renewalScheduler.shutdownNow();
                renewalScheduler = null;
            }

            if (renewalExecutor.awaitTermination(SHUT_DOWN_WAITING_MILI, TimeUnit.MILLISECONDS)) {
                renewalExecutor.shutdownNow();
                renewalExecutor = null;
            }
        } catch (InterruptedException e) {
            log.warn("scheduler.awaitTermination interrupted.");
        } catch (Exception ex) {
            log.warn("scheduler.awaitTermination error.", ex);
        } finally {
            LOCK_TIMER_MAP.clear();
        }

        log.info("RenewalWatchdog destroyed.");
    }

    /**
     * threadpool can be refined
     * @return
     */
    protected ThreadPoolExecutor createProcessorExecutor() {
        return new ThreadPoolExecutor(5, 10, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }

    /**
     *
     */
    protected boolean renewal(LockTimer<T> timer) {
        if (timer == null) {
            return false;
        }

        if (timer.isExpired()) {
            log.debug("unwatch expired lock, time: {}", timer);

            //need remove timer.
            unWatch(timer.getLock());

            return false;
        }

        //put into renewal threadPool
        renewalExecutor.execute(timer);

        return true;
    }

    protected abstract LockTimer<T> createLockTimer(T lock);
}
  • watchdog機(jī)制實(shí)現(xiàn)

1.解決的問題

直接設(shè)置Redis超時(shí) 實(shí)現(xiàn)watchdog續(xù)租機(jī)制(1.7.4-snapshot)
設(shè)置合理的超時(shí)時(shí)間比較困難 設(shè)置合理的超時(shí)時(shí)間比較簡(jiǎn)單
超時(shí)時(shí)間設(shè)置過長(zhǎng):存在client端失效時(shí),死鎖不能及時(shí)被釋放的問題 可以直接設(shè)置較長(zhǎng)超時(shí)時(shí)間,client失效后持有的鎖會(huì)在短時(shí)間內(nèi)被釋放,不需要等到指定的超時(shí)時(shí)間后釋放。
超時(shí)時(shí)間設(shè)置過短:存在鎖被強(qiáng)制釋放,導(dǎo)致并發(fā)訪問的問題

2.實(shí)現(xiàn)方案

  • SquirrelLockWatchdog負(fù)責(zé)管理squirrel引擎下所有l(wèi)ock的續(xù)租
  • 每次獲取到鎖,向SquirrelLockWatchdog注冊(cè)鎖自身
  • 每次鎖超時(shí),或者被從squirrel釋放,在SquirrelLockWatchdog移除自身
  • SquirrelLockWatchdog使用定時(shí)線程池,定期為每個(gè)鎖續(xù)租
  • 續(xù)租過程由專用線程池并發(fā)處理
  • 續(xù)租參數(shù)如下:
    • 續(xù)租的刷新間隔為500ms
    • 每次續(xù)租步長(zhǎng)為2s(所以鎖的超時(shí)時(shí)間最小值為2s)
    • 鎖余下的超時(shí)時(shí)間不足時(shí),續(xù)租步長(zhǎng)為剩余時(shí)間(不足一秒時(shí)向上取整)

3.詳細(xì)流程

8、如何利用緩存提高應(yīng)用性能的?
  • 多級(jí)緩存:本地緩存+Redis(定價(jià)策略、指標(biāo)元數(shù)據(jù)等)
  • 緩存預(yù)熱+預(yù)加載:?jiǎn)?dòng)時(shí)加載熱點(diǎn)數(shù)據(jù)、啟動(dòng)后逐步放流量加載(定價(jià)策略預(yù)熱)
  • 客戶端緩存:針對(duì)依賴較慢且不經(jīng)常變的接口,可做緩存(調(diào)價(jià)工作臺(tái)與權(quán)限系統(tǒng)交互)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容