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)交互)
