自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我使用緩存,踩過(guò)的7個(gè)坑

開(kāi)發(fā) 架構(gòu)
假設(shè)在高并發(fā)的場(chǎng)景中,同一個(gè)用戶的同一條數(shù)據(jù),有一個(gè)讀數(shù)據(jù)請(qǐng)求c,還有另一個(gè)寫數(shù)據(jù)請(qǐng)求d(一個(gè)更新操作),同時(shí)請(qǐng)求到業(yè)務(wù)系統(tǒng)。在這個(gè)過(guò)程當(dāng)中,有可能會(huì)出現(xiàn)請(qǐng)求d的新值,并沒(méi)有被請(qǐng)求c寫入緩存,同樣會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致的情況。

前言

緩存在我們?nèi)粘9ぷ髦?,?jīng)常會(huì)使用,但如果用不好坑也挺多的。

這篇文章總結(jié)了我工作中使用緩存遇到過(guò)的7個(gè)坑,還是非常有參考價(jià)值得,希望對(duì)你會(huì)有所幫助。

圖片圖片

1 緩存穿透

大部分情況下,加緩存的目的是:為了減輕數(shù)據(jù)庫(kù)的壓力,提升系統(tǒng)的性能。

一般情況下,如果有用戶請(qǐng)求過(guò)來(lái),先查緩存,如果緩存中存在數(shù)據(jù),則直接返回。

如果緩存中不存在,則再查數(shù)據(jù)庫(kù),如果數(shù)據(jù)庫(kù)中存在,則將數(shù)據(jù)放入緩存,然后返回。如果數(shù)據(jù)庫(kù)中也不存在,則直接返回失敗。

流程圖如下:

圖片圖片

但如果出現(xiàn)以下這兩種特殊情況,比如:

  • 用戶請(qǐng)求的id在緩存中不存在。
  • 惡意用戶偽造不存在的id發(fā)起請(qǐng)求。

這樣的用戶請(qǐng)求導(dǎo)致的結(jié)果是:每次從緩存中都查不到數(shù)據(jù),而需要查詢數(shù)據(jù)庫(kù),同時(shí)數(shù)據(jù)庫(kù)中也沒(méi)有查到該數(shù)據(jù),也沒(méi)法放入緩存。

也就是說(shuō),每次這個(gè)用戶請(qǐng)求過(guò)來(lái)的時(shí)候,都要查詢一次數(shù)據(jù)庫(kù)。

圖片圖片

圖中標(biāo)紅的箭頭表示每次走的路線。

很顯然,緩存根本沒(méi)起作用,好像被穿透了一樣,每次都會(huì)去訪問(wèn)數(shù)據(jù)庫(kù)。

這就是我們所說(shuō)的:緩存穿透問(wèn)題。

如果此時(shí)穿透了緩存,而直接數(shù)據(jù)庫(kù)的請(qǐng)求數(shù)量非常多,數(shù)據(jù)庫(kù)可能因?yàn)榭覆蛔毫Χ鴴斓?。嗚嗚嗚?/p>

那么問(wèn)題來(lái)了,如何解決這個(gè)問(wèn)題呢?

1.1 校驗(yàn)參數(shù)

我們可以對(duì)用戶id做檢驗(yàn)。

比如你的合法id是15xxxxxx,以15開(kāi)頭的。如果用戶傳入了16開(kāi)頭的id,比如:16232323,則參數(shù)校驗(yàn)失敗,直接把相關(guān)請(qǐng)求攔截掉。這樣可以過(guò)濾掉一部分惡意偽造的用戶id。

1.2 使用布隆過(guò)濾器

如果數(shù)據(jù)比較少,我們可以把數(shù)據(jù)庫(kù)中的數(shù)據(jù),全部放到內(nèi)存的一個(gè)map中。

這樣能夠非常快速的識(shí)別,數(shù)據(jù)在緩存中是否存在。如果存在,則讓其訪問(wèn)緩存。如果不存在,則直接拒絕該請(qǐng)求。

但如果數(shù)據(jù)量太多了,有數(shù)千萬(wàn)或者上億的數(shù)據(jù),全都放到內(nèi)存中,很顯然會(huì)占用太多的內(nèi)存空間。

那么,有沒(méi)有辦法減少內(nèi)存空間呢?

答:這就需要使用布隆過(guò)濾器了。

布隆過(guò)濾器底層使用bit數(shù)組存儲(chǔ)數(shù)據(jù),該數(shù)組中的元素默認(rèn)值是0。

