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ù)一致性問題坐昙,針對(duì)這一點(diǎn)的話绳匀,只能說,如果你的項(xiàng)目對(duì)緩存的要求是強(qiáng)一致性的炸客,那么請(qǐng)不要使用緩存疾棵。我們只能采取合適的策略來降低緩存和數(shù)據(jù)庫間數(shù)據(jù)不一致的概率,而無法保證兩者間的強(qiáng)一致性痹仙。合適的策略包括 合適的緩存更新策略是尔,更新數(shù)據(jù)庫后要及時(shí)更新緩存、緩存失敗時(shí)增加重試機(jī)制开仰,例如MQ模式的消息隊(duì)列拟枚。

(二)緩存擊穿問題

緩存擊穿表示惡意用戶模擬請(qǐng)求很多緩存中不存在的數(shù)據(jù),由于緩存中都沒有抖所,導(dǎo)致這些請(qǐng)求短時(shí)間內(nèi)直接落在了數(shù)據(jù)庫上梨州,導(dǎo)致數(shù)據(jù)庫異常。這個(gè)我們?cè)趯?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杀捻、接口限流與熔斷井厌、降級(jí)

重要的接口一定要做好限流策略,防止用戶惡意刷接口致讥,同時(shí)要降級(jí)準(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.png

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


image.png
image.png

對(duì)比兩個(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)大量鍵過期(失效),接著來的一大波請(qǐng)求瞬間都落在了數(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ù)亿虽,留給各位看官自己思考探究。

同時(shí)需要更多java相關(guān)資料以及面試心得和視頻資料的苞也,歡迎加QQ群:810589193
免費(fèi)獲取Java工程化洛勉、高性能及分布式、高性能如迟、高架構(gòu)收毫、性能調(diào)優(yōu)攻走、Spring、MyBatis此再、Netty源碼分析等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨的直播免費(fèi)學(xué)習(xí)權(quán)限及相關(guān)視頻資料昔搂,還有spring和虛擬機(jī)等書籍掃描版

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市输拇,隨后出現(xiàn)的幾起案子摘符,更是在濱河造成了極大的恐慌,老刑警劉巖策吠,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逛裤,死亡現(xiàn)場離奇詭異,居然都是意外死亡猴抹,警方通過查閱死者的電腦和手機(jī)带族,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟀给,“玉大人店读,你說我怎么就攤上這事功咒√对妫” “怎么了筷厘?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長薪介。 經(jīng)常有香客問我祠饺,道長,這世上最難降的妖魔是什么汁政? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任道偷,我火速辦了婚禮,結(jié)果婚禮上记劈,老公的妹妹穿的比我還像新娘勺鸦。我一直安慰自己,他們只是感情好目木,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布换途。 她就那樣靜靜地躺著,像睡著了一般刽射。 火紅的嫁衣襯著肌膚如雪军拟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天誓禁,我揣著相機(jī)與錄音懈息,去河邊找鬼。 笑死摹恰,一個(gè)胖子當(dāng)著我的面吹牛辫继,可吹牛的內(nèi)容都是我干的怒见。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼姑宽,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼遣耍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起炮车,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤舵变,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后瘦穆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棋傍,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年难审,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亿絮。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡告喊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出派昧,到底是詐尸還是另有隱情黔姜,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布蒂萎,位于F島的核電站秆吵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏五慈。R本人自食惡果不足惜纳寂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望泻拦。 院中可真熱鬧毙芜,春花似錦、人聲如沸争拐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽架曹。三九已至隘冲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绑雄,已是汗流浹背展辞。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绳慎,地道東北人纵竖。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓漠烧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親靡砌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子已脓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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