Redis緩存穿透聪黎、緩存雪崩、redis并發(fā)問題分析

把redis作為緩存使用已經(jīng)是司空見慣备恤,但是使用redis后也可能會(huì)碰到一系列的問題稿饰,尤其是數(shù)據(jù)量很大的時(shí)候锦秒,經(jīng)典的幾個(gè)問題如下:

(一)緩存和數(shù)據(jù)庫間數(shù)據(jù)一致性問題

分布式環(huán)境下(單機(jī)就不用說了)非常容易出現(xiàn)緩存和數(shù)據(jù)庫間的數(shù)據(jù)一致性問題,針對這一點(diǎn)的話喉镰,只能說旅择,如果你的項(xiàng)目對緩存的要求是強(qiáng)一致性的,那么請不要使用緩存侣姆。我們只能采取合適的策略來降低緩存和數(shù)據(jù)庫間數(shù)據(jù)不一致的概率生真,而無法保證兩者間的強(qiáng)一致性。合適的策略包括 合適的緩存更新策略捺宗,更新數(shù)據(jù)庫后要及時(shí)更新緩存柱蟀、緩存失敗時(shí)增加重試機(jī)制,例如MQ模式的消息隊(duì)列蚜厉。

(二)緩存擊穿問題

緩存擊穿表示惡意用戶模擬請求很多緩存中不存在的數(shù)據(jù)产弹,由于緩存中都沒有,導(dǎo)致這些請求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫上弯囊,導(dǎo)致數(shù)據(jù)庫異常痰哨。這個(gè)我們在實(shí)際項(xiàng)目就遇到了,有些搶購活動(dòng)匾嘱、秒殺活動(dòng)的接口API被大量的惡意用戶刷斤斧,導(dǎo)致短時(shí)間內(nèi)數(shù)據(jù)庫c超時(shí)了,好在數(shù)據(jù)庫是讀寫分離霎烙,同時(shí)也有進(jìn)行接口限流撬讽,hold住了。

解決方案的話:

方案1悬垃、使用互斥鎖排隊(duì)

業(yè)界比價(jià)普遍的一種做法游昼,即根據(jù)key獲取value值為空時(shí),鎖上尝蠕,從數(shù)據(jù)庫中l(wèi)oad數(shù)據(jù)后再釋放鎖烘豌。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試看彼。這里要注意廊佩,分布式環(huán)境中要使用分布式鎖,單機(jī)的話用普通的鎖(synchronized靖榕、Lock)就夠了标锄。

public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
// 通過key獲取value
String value = redisService.get(key);
if (StringUtil.isEmpty(value)) {
// 分布式鎖,詳細(xì)可以參考https://blog.csdn.net/fanrenxiang/article/details/79803037
//封裝的tryDistributedLock包括setnx和expire兩個(gè)功能茁计,在低版本的redis中不支持
try {
boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
if (locked) {
value = userService.getById(key);
redisService.set(key, value);
redisService.del(lockKey);
return value;
} else {
// 其它線程進(jìn)來了沒獲取到鎖便等待50ms后重試
Thread.sleep(50);
getWithLock(key, jedis, lockKey, uniqueId, expireTime);
}
} catch (Exception e) {
log.error("getWithLock exception=" + e);
return value;
} finally {
redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
}
}
return value;
}
這樣做思路比較清晰料皇,也從一定程度上減輕數(shù)據(jù)庫壓力,但是鎖機(jī)制使得邏輯的復(fù)雜度增加,吞吐量也降低了践剂,有點(diǎn)治標(biāo)不治本毒返。

方案2、接口限流與熔斷舷手、降級

重要的接口一定要做好限流策略,防止用戶惡意刷接口男窟,同時(shí)要降級準(zhǔn)備盆赤,當(dāng)接口中的某些服務(wù)不可用時(shí)候,進(jìn)行熔斷歉眷,失敗快速返回機(jī)制牺六。

方案3、布隆過濾器

bloomfilter就類似于一個(gè)hash set汗捡,用于快速判某個(gè)元素是否存在于集合中淑际,其典型的應(yīng)用場景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回扇住。布隆過濾器的關(guān)鍵就在于hash算法和容器大小春缕,下面先來簡單的實(shí)現(xiàn)下看看效果,我這里用guava實(shí)現(xiàn)的布隆過濾器:

<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
</dependencies>
public class BloomFilterTest {

private static final int capacity = 1000000;
private static final int key = 999998;

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);

static {
    for (int i = 0; i < capacity; i++) {
        bloomFilter.put(i);
    }
}

public static void main(String[] args) {
    /*返回計(jì)算機(jī)最精確的時(shí)間艘蹋,單位微妙*/
    long start = System.nanoTime();

    if (bloomFilter.mightContain(key)) {
        System.out.println("成功過濾到" + key);
    }
    long end = System.nanoTime();
    System.out.println("布隆過濾器消耗時(shí)間:" + (end - start));
    int sum = 0;
    for (int i = capacity + 20000; i < capacity + 30000; i++) {
        if (bloomFilter.mightContain(i)) {
            sum = sum + 1;
        }
    }
    System.out.println("錯(cuò)判率為:" + sum);
}

}
成功過濾到999998
布隆過濾器消耗時(shí)間:215518
錯(cuò)判率為:318
可以看到锄贼,100w個(gè)數(shù)據(jù)中只消耗了約0.2毫秒就匹配到了key,速度足夠快女阀。然后模擬了1w個(gè)不存在于布隆過濾器中的key宅荤,匹配錯(cuò)誤率為318/10000,也就是說浸策,出錯(cuò)率大概為3%冯键,跟蹤下BloomFilter的源碼發(fā)現(xiàn)默認(rèn)的容錯(cuò)率就是0.03:

public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}
我們可調(diào)用BloomFilter的這個(gè)方法顯式的指定誤判率:

image

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我們斷點(diǎn)跟蹤下,誤判率為0.02和默認(rèn)的0.03時(shí)候的區(qū)別:

image
image

對比兩個(gè)出錯(cuò)率可以發(fā)現(xiàn)庸汗,誤判率為0.02時(shí)數(shù)組大小為8142363惫确,0.03時(shí)為7298440,誤判率降低了0.01夫晌,BloomFilter維護(hù)的數(shù)組大小也減少了843923雕薪,可見BloomFilter默認(rèn)的誤判率0.03是設(shè)計(jì)者權(quán)衡系統(tǒng)性能后得出的值。要注意的是晓淀,布隆過濾器不支持刪除操作。用在這邊解決緩存穿透問題就是:

public String getByKey(String key) {
// 通過key獲取value
String value = redisService.get(key);
if (StringUtil.isEmpty(value)) {
if (bloomFilter.mightContain(key)) {
value = userService.getById(key);
redisService.set(key, value);
return value;
} else {
return null;
}
}
return value;
}

(三)緩存雪崩問題

緩存在同一時(shí)間內(nèi)大量鍵過期(失效)盏档,接著來的一大波請求瞬間都落在了數(shù)據(jù)庫中導(dǎo)致連接異常凶掰。

解決方案:

方案1、也是像解決緩存穿透一樣加鎖排隊(duì),實(shí)現(xiàn)同上;

方案2懦窘、建立備份緩存前翎,緩存A和緩存B,A設(shè)置超時(shí)時(shí)間畅涂,B不設(shè)值超時(shí)時(shí)間港华,先從A讀緩存,A沒有讀B午衰,并且更新A緩存和B緩存;

方案3立宜、設(shè)置緩存超時(shí)時(shí)間的時(shí)候加上一個(gè)隨機(jī)的時(shí)間長度,比如這個(gè)緩存key的超時(shí)時(shí)間是固定的5分鐘加上隨機(jī)的2分鐘臊岸,醬紫可從一定程度上避免雪崩問題橙数;

public String getByKey(String keyA,String keyB) {
String value = redisService.get(keyA);
if (StringUtil.isEmpty(value)) {
value = redisService.get(keyB);
String newValue = getFromDbById();
redisService.set(keyA,newValue,31, TimeUnit.DAYS);
redisService.set(keyB,newValue);
}
return value;
}

(四)緩存并發(fā)問題

這里的并發(fā)指的是多個(gè)redis的client同時(shí)set key引起的并發(fā)問題。其實(shí)redis自身就是單線程操作帅戒,多個(gè)client并發(fā)操作灯帮,按照先到先執(zhí)行的原則,先到的先執(zhí)行逻住,其余的阻塞钟哥。當(dāng)然,另外的解決方案是把redis.set操作放在隊(duì)列中使其串行化瞎访,必須的一個(gè)一個(gè)執(zhí)行瞪醋,具體的代碼就不上了,當(dāng)然加鎖也是可以的装诡,至于為什么不用redis中的事務(wù)银受,留給各位看官自己思考探究。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸦采,一起剝皮案震驚了整個(gè)濱河市宾巍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渔伯,老刑警劉巖顶霞,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異锣吼,居然都是意外死亡选浑,警方通過查閱死者的電腦和手機(jī)掺出,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門匿级,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秩冈,“玉大人棍厂,你說我怎么就攤上這事慎菲“炙保” “怎么了钙态?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵述呐,是天一觀的道長。 經(jīng)常有香客問我疹吃,道長蹦疑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任萨驶,我火速辦了婚禮歉摧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腔呜。我一直安慰自己叁温,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布育谬。 她就那樣靜靜地躺著券盅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪膛檀。 梳的紋絲不亂的頭發(fā)上锰镀,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機(jī)與錄音咖刃,去河邊找鬼泳炉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嚎杨,可吹牛的內(nèi)容都是我干的花鹅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼枫浙,長吁一口氣:“原來是場噩夢啊……” “哼刨肃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起箩帚,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤真友,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后紧帕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盔然,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年是嗜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愈案。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鹅搪,死狀恐怖站绪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涩嚣,我是刑警寧澤崇众,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布掂僵,位于F島的核電站航厚,受9級特大地震影響顷歌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜幔睬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一眯漩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧麻顶,春花似錦赦抖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至矫钓,卻和暖如春要尔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背新娜。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工赵辕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人概龄。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓还惠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親私杜。 傳聞我的和親對象是個(gè)殘疾皇子蚕键,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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