布隆過(guò)濾器第一次初始化的時(shí)候,會(huì)把數(shù)據(jù)庫(kù)中所有已存在的key,經(jīng)過(guò)一些列的hash算法(比如:三次hash算法)計(jì)算,每個(gè)key都會(huì)計(jì)算出多個(gè)位置,然后把這些位置上的元素值設(shè)置成1。

圖片圖片

之后,有用戶key請(qǐng)求過(guò)來(lái)的時(shí)候,再用相同的hash算法計(jì)算位置。

  • 如果多個(gè)位置中的元素值都是1,則說(shuō)明該key在數(shù)據(jù)庫(kù)中已存在。這時(shí)允許繼續(xù)往后面操作。
  • 如果有1個(gè)以上的位置上的元素值是0,則說(shuō)明該key在數(shù)據(jù)庫(kù)中不存在。這時(shí)可以拒絕該請(qǐng)求,而直接返回。

1.3 緩存空值

上面使用布隆過(guò)濾器,雖說(shuō)可以過(guò)濾掉很多不存在的用戶id請(qǐng)求。但它除了增加系統(tǒng)的復(fù)雜度之外,會(huì)帶來(lái)兩個(gè)問(wèn)題:

  • 布隆過(guò)濾器存在誤殺的情況,可能會(huì)把少部分正常用戶的請(qǐng)求也過(guò)濾了。
  • 如果用戶信息有變化,需要實(shí)時(shí)同步到布隆過(guò)濾器,不然會(huì)有問(wèn)題。

所以,通常情況下,我們很少用布隆過(guò)濾器解決緩存穿透問(wèn)題。其實(shí),還有另外一種更簡(jiǎn)單的方案,即:緩存空值。

當(dāng)某個(gè)用戶id在緩存中查不到,在數(shù)據(jù)庫(kù)中也查不到時(shí),也需要將該用戶id緩存起來(lái),只不過(guò)值是空的。

這樣后面的請(qǐng)求,再拿相同的用戶id發(fā)起請(qǐng)求時(shí),就能從緩存中獲取空數(shù)據(jù),直接返回了,而無(wú)需再去查一次數(shù)據(jù)庫(kù)。

優(yōu)化之后的流程圖如下:

圖片圖片

關(guān)鍵點(diǎn)是不管從數(shù)據(jù)庫(kù)有沒(méi)有查到數(shù)據(jù),都將結(jié)果放入緩存中,只是如果沒(méi)有查到數(shù)據(jù),緩存中的值是空的罷了。

2 緩存擊穿

有時(shí)候,我們?cè)谠L問(wèn)熱點(diǎn)數(shù)據(jù)時(shí)。比如:我們?cè)谀硞€(gè)商城購(gòu)買某個(gè)熱門商品。

為了保證訪問(wèn)速度,通常情況下,商城系統(tǒng)會(huì)把商品信息放到緩存中。但如果某個(gè)時(shí)刻,該商品到了過(guò)期時(shí)間失效了。

此時(shí),如果有大量的用戶請(qǐng)求同一個(gè)商品,但該商品在緩存中失效了,一下子這些用戶請(qǐng)求都直接懟到數(shù)據(jù)庫(kù),可能會(huì)造成瞬間數(shù)據(jù)庫(kù)壓力過(guò)大,而直接掛掉。

流程圖如下:

圖片圖片

那么,如何解決這個(gè)問(wèn)題呢?

2.1 加鎖

數(shù)據(jù)庫(kù)壓力過(guò)大的根源是,因?yàn)橥粫r(shí)刻太多的請(qǐng)求訪問(wèn)了數(shù)據(jù)庫(kù)。

如果我們能夠限制,同一時(shí)刻只有一個(gè)請(qǐng)求才能訪問(wèn)某個(gè)productId的數(shù)據(jù)庫(kù)商品信息,不就能解決問(wèn)題了?

答:沒(méi)錯(cuò),我們可以用加鎖的方式,實(shí)現(xiàn)上面的功能。

偽代碼如下:

try {
  String result = jedis.set(productId, requestId, "NX", "PX", expireTime);
  if ("OK".equals(result)) {
    return queryProductFromDbById(productId);
  }
} finally{
    unlock(productId,requestId);
}  
return null;

在訪問(wèn)數(shù)據(jù)庫(kù)時(shí)加鎖,防止多個(gè)相同productId的請(qǐng)求同時(shí)訪問(wèn)數(shù)據(jù)庫(kù)。

然后,還需要一段代碼,把從數(shù)據(jù)庫(kù)中查詢到的結(jié)果,又重新放入緩存中。辦法挺多的,在這里我就不展開(kāi)了。

2.2 自動(dòng)續(xù)期

出現(xiàn)緩存擊穿問(wèn)題是由于key過(guò)期了導(dǎo)致的。那么,我們換一種思路,在key快要過(guò)期之前,就自動(dòng)給它續(xù)期,不就OK了?

答:沒(méi)錯(cuò),我們可以用job給指定key自動(dòng)續(xù)期。

比如說(shuō),我們有個(gè)分類功能,設(shè)置的緩存過(guò)期時(shí)間是30分鐘。但有個(gè)job每隔20分鐘執(zhí)行一次,自動(dòng)更新緩存,重新設(shè)置過(guò)期時(shí)間為30分鐘。

圖片圖片

這樣就能保證,分類緩存不會(huì)失效。

2.3 永久有效

此外,對(duì)于很多熱門key,其實(shí)是可以不用設(shè)置過(guò)期時(shí)間,讓其永久有效的。

比如參與秒殺活動(dòng)的熱門商品,由于這類商品id并不多,在緩存中我們可以不設(shè)置過(guò)期時(shí)間。

在秒殺活動(dòng)開(kāi)始前,我們先用一個(gè)程序提前從數(shù)據(jù)庫(kù)中查詢出商品的數(shù)據(jù),然后同步到緩存中,提前做預(yù)熱。

等秒殺活動(dòng)結(jié)束一段時(shí)間之后,我們?cè)偈謩?dòng)刪除這些無(wú)用的緩存即可。

3 緩存雪崩

而緩存雪崩是緩存擊穿的升級(jí)版,緩存擊穿說(shuō)的是某一個(gè)熱門key失效了,而緩存雪崩說(shuō)的是有多個(gè)熱門key同時(shí)失效。

看起來(lái),如果發(fā)生緩存雪崩,問(wèn)題更嚴(yán)重。

緩存雪崩目前有兩種:

  • 有大量的熱門緩存,同時(shí)失效。會(huì)導(dǎo)致大量的請(qǐng)求,訪問(wèn)數(shù)據(jù)庫(kù)。而數(shù)據(jù)庫(kù)很有可能因?yàn)榭覆蛔毫?,而直接掛掉?/li>
  • 緩存服務(wù)器down機(jī)了,可能是機(jī)器硬件問(wèn)題,或者機(jī)房網(wǎng)絡(luò)問(wèn)題??傊?,造成了整個(gè)緩存的不可用。

歸根結(jié)底都是有大量的請(qǐng)求,透過(guò)緩存,而直接訪問(wèn)數(shù)據(jù)庫(kù)了。

圖片圖片

那么,要如何解決這個(gè)問(wèn)題呢?

3.1 過(guò)期時(shí)間加隨機(jī)數(shù)

為了解決緩存雪崩問(wèn)題,我們首先要盡量避免緩存同時(shí)失效的情況發(fā)生。

這就要求我們不要設(shè)置相同的過(guò)期時(shí)間。

可以在設(shè)置的過(guò)期時(shí)間基礎(chǔ)上,再加個(gè)1~60秒的隨機(jī)數(shù)。

實(shí)際過(guò)期時(shí)間 = 過(guò)期時(shí)間 + 1~60秒的隨機(jī)數(shù)

這樣即使在高并發(fā)的情況下,多個(gè)請(qǐng)求同時(shí)設(shè)置過(guò)期時(shí)間,由于有隨機(jī)數(shù)的存在,也不會(huì)出現(xiàn)太多相同的過(guò)期key。

3.2 保證高可用

針對(duì)緩存服務(wù)器down機(jī)的情況,在前期做系統(tǒng)設(shè)計(jì)時(shí),可以做一些高可用架構(gòu)。

比如:如果使用了redis,可以使用哨兵模式,或者集群模式,避免出現(xiàn)單節(jié)點(diǎn)故障導(dǎo)致整個(gè)redis服務(wù)不可用的情況。

使用哨兵模式之后,當(dāng)某個(gè)master服務(wù)下線時(shí),自動(dòng)將該master下的某個(gè)slave服務(wù)升級(jí)為master服務(wù),替代已下線的master服務(wù)繼續(xù)處理請(qǐng)求。

3.3 服務(wù)降級(jí)

如果做了高可用架構(gòu),redis服務(wù)還是掛了,該怎么辦呢?

這時(shí)候,就需要做服務(wù)降級(jí)了。

我們需要配置一些默認(rèn)的兜底數(shù)據(jù)。

程序中有個(gè)全局開(kāi)關(guān),比如有10個(gè)請(qǐng)求在最近一分鐘內(nèi),從redis中獲取數(shù)據(jù)失敗,則全局開(kāi)關(guān)打開(kāi)。后面的新請(qǐng)求,就直接從配置中心中獲取默認(rèn)的數(shù)據(jù)。

當(dāng)然,還需要有個(gè)job,每隔一定時(shí)間去從redis中獲取數(shù)據(jù),如果在最近一分鐘內(nèi)可以獲取到兩次數(shù)據(jù)(這個(gè)參數(shù)可以自己定),則把全局開(kāi)關(guān)關(guān)閉。后面來(lái)的請(qǐng)求,又可以正常從redis中獲取數(shù)據(jù)了。

需要特別說(shuō)一句,該方案并非所有的場(chǎng)景都適用,需要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景決定。

4 數(shù)據(jù)不一致

數(shù)據(jù)庫(kù)和緩存(比如:redis)雙寫數(shù)據(jù)一致性問(wèn)題,是一個(gè)跟開(kāi)發(fā)語(yǔ)言無(wú)關(guān)的公共問(wèn)題。尤其在高并發(fā)的場(chǎng)景下,這個(gè)問(wèn)題變得更加嚴(yán)重。

那么,我們?cè)撊绾胃戮彺婺兀?/p>

目前有以下4種方案:

  • 先寫緩存,再寫數(shù)據(jù)庫(kù)
  • 先寫數(shù)據(jù)庫(kù),再寫緩存
  • 先刪緩存,再寫數(shù)據(jù)庫(kù)
  • 先寫數(shù)據(jù)庫(kù),再刪緩存

4.1 先寫緩存,再寫數(shù)據(jù)庫(kù)

對(duì)于更新緩存的方案,很多人第一個(gè)想到的可能是在寫操作中直接更新緩存(寫緩存),更直接明了。

那么,問(wèn)題來(lái)了:在寫操作中,到底是先寫緩存,還是先寫數(shù)據(jù)庫(kù)呢?

我們?cè)谶@里先聊聊先寫緩存,再寫數(shù)據(jù)庫(kù)的情況,因?yàn)樗膯?wèn)題最嚴(yán)重。

某一個(gè)用戶的每一次寫操作,如果剛寫完緩存,突然網(wǎng)絡(luò)出現(xiàn)了異常,導(dǎo)致寫數(shù)據(jù)庫(kù)失敗了。

圖片圖片

其結(jié)果是緩存更新成了最新數(shù)據(jù),但數(shù)據(jù)庫(kù)沒(méi)有,這樣緩存中的數(shù)據(jù)不就變成臟數(shù)據(jù)了?如果此時(shí)該用戶的查詢請(qǐng)求,正好讀取到該數(shù)據(jù),就會(huì)出現(xiàn)問(wèn)題,因?yàn)樵摂?shù)據(jù)在數(shù)據(jù)庫(kù)中根本不存在,這個(gè)問(wèn)題非常嚴(yán)重。

我們都知道,緩存的主要目的是把數(shù)據(jù)庫(kù)的數(shù)據(jù)臨時(shí)保存在內(nèi)存,便于后續(xù)的查詢,提升查詢速度。

但如果某條數(shù)據(jù),在數(shù)據(jù)庫(kù)中都不存在,你緩存這種“假數(shù)據(jù)”又有啥意義呢?

因此,先寫緩存,再寫數(shù)據(jù)庫(kù)的方案是不可取的,在實(shí)際工作中用得不多。

4.2 先寫數(shù)據(jù)庫(kù),再寫緩存

既然上面的方案行不通,接下來(lái),聊聊先寫數(shù)據(jù)庫(kù),再寫緩存的方案,該方案在低并發(fā)編程中有人在用(我猜的)。

用戶的寫操作,先寫數(shù)據(jù)庫(kù),再寫緩存,可以避免之前“假數(shù)據(jù)”的問(wèn)題。但它卻帶來(lái)了新的問(wèn)題。

什么問(wèn)題呢?

4.2.1 寫緩存失敗了

如果把寫數(shù)據(jù)庫(kù)和寫緩存操作,放在同一個(gè)事務(wù)當(dāng)中,當(dāng)寫緩存失敗了,我們可以把寫入數(shù)據(jù)庫(kù)的數(shù)據(jù)進(jìn)行回滾。

圖片圖片

如果是并發(fā)量比較小,對(duì)接口性能要求不太高的系統(tǒng),可以這么玩。

但如果在高并發(fā)的業(yè)務(wù)場(chǎng)景中,寫數(shù)據(jù)庫(kù)和寫緩存,都屬于遠(yuǎn)程操作。為了防止出現(xiàn)大事務(wù),造成的死鎖問(wèn)題,通常建議寫數(shù)據(jù)庫(kù)和寫緩存不要放在同一個(gè)事務(wù)中。

也就是說(shuō)在該方案中,如果寫數(shù)據(jù)庫(kù)成功了,但寫緩存失敗了,數(shù)據(jù)庫(kù)中已寫入的數(shù)據(jù)不會(huì)回滾。

這就會(huì)出現(xiàn):數(shù)據(jù)庫(kù)是新數(shù)據(jù),而緩存是舊數(shù)據(jù),兩邊數(shù)據(jù)不一致的情況。

4.2.2 高并發(fā)下的問(wèn)題

假設(shè)在高并發(fā)的場(chǎng)景中,針對(duì)同一個(gè)用戶的同一條數(shù)據(jù),有兩個(gè)寫數(shù)據(jù)請(qǐng)求:a和b,它們同時(shí)請(qǐng)求到業(yè)務(wù)系統(tǒng)。

在這個(gè)過(guò)程當(dāng)中,可能會(huì)出現(xiàn)請(qǐng)求b在緩存中的新數(shù)據(jù),被請(qǐng)求a的舊數(shù)據(jù)覆蓋了。

也就是說(shuō):在高并發(fā)場(chǎng)景中,如果多個(gè)線程同時(shí)執(zhí)行先寫數(shù)據(jù)庫(kù),再寫緩存的操作,可能會(huì)出現(xiàn)數(shù)據(jù)庫(kù)是新值,而緩存中是舊值,兩邊數(shù)據(jù)不一致的情況。

4.2.3 浪費(fèi)系統(tǒng)資源

該方案還有一個(gè)比較大的問(wèn)題就是:每個(gè)寫操作,寫完數(shù)據(jù)庫(kù),會(huì)馬上寫緩存,比較浪費(fèi)系統(tǒng)資源。

為什么這么說(shuō)呢?

你可以試想一下,如果寫的緩存,并不是簡(jiǎn)單的數(shù)據(jù)內(nèi)容,而是要經(jīng)過(guò)非常復(fù)雜的計(jì)算得出的最終結(jié)果。這樣每寫一次緩存,都需要經(jīng)過(guò)一次非常復(fù)雜的計(jì)算,不是非常浪費(fèi)系統(tǒng)資源嗎?

尤其是cpu和內(nèi)存資源。

還有些業(yè)務(wù)場(chǎng)景比較特殊:寫多讀少。

如果在這類業(yè)務(wù)場(chǎng)景中,每個(gè)用的寫操作,都需要寫一次緩存,有點(diǎn)得不償失。

由此可見(jiàn),在高并發(fā)的場(chǎng)景中,先寫數(shù)據(jù)庫(kù),再寫緩存,這套方案問(wèn)題挺多的,也不太建議使用。

4.3 先刪緩存,再寫數(shù)據(jù)庫(kù)

說(shuō)白了,在用戶的寫操作中,先執(zhí)行刪除緩存操作,再去寫數(shù)據(jù)庫(kù)。這套方案,可以是可以,但也會(huì)有一樣問(wèn)題。

4.3.1 高并發(fā)下的問(wèn)題

假設(shè)在高并發(fā)的場(chǎng)景中,同一個(gè)用戶的同一條數(shù)據(jù),有一個(gè)讀數(shù)據(jù)請(qǐng)求c,還有另一個(gè)寫數(shù)據(jù)請(qǐng)求d(一個(gè)更新操作),同時(shí)請(qǐng)求到業(yè)務(wù)系統(tǒng)。

在這個(gè)過(guò)程當(dāng)中,有可能會(huì)出現(xiàn)請(qǐng)求d的新值,并沒(méi)有被請(qǐng)求c寫入緩存,同樣會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致的情況。

4.4 先寫數(shù)據(jù)庫(kù),再刪緩存

在高并發(fā)的場(chǎng)景中,有一個(gè)讀數(shù)據(jù)請(qǐng)求f,有一個(gè)寫數(shù)據(jù)請(qǐng)求e。

在高并發(fā)的場(chǎng)景中,有一個(gè)讀數(shù)據(jù)請(qǐng)求,有一個(gè)寫數(shù)據(jù)請(qǐng)求,更新過(guò)程如下:

請(qǐng)求e先寫數(shù)據(jù)庫(kù),由于網(wǎng)絡(luò)原因卡頓了一下,沒(méi)有來(lái)得及刪除緩存。請(qǐng)求f查詢緩存,發(fā)現(xiàn)緩存中有數(shù)據(jù),直接返回該數(shù)據(jù)。請(qǐng)求e刪除緩存。在這個(gè)過(guò)程中,只有請(qǐng)求f讀了一次舊數(shù)據(jù),后來(lái)舊數(shù)據(jù)被請(qǐng)求e及時(shí)刪除了,看起來(lái)問(wèn)題不大。

但如果是讀數(shù)據(jù)請(qǐng)求先過(guò)來(lái)呢?

  • 請(qǐng)求f查詢緩存,發(fā)現(xiàn)緩存中有數(shù)據(jù),直接返回該數(shù)據(jù)。
  • 請(qǐng)求e先寫數(shù)據(jù)庫(kù)。
  • 請(qǐng)求e刪除緩存。

這種情況看起來(lái)也沒(méi)問(wèn)題呀?

答:對(duì)的。

但就怕出現(xiàn)下面這種情況,即緩存自己失效了。如下圖所示:

圖片圖片

  • 緩存過(guò)期時(shí)間到了,自動(dòng)失效。
  • 請(qǐng)求f查詢緩存,發(fā)緩存中沒(méi)有數(shù)據(jù),查詢數(shù)據(jù)庫(kù)的舊值,但由于網(wǎng)絡(luò)原因卡頓了,沒(méi)有來(lái)得及更新緩存。
  • 請(qǐng)求e先寫數(shù)據(jù)庫(kù),接著刪除了緩存。
  • 請(qǐng)求f更新舊值到緩存中。

這時(shí),緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)同樣出現(xiàn)不一致的情況了。

但這種情況還是比較少的,需要同時(shí)滿足以下條件才可以:

  • 緩存剛好自動(dòng)失效。
  • 請(qǐng)求f從數(shù)據(jù)庫(kù)查出舊值,更新緩存的耗時(shí),比請(qǐng)求e寫數(shù)據(jù)庫(kù),并且刪除緩存的還長(zhǎng)。

我們都知道查詢數(shù)據(jù)庫(kù)的速度,一般比寫數(shù)據(jù)庫(kù)要快,更何況寫完數(shù)據(jù)庫(kù),還要?jiǎng)h除緩存。所以絕大多數(shù)情況下,寫數(shù)據(jù)請(qǐng)求比讀數(shù)據(jù)情況耗時(shí)更長(zhǎng)。

由此可見(jiàn),系統(tǒng)同時(shí)滿足上述兩個(gè)條件的概率非常小。

如果大家想更詳細(xì)的了解數(shù)據(jù)和緩存雙寫一致性問(wèn)題,可以看看我之前寫的一篇文章《如何保證數(shù)據(jù)庫(kù)和緩存雙寫一致性?》,里面有非常詳細(xì)的介紹。

5 大key問(wèn)題

我們?cè)谑褂镁彺娴臅r(shí)候,特別是Redis,還有一個(gè)經(jīng)常會(huì)遇到的問(wèn)題是大key問(wèn)題。

可能系統(tǒng)剛上線時(shí),數(shù)據(jù)量少,在Redis中定義的key比較小,開(kāi)發(fā)人員在做系統(tǒng)設(shè)計(jì)時(shí),也沒(méi)考慮這個(gè)問(wèn)題。

系統(tǒng)運(yùn)行了很長(zhǎng)一段時(shí)間也沒(méi)有問(wèn)題。

但隨著時(shí)間的推移,用戶的數(shù)據(jù)越來(lái)越多,慢慢形成了大key問(wèn)題。

可能在突然的某一天之后發(fā)現(xiàn),線上某個(gè)接口耗時(shí)越來(lái)越長(zhǎng)了。

追查原因,發(fā)現(xiàn)是大key問(wèn)題導(dǎo)致的。

大key問(wèn)題是指:緩存中單個(gè)key的value值過(guò)大。

之前我開(kāi)發(fā)過(guò)一個(gè)分類樹(shù)查詢接口,為了性能考慮,使用job提前將分類樹(shù),保存到緩存中。

剛開(kāi)始分類不多,只有幾百個(gè),分類樹(shù)查詢接口的響應(yīng)挺快的。

但用了幾年之后,分類數(shù)據(jù)漲到了上萬(wàn)個(gè),該接口出現(xiàn)了性能問(wèn)題,一查發(fā)現(xiàn)是大key引起的。

我們需要做優(yōu)化,那么如何優(yōu)化呢?

5.1 縮減字段名

為了優(yōu)化在Redis中存儲(chǔ)數(shù)據(jù)的大小,我們首先需要對(duì)數(shù)據(jù)進(jìn)行瘦身。

只保存需要用到的字段。

例如:

@AllArgsConstructor
@Data
public class Category {

    private Long id;
    private String name;
    private Long parentId;
    private Date inDate;
    private Long inUserId;
    private String inUserName;
    private List<Category> children;
}

像這個(gè)分類對(duì)象中inDate、inUserId和inUserName字段是可以不用保存的。

修改自動(dòng)名稱。

例如:

@AllArgsConstructor
@Data
public class Category {
    /**
     * 分類編號(hào)
     */
    @JsonProperty("i")
    private Long id;

    /**
     * 分類層級(jí)
     */
    @JsonProperty("l")
    private Integer level;

    /**
     * 分類名稱
     */
    @JsonProperty("n")
    private String name;

    /**
     * 父分類編號(hào)
     */
    @JsonProperty("p")
    private Long parentId;

    /**
     * 子分類列表
     */
    @JsonProperty("c")
    private List<Category> children;
}

由于在一萬(wàn)多條數(shù)據(jù)中,每條數(shù)據(jù)的字段名稱是固定的,他們的重復(fù)率太高了。

由此,可以在json序列化時(shí),改成一個(gè)簡(jiǎn)短的名稱,以便于返回更少的數(shù)據(jù)大小。

5.2 數(shù)據(jù)做壓縮

這還不夠,需要對(duì)存儲(chǔ)的數(shù)據(jù)做壓縮。

之前在Redis中保存的key/value,其中的value是json格式的字符串。

其實(shí)RedisTemplate支持,value保存byte數(shù)組。

先將json字符串?dāng)?shù)據(jù)用GZip工具類壓縮成byte數(shù)組,然后保存到Redis中。

再獲取數(shù)據(jù)時(shí),將byte數(shù)組轉(zhuǎn)換成json字符串,然后再轉(zhuǎn)換成分類樹(shù)。

這樣優(yōu)化之后,保存到Redis中的分類樹(shù)的數(shù)據(jù)大小,一下子減少了10倍,Redis的大key問(wèn)題被解決了。

如果大家對(duì)大key問(wèn)題如何優(yōu)化,比較感興趣,可以看看我的另一篇文章《分類樹(shù),我從2s優(yōu)化到0.1s》,里面有真實(shí)的案例。

6 熱key問(wèn)題

不知道大家聽(tīng)說(shuō)過(guò)二八原理沒(méi)有。

80%的用戶經(jīng)常訪問(wèn)20%的熱點(diǎn)數(shù)據(jù)。

這樣帶來(lái)的結(jié)果是數(shù)據(jù)的傾斜,不能均勻分布,尤其是高并發(fā)系統(tǒng)中問(wèn)題比較大。

比如你現(xiàn)在搞了一個(gè)促銷活動(dòng),有幾款商品性價(jià)比非常高,這些商品數(shù)據(jù)在Redis中按分片保存的,不同的數(shù)據(jù)保存在不同的服務(wù)器節(jié)點(diǎn)上。

如果用戶瘋狂搶購(gòu)其中3款商品,而這3款商品正好保存在同一臺(tái)Redis服務(wù)端節(jié)點(diǎn)。

這樣會(huì)出現(xiàn)大量的用戶請(qǐng)求集中訪問(wèn)同一天Redis服務(wù)器節(jié)點(diǎn),該節(jié)點(diǎn)很有可能會(huì)因?yàn)榭覆蛔∵@么大的壓力,而直接down機(jī)。

這個(gè)就是熱key問(wèn)題帶來(lái)的危害。

那么,如何解決這個(gè)問(wèn)題呢?

6.1 拆分key

在促銷活動(dòng)開(kāi)始之前,我們要提前做好評(píng)估,分析這些商品哪些是熱點(diǎn)商品。

然后將熱點(diǎn)商品分開(kāi)保存,不要集中保存到同一臺(tái)Redis服務(wù)器節(jié)點(diǎn)。

這樣不同的Redis服務(wù)器節(jié)點(diǎn),可以分?jǐn)傄恍┯脩舻恼?qǐng)求壓力。

6.2 增加本地緩存

對(duì)應(yīng)熱key,我們可以增加一層本地緩存,能夠提升性能的同時(shí)也能避免Redis訪問(wèn)量過(guò)大的問(wèn)題。

但帶來(lái)的壞處是,可能會(huì)出現(xiàn)數(shù)據(jù)不一致問(wèn)題,要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景選擇。

7 命中率問(wèn)題

緩存的命中率問(wèn)題,是一個(gè)讓人非常頭疼的問(wèn)題。

前面的章節(jié)已經(jīng)介紹過(guò)。

一般情況下,如果有用戶請(qǐng)求過(guò)來(lái),先查緩存,如果緩存中存在數(shù)據(jù),則直接返回。

如果緩存中不存在,則再查數(shù)據(jù)庫(kù),如果數(shù)據(jù)庫(kù)中存在,則將數(shù)據(jù)放入緩存,然后返回。如果數(shù)據(jù)庫(kù)中也不存在,則直接返回失敗。

流程圖如下:

圖片圖片

緩存命中:直接從緩存中獲取數(shù)據(jù)。

緩存不命中:無(wú)法從緩存中獲取數(shù)據(jù),而要從數(shù)據(jù)庫(kù)獲取其他途徑獲取數(shù)據(jù)。

我們肯定是希望緩存命中率越高越好,這樣接口的性能越好,但實(shí)際工作中卻經(jīng)常啪啪打臉。

因?yàn)榭赡軙?huì)出現(xiàn)緩存不存在,或者緩存過(guò)期等問(wèn)題,導(dǎo)致緩存不能命中。

那么,如何提升緩存的命中率呢?

7.1 緩存預(yù)熱

我們?cè)贏PI服務(wù)啟動(dòng)之前,可以先用job,將相關(guān)數(shù)據(jù)先保存到緩存中,做預(yù)熱。

這樣后面的用戶請(qǐng)求,就能直接從緩存中獲取數(shù)據(jù),而無(wú)需訪問(wèn)數(shù)據(jù)庫(kù)了。

7.2 合理調(diào)整過(guò)期時(shí)間

有時(shí)候,我們給緩存設(shè)置的過(guò)期時(shí)間太短,導(dǎo)致后面會(huì)產(chǎn)生大量的過(guò)期緩存。

會(huì)導(dǎo)致緩存命中率非常低。

這時(shí)需要合理調(diào)整過(guò)期時(shí)間,比如:之前設(shè)置1秒的,現(xiàn)在改成5秒,10秒,30秒或者1分鐘等等。

7.3 增加緩存內(nèi)存

如果我們部署的Redis服務(wù)器的內(nèi)存太小,很容易出現(xiàn)內(nèi)存不足的情況,從而會(huì)頻繁觸發(fā)內(nèi)存淘汰機(jī)制。

也會(huì)影響緩存的命中率。

這種情況下,我們需要增加緩存內(nèi)存。

緩存的內(nèi)存過(guò)小問(wèn)題,也經(jīng)常會(huì)出現(xiàn)。

今天的內(nèi)容先分享到這里,感謝你的閱讀,希望對(duì)你會(huì)有所幫助。

責(zé)任編輯:武曉燕 來(lái)源: 蘇三說(shuō)技術(shù)
相關(guān)推薦

2017-07-17 15:46:20

Oracle并行機(jī)制

2019-10-30 14:44:41

Prometheus開(kāi)源監(jiān)控系統(tǒng)

2024-04-01 08:05:27

Go開(kāi)發(fā)Java

2015-03-24 16:29:55

默認(rèn)線程池java

2020-11-03 13:50:31

Redis緩存數(shù)據(jù)庫(kù)

2023-12-14 17:34:22

Kubernetes集群K8s

2025-04-29 10:17:42

2018-01-10 13:40:03

數(shù)據(jù)庫(kù)MySQL表設(shè)計(jì)

2024-04-10 08:39:56

BigDecimal浮點(diǎn)數(shù)二進(jìn)制

2024-06-26 10:37:05

2018-09-11 09:14:52

面試公司缺點(diǎn)

2023-03-13 13:36:00

Go擴(kuò)容切片

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2021-02-21 09:28:24

kafka系統(tǒng)并發(fā)量

2024-03-14 10:30:05

緩存場(chǎng)景DEMO

2018-01-10 06:17:24

2022-04-26 21:49:55

Spring事務(wù)數(shù)據(jù)庫(kù)

2025-02-26 00:33:59

Java編程程序

2018-04-08 22:16:21

2020-11-18 10:16:23

人工智能機(jī)器學(xué)習(xí)技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)