Redis,一文帶你領(lǐng)略它的方方面面

1. 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)及其應(yīng)用場(chǎng)景

Redis的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)分別為:String、Hash、List、Set、Zset。

  • String: 字符串

  • Hash: 散列

  • List: 列表

  • Set: 集合

  • Sorted Set(Zset): 有序集合

1.1 String

key都是字符串,value可以是五種數(shù)據(jù)類型。

使用場(chǎng)景:

  • 緩存;

  • 計(jì)數(shù)器:incr key,對(duì)應(yīng)鍵值自增1,如果key不存在,自增后get(key)=1,由于是單線程無(wú)競(jìng)爭(zhēng),為此不會(huì)出錯(cuò)。可以應(yīng)用于網(wǎng)站記錄每個(gè)用戶個(gè)人主頁(yè)的訪問(wèn)量,一定時(shí)間后再將訪問(wèn)量持久到數(shù)據(jù)庫(kù)中,這樣就不用每次多一個(gè)人訪問(wèn)就修改一次數(shù)據(jù)庫(kù)中的訪問(wèn)量值,提高性能;

  • 分布式鎖:setnx key value,key不存在時(shí),才生效,設(shè)置成功返回1,失敗則返回0。根據(jù)返回值確定當(dāng)前線程是否獲得鎖;

  • 簡(jiǎn)單分布式id生成:利用incr的自增實(shí)現(xiàn)分布式應(yīng)用id唯一。

1.2 Hash

key為字符串,值分為兩部分field和value,視為屬性和值??梢园裬ey當(dāng)作一張表的一行,Key就代表一個(gè)id,每個(gè)屬性可以看作關(guān)系型數(shù)據(jù)庫(kù)的一個(gè)字段。fields不能相同,value可以。

image.png

1.3 List

key是字符串,value是一個(gè)有序的list。特點(diǎn)是有序、可以重復(fù)。Redis列表是簡(jiǎn)單的字符串列表,按照插入順序排序。你可以添加一個(gè)元素到列表的頭部(左邊)或者尾部(右邊)一個(gè)列表最多可以包含 2^32 - 1 個(gè)元素 (4294967295, 每個(gè)列表超過(guò)40億個(gè)元素)。

為了說(shuō)明其應(yīng)用場(chǎng)景,我們先了解下主要的幾個(gè)命令:

命令格式 描述
LTRIM key start stop 對(duì)一個(gè)列表進(jìn)行修剪(trim),就是說(shuō),讓列表只保留指定區(qū)間內(nèi)的元素,不在指定區(qū)間之內(nèi)的元素都將被刪除。
LPOP key 移出并獲取列表的第一個(gè)元素
RPOP key 移除列表的最后一個(gè)元素,返回值為移除的元素。
LPUSH key 將一個(gè)或多個(gè)值插入到列表頭部
RPUSH key 將一個(gè)或多個(gè)值插入到列表尾部
RPOPLPUSH source des 從源列表中彈出最后一個(gè)元素,將彈出的元素插入到目標(biāo)列表頭部并返回它;
BRPOP key timeout 移出并獲取列表的最后一個(gè)元素, 如果列表沒(méi)有元素會(huì)阻塞列表直到等待超時(shí)或發(fā)現(xiàn)可彈出元素為止。前面加的B其實(shí)為Blocking的首字母。

應(yīng)用場(chǎng)景:

  • LPUSH + LPOP = Stack (棧);

  • LPUSH + RPOP = Queue(隊(duì)列);

  • LPUSH + LTRIM = Capped Collection(固定集合。對(duì)于大小固定,我們可以想象其就像一個(gè)環(huán)形隊(duì)列,當(dāng)集合空間用完后,再插入的元素就會(huì)覆蓋最初始的頭部的元素);

  • LPUSH + BRPOP = Message Quene(消息隊(duì)列,利用BRPOP的阻塞性,實(shí)現(xiàn)阻塞消息隊(duì)列);

  • RPOPLPUSH可應(yīng)用于物流,假如某派送流程為:發(fā)貨->中轉(zhuǎn)A->中轉(zhuǎn)B->送達(dá)目的地,當(dāng)商家發(fā)貨后,并送達(dá)了中轉(zhuǎn)A,用戶應(yīng)該可以看到已發(fā)貨(即完成了發(fā)貨流程)。此時(shí),派送列表流程下一步應(yīng)該為:中轉(zhuǎn)B->送達(dá)目的地,用戶查看列表為發(fā)貨-> 中轉(zhuǎn)A。

1.4 Set

Redis 的 Set 是 String 類型的無(wú)序集合。集合成員是唯一的,這就意味著集合中不能出現(xiàn)重復(fù)的數(shù)據(jù)。Redis 中集合是通過(guò)哈希表實(shí)現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是 O(1)。集合中最大的成員數(shù)為 2^32 - 1 (4294967295, 每個(gè)集合可存儲(chǔ)40多億個(gè)成員)。

為了說(shuō)明應(yīng)用場(chǎng)景,我們也先了解以下主要幾個(gè)命令:

命令格式 描述
SADD key member1 [member2] 向集合添加一個(gè)或多個(gè)成員
SPOP key 移除并返回集合中的一個(gè)隨機(jī)元素
SRANDMEMBER key [count] 返回集合中一個(gè)或多個(gè)隨機(jī)數(shù)
SINTER key1 [key2] 返回給定所有集合的交集

應(yīng)用場(chǎng)景:

  • SADD = Tagging(給用戶添加標(biāo)簽);

  • SPOP/SRANDMEMBER = Random item(隨機(jī)元素,可用于抽獎(jiǎng)平臺(tái)抽獎(jiǎng)等)

  • SADD+ SINTER= social graph (社交相關(guān)應(yīng)用,如微博的共同關(guān)注,QQ的共同好友等)。

1.5 Zset

Redis 有序集合和集合一樣也是string類型元素的集合,且不允許重復(fù)的成員。不同的是每個(gè)元素都會(huì)關(guān)聯(lián)一個(gè)double類型的分?jǐn)?shù)。redis正是通過(guò)分?jǐn)?shù)來(lái)為集合中的成員進(jìn)行從小到大的排序。有序集合的成員是唯一的,但分?jǐn)?shù)(score)卻可以重復(fù)。集合是通過(guò)哈希表實(shí)現(xiàn)的,所以添加,刪除,查找的復(fù)雜度都是O(1)。 集合中最大的成員數(shù)為 2^32 - 1 (4294967295, 每個(gè)集合可存儲(chǔ)40多億個(gè)成員)。

image.png

應(yīng)用場(chǎng)景:

  • 排行榜;

  • 分?jǐn)?shù)添加和更新;

2. 過(guò)期策略

2.1 定時(shí)刪除

在設(shè)置key的過(guò)期時(shí)間的同時(shí),為該key創(chuàng)建一個(gè)定時(shí)器,讓定時(shí)器在key的過(guò)期時(shí)間來(lái)臨時(shí),對(duì)key進(jìn)行刪除;

優(yōu)點(diǎn):保證內(nèi)存被盡快釋放;

缺點(diǎn)

  • 若過(guò)期key很多,刪除這些key會(huì)占用很多的CPU時(shí)間;

  • 定時(shí)器的創(chuàng)建耗時(shí),若為每一個(gè)設(shè)置過(guò)期時(shí)間的key創(chuàng)建一個(gè)定時(shí)器(將會(huì)有大量的定時(shí)器產(chǎn)生),性能影響嚴(yán)重。

2.2 惰性刪除

key過(guò)期的時(shí)候不刪除,每次從數(shù)據(jù)庫(kù)獲取key的時(shí)候去檢查是否過(guò)期,若過(guò)期,則刪除,返回null。

優(yōu)點(diǎn):刪除操作只發(fā)生在從數(shù)據(jù)庫(kù)取出key的時(shí)候發(fā)生,而且只刪除當(dāng)前key,所以對(duì)CPU時(shí)間的占用是比較少的,而且此時(shí)的刪除是已經(jīng)到了非做不可的地步(如果此時(shí)還不刪除的話,我們就會(huì)獲取到了已經(jīng)過(guò)期的key了);

缺點(diǎn):若大量的key在超出超時(shí)時(shí)間后,很久一段時(shí)間內(nèi),都沒(méi)有被獲取過(guò),那么可能發(fā)生內(nèi)存泄露(無(wú)用的垃圾占用了大量的內(nèi)存)。

2.3 定期刪除

每隔一段時(shí)間執(zhí)行一次刪除(在redis.conf配置文件設(shè)置hz,1s刷新的頻率)過(guò)期key操作。需要合理設(shè)置刪除操作的執(zhí)行時(shí)長(zhǎng)(每次刪除執(zhí)行多長(zhǎng)時(shí)間)和執(zhí)行頻率(每隔多長(zhǎng)時(shí)間做一次刪除)。

優(yōu)點(diǎn)

  • 通過(guò)限制刪除操作的時(shí)長(zhǎng)和頻率,來(lái)減少刪除操作對(duì)CPU時(shí)間的占用--處理"定時(shí)刪除"的缺點(diǎn);

  • 定期刪除過(guò)期key--處理"惰性刪除"的缺點(diǎn)。

缺點(diǎn)

  • 在內(nèi)存友好方面,不如"定時(shí)刪除";

  • 在CPU時(shí)間友好方面,不如"惰性刪除"。

2.4 Redis采用的過(guò)期策略

redis采用的過(guò)期策略為惰性刪除+定期刪除,其兩大流程如下:

  • 惰性刪除流程:

    • 在進(jìn)行g(shù)et或setnx等操作時(shí),先檢查key是否過(guò)期;

    • 若過(guò)期,則刪除key,然后執(zhí)行相應(yīng)的操作;

    • 若沒(méi)過(guò)期,則直接執(zhí)行相應(yīng)操作。

  • 定期刪除流程:

    • 對(duì)指定的n個(gè)數(shù)據(jù)庫(kù)(redis默認(rèn)的n為16),每一個(gè)庫(kù)隨機(jī)刪除小于等于指定的m個(gè)過(guò)期key;

    • 遍歷每個(gè)數(shù)據(jù)庫(kù),并檢查當(dāng)前庫(kù)中的m個(gè)key(默認(rèn)m是20個(gè),即每個(gè)庫(kù)檢查20個(gè)key,相當(dāng)于循環(huán)執(zhí)行20次);

    • 如果當(dāng)前庫(kù)沒(méi)有一個(gè)key設(shè)置了過(guò)期時(shí)間,則直接執(zhí)行一下個(gè)庫(kù)的遍歷;

    • 隨機(jī)獲取一個(gè)設(shè)置了過(guò)期時(shí)間的key,檢查該key是否過(guò)期,如果過(guò)期,刪除key;

    • 判斷定期刪除操作是否已經(jīng)達(dá)到指定時(shí)長(zhǎng),若已經(jīng)達(dá)到,直接退出定期刪除。

3. 持久化策略

由于redis是基于內(nèi)存的數(shù)據(jù)庫(kù),其運(yùn)行時(shí)數(shù)據(jù)都保存在內(nèi)存中,在沒(méi)有進(jìn)行持久化數(shù)據(jù)的情況下,一旦redis服務(wù)器關(guān)閉,則會(huì)丟失內(nèi)存中的數(shù)據(jù)。為此Redis為我們提供了兩種持久化機(jī)制,分別是RDB(Redis DataBase)和AOF(Append Only File)。

3.1 RDB機(jī)制

RDB持久化是指在指定的時(shí)間間隔內(nèi)將內(nèi)存中的數(shù)據(jù)集快照寫(xiě)入磁盤(pán)。也是默認(rèn)的持久化方式,這種方式是就是將內(nèi)存中數(shù)據(jù)以快照的方式寫(xiě)入到二進(jìn)制文件中,默認(rèn)的文件名為dump.rdb。

既然RDB機(jī)制是通過(guò)把某個(gè)時(shí)刻的所有數(shù)據(jù)生成一個(gè)快照來(lái)保存,那么就應(yīng)該有一種觸發(fā)機(jī)制,是實(shí)現(xiàn)這個(gè)過(guò)程。對(duì)于RDB來(lái)說(shuō),提供了三種機(jī)制:save、bgsave、自動(dòng)化。我們分別來(lái)看一下:

3.1.1 save觸發(fā)方式

該命令會(huì)阻塞當(dāng)前Redis服務(wù)器,執(zhí)行save命令期間,Redis不能處理其他命令,直到RDB過(guò)程完成為止。具體流程如下:

image.png

執(zhí)行完成時(shí)候如果存在老的RDB文件,就用新的替代掉舊的。我們的客戶端可能都是幾萬(wàn)或者是幾十萬(wàn),這種方式顯然不可取。

3.1.2 bgsave觸發(fā)方式

執(zhí)行該命令時(shí),Redis會(huì)在后臺(tái)異步進(jìn)行快照操作,快照同時(shí)還可以響應(yīng)客戶端請(qǐng)求。具體流程如下:

image.png

具體操作是Redis進(jìn)程執(zhí)行fork操作創(chuàng)建子進(jìn)程,RDB持久化過(guò)程由子進(jìn)程負(fù)責(zé),完成后自動(dòng)結(jié)束。阻塞只發(fā)生在fork階段,一般時(shí)間很短?;旧?Redis 內(nèi)部所有的RDB操作都是采用 bgsave 命令。

3.1.3 自動(dòng)觸發(fā)

自動(dòng)觸發(fā)是由我們的配置文件來(lái)完成的。在redis.conf配置文件中,里面有如下配置,我們可以去設(shè)置:

①save:這里是用來(lái)配置觸發(fā) Redis的 RDB 持久化條件,也就是什么時(shí)候?qū)?nèi)存中的數(shù)據(jù)保存到硬盤(pán)。比如“save m n”。表示m秒內(nèi)數(shù)據(jù)集存在n次修改時(shí),自動(dòng)觸發(fā)bgsave。

默認(rèn)如下配置:

#表示900 秒內(nèi)如果至少有 1 個(gè) key 的值變化,則保存
save 900 1
#表示300 秒內(nèi)如果至少有 10 個(gè) key 的值變化,則保存
save 300 10
#表示60 秒內(nèi)如果至少有 10000 個(gè) key 的值變化,則保存save 60 10000

不需要持久化,那么你可以注釋掉所有的 save 行來(lái)停用保存功能。
②stop-writes-on-bgsave-error :默認(rèn)值為yes。當(dāng)啟用了RDB且最后一次后臺(tái)保存數(shù)據(jù)失敗,Redis是否停止接收數(shù)據(jù)。這會(huì)讓用戶意識(shí)到數(shù)據(jù)沒(méi)有正確持久化到磁盤(pán)上,否則沒(méi)有人會(huì)注意到災(zāi)難(disaster)發(fā)生了。如果Redis重啟了,那么又可以重新開(kāi)始接收數(shù)據(jù)了
③rdbcompression ;默認(rèn)值是yes。對(duì)于存儲(chǔ)到磁盤(pán)中的快照,可以設(shè)置是否進(jìn)行壓縮存儲(chǔ)。
④rdbchecksum :默認(rèn)值是yes。在存儲(chǔ)快照后,我們還可以讓redis使用CRC64算法來(lái)進(jìn)行數(shù)據(jù)校驗(yàn),但是這樣做會(huì)增加大約10%的性能消耗,如果希望獲取到最大的性能提升,可以關(guān)閉此功能。
⑤dbfilename :設(shè)置快照的文件名,默認(rèn)是 dump.rdb
⑥dir:設(shè)置快照文件的存放路徑,這個(gè)配置項(xiàng)一定是個(gè)目錄,而不能是文件名。

我們可以修改這些配置來(lái)實(shí)現(xiàn)我們想要的效果。因?yàn)榈谌N方式是配置的,所以我們對(duì)前兩種進(jìn)行一個(gè)對(duì)比:

image.png

3.1.4 RDB的優(yōu)劣

①優(yōu)勢(shì)
(1)RDB文件緊湊,全量備份,非常適合用于進(jìn)行備份和災(zāi)難恢復(fù)。
(2)生成RDB文件的時(shí)候,redis主進(jìn)程會(huì)fork()一個(gè)子進(jìn)程來(lái)處理所有保存工作,主進(jìn)程不需要進(jìn)行任何磁盤(pán)IO操作。
(3)RDB 在恢復(fù)大數(shù)據(jù)集時(shí)的速度比 AOF 的恢復(fù)速度要快。
②劣勢(shì)
RDB快照是一次全量備份,存儲(chǔ)的是內(nèi)存數(shù)據(jù)的二進(jìn)制序列化形式,存儲(chǔ)上非常緊湊。當(dāng)進(jìn)行快照持久化時(shí),會(huì)開(kāi)啟一個(gè)子進(jìn)程專門(mén)負(fù)責(zé)快照持久化,子進(jìn)程會(huì)擁有父進(jìn)程的內(nèi)存數(shù)據(jù),父進(jìn)程修改內(nèi)存子進(jìn)程不會(huì)反應(yīng)出來(lái),所以在快照持久化期間修改的數(shù)據(jù)不會(huì)被保存,可能丟失數(shù)據(jù)。

3.2 AOF機(jī)制

全量備份總是耗時(shí)的,有時(shí)候我們提供一種更加高效的方式AOF,工作機(jī)制很簡(jiǎn)單,redis會(huì)將每一個(gè)收到的寫(xiě)命令都通過(guò)write函數(shù)追加到文件中。通俗的理解就是日志記錄。

3.2.1 持久化原理

原理如下圖:

image.png

每當(dāng)有一個(gè)寫(xiě)命令過(guò)來(lái)時(shí),就直接保存在我們的AOF文件中。

3.2.2 文件重寫(xiě)原理

AOF的方式也同時(shí)帶來(lái)了另一個(gè)問(wèn)題。持久化文件會(huì)變的越來(lái)越大。為了壓縮aof的持久化文件。redis提供了bgrewriteaof命令。將內(nèi)存中的數(shù)據(jù)以命令的方式保存到臨時(shí)文件中,同時(shí)會(huì)fork出一條新進(jìn)程來(lái)將文件重寫(xiě)。

image.png

重寫(xiě)aof文件的操作,并沒(méi)有讀取舊的aof文件,而是將整個(gè)內(nèi)存中的數(shù)據(jù)庫(kù)內(nèi)容用命令的方式重寫(xiě)了一個(gè)新的aof文件,這點(diǎn)和快照有點(diǎn)類似。

3.2.3 AOF三種觸發(fā)機(jī)制

(1)每修改同步(always):同步持久化,每次發(fā)生數(shù)據(jù)變更會(huì)被立即記錄到磁盤(pán) 性能較差但數(shù)據(jù)完整性比較好;
(2)每秒同步(everysec):異步操作,每秒記錄 如果一秒內(nèi)宕機(jī),有數(shù)據(jù)丟失;
(3)不同(no):從不同步。
[圖片上傳失敗...(image-dabc24-1597293704284)]

3.2.4 AOF優(yōu)劣

①優(yōu)點(diǎn)
(1)AOF可以更好的保護(hù)數(shù)據(jù)不丟失,一般AOF會(huì)每隔1秒,通過(guò)一個(gè)后臺(tái)線程執(zhí)行一次fsync操作,最多丟失1秒鐘的數(shù)據(jù);
(2)AOF日志文件沒(méi)有任何磁盤(pán)尋址的開(kāi)銷(xiāo),寫(xiě)入性能非常高,文件不容易破損;
(3)AOF日志文件即使過(guò)大的時(shí)候,出現(xiàn)后臺(tái)重寫(xiě)操作,也不會(huì)影響客戶端的讀寫(xiě);
(4)AOF日志文件的命令通過(guò)非??勺x的方式進(jìn)行記錄,這個(gè)特性非常適合做災(zāi)難性的誤刪除的緊急恢復(fù)。比如某人不小心用flushall命令清空了所有數(shù)據(jù),只要這個(gè)時(shí)候后臺(tái)rewrite還沒(méi)有發(fā)生,那么就可以立即拷貝AOF文件,將最后一條flushall命令給刪了,然后再將該AOF文件放回去,就可以通過(guò)恢復(fù)機(jī)制,自動(dòng)恢復(fù)所有數(shù)據(jù)。
②劣勢(shì)
(1)對(duì)于同一份數(shù)據(jù)來(lái)說(shuō),AOF日志文件通常比RDB數(shù)據(jù)快照文件更大;
(2)AOF開(kāi)啟后,支持的寫(xiě)QPS會(huì)比RDB支持的寫(xiě)QPS低,因?yàn)锳OF一般會(huì)配置成每秒fsync一次日志文件,當(dāng)然,每秒一次fsync,性能也還是很高的
(3)以前AOF發(fā)生過(guò)bug,就是通過(guò)AOF記錄的日志,進(jìn)行數(shù)據(jù)恢復(fù)的時(shí)候,沒(méi)有恢復(fù)一模一樣的數(shù)據(jù)出來(lái)。

3.3 兩種機(jī)制對(duì)比以及兩種機(jī)制對(duì)過(guò)期key的處理

image.png

3.3.1 RDB對(duì)過(guò)期key的處理

過(guò)期key對(duì)RDB沒(méi)有任何影響。

  • 在從內(nèi)存數(shù)據(jù)庫(kù)持久化數(shù)據(jù)到RDB文件:持久化key之前,會(huì)檢查是否過(guò)期,過(guò)期的key不進(jìn)入RDB文件;

  • 從RDB文件恢復(fù)數(shù)據(jù)到內(nèi)存數(shù)據(jù)庫(kù):數(shù)據(jù)載入數(shù)據(jù)庫(kù)之前,會(huì)對(duì)key先進(jìn)行過(guò)期檢查,如果過(guò)期,不導(dǎo)入數(shù)據(jù)庫(kù)(主庫(kù)情況)。

3.3.2 AOF對(duì)過(guò)期key的處理

過(guò)期key對(duì)AOF也沒(méi)有任何影響。

  • 從內(nèi)存數(shù)據(jù)庫(kù)持久化數(shù)據(jù)到AOF文件:若key過(guò)期后,還沒(méi)有被刪除,此時(shí)進(jìn)行執(zhí)行持久化操作(該key是不會(huì)進(jìn)入aof文件的,因?yàn)闆](méi)有發(fā)生修改命令)。若key過(guò)期后,在發(fā)生刪除操作時(shí),程序會(huì)向aof文件追加一條del命令(在將來(lái)的以aof文件恢復(fù)數(shù)據(jù)的時(shí)候該過(guò)期的鍵就會(huì)被刪掉);

  • AOF重寫(xiě):重寫(xiě)時(shí),會(huì)先判斷key是否過(guò)期,已過(guò)期的key不會(huì)重寫(xiě)到aof文件 。

4. Redis的緩存穿透、緩存擊穿、緩存雪崩及其解決方案

4.1 概念

  • 緩存穿透:key對(duì)應(yīng)的數(shù)據(jù)在數(shù)據(jù)源并不存在,每次針對(duì)此key的請(qǐng)求從緩存獲取不到,請(qǐng)求都會(huì)到數(shù)據(jù)源,從而可能壓垮數(shù)據(jù)源。比如用一個(gè)不存在的用戶id獲取用戶信息,不論緩存還是數(shù)據(jù)庫(kù)都沒(méi)有,若黑客利用此漏洞進(jìn)行攻擊可能壓垮數(shù)據(jù)庫(kù)。

  • 緩存擊穿:key對(duì)應(yīng)的數(shù)據(jù)存在,但在redis中過(guò)期,此時(shí)若有大量并發(fā)請(qǐng)求過(guò)來(lái),這些請(qǐng)求發(fā)現(xiàn)緩存過(guò)期一般都會(huì)從后端DB加載數(shù)據(jù)并回設(shè)到緩存,這個(gè)時(shí)候大并發(fā)的請(qǐng)求可能會(huì)瞬間把后端DB壓垮。

  • 緩存雪崩:當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個(gè)時(shí)間段失效,這樣在失效的時(shí)候,也會(huì)給后端系統(tǒng)(比如DB)帶來(lái)很大壓力

4.2 緩存穿透解決方案

一個(gè)一定不存在緩存及查詢不到的數(shù)據(jù),由于緩存是不命中時(shí)被動(dòng)寫(xiě)的,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫(xiě)入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存的意義。

有很多種方法可以有效地解決緩存穿透問(wèn)題,最常見(jiàn)的則是采用布隆過(guò)濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。另外也有一個(gè)更為簡(jiǎn)單粗暴的方法(我們采用的就是這種),如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存,但它的過(guò)期時(shí)間會(huì)很短,最長(zhǎng)不超過(guò)五分鐘。

4.2.1 布隆過(guò)濾器

布隆過(guò)濾器是一種數(shù)據(jù)結(jié)構(gòu),垃圾網(wǎng)站和正常網(wǎng)站加起來(lái)全世界據(jù)統(tǒng)計(jì)也有幾十億個(gè)。網(wǎng)警要過(guò)濾這些垃圾網(wǎng)站,總不能到數(shù)據(jù)庫(kù)里面一個(gè)一個(gè)去比較吧,這就可以使用布隆過(guò)濾器。假設(shè)我們存儲(chǔ)一億個(gè)垃圾網(wǎng)站地址。

可以先有一億個(gè)二進(jìn)制比特,然后網(wǎng)警用八個(gè)不同的隨機(jī)數(shù)產(chǎn)生器(F1,F2, …,F8) 產(chǎn)生八個(gè)信息指紋(f1, f2, …, f8)。接下來(lái)用一個(gè)隨機(jī)數(shù)產(chǎn)生器 G 把這八個(gè)信息指紋映射到 1 到1億中的八個(gè)自然數(shù) g1, g2, …,g8。最后把這八個(gè)位置的二進(jìn)制全部設(shè)置為一。過(guò)程如下:

image.png

有一天網(wǎng)警查到了一個(gè)可疑的網(wǎng)站,想判斷一下是否是XX網(wǎng)站,首先將可疑網(wǎng)站通過(guò)哈希映射到1億個(gè)比特?cái)?shù)組上的8個(gè)點(diǎn)。如果8個(gè)點(diǎn)的其中有一個(gè)點(diǎn)不為1,則可以判斷該元素一定不存在集合中。

那這個(gè)布隆過(guò)濾器是如何解決redis中的緩存穿透呢?很簡(jiǎn)單首先也是對(duì)所有可能查詢的參數(shù)以hash形式存儲(chǔ),當(dāng)用戶想要查詢的時(shí)候,使用布隆過(guò)濾器發(fā)現(xiàn)不在集合中,就直接丟棄,不再對(duì)持久層查詢。

image.png

4.2.2 緩存空對(duì)象

當(dāng)存儲(chǔ)層不命中后,即使返回的空對(duì)象也將其緩存起來(lái),同時(shí)會(huì)設(shè)置一個(gè)過(guò)期時(shí)間,之后再訪問(wèn)這個(gè)數(shù)據(jù)將會(huì)從緩存中獲取,保護(hù)了后端數(shù)據(jù)源;

image.png

但是這種方法會(huì)存在兩個(gè)問(wèn)題:

  • 如果空值能夠被緩存起來(lái),這就意味著緩存需要更多的空間存儲(chǔ)更多的鍵,因?yàn)檫@當(dāng)中可能會(huì)有很多的空值的鍵;

  • 即使對(duì)空值設(shè)置了過(guò)期時(shí)間,還是會(huì)存在緩存層和存儲(chǔ)層的數(shù)據(jù)會(huì)有一段時(shí)間窗口的不一致,這對(duì)于需要保持一致性的業(yè)務(wù)會(huì)有影響。

4.3 緩存擊穿解決方案

key可能會(huì)在某些時(shí)間點(diǎn)被超高并發(fā)地訪問(wèn),是一種非?!盁狳c(diǎn)”的數(shù)據(jù)。這個(gè)時(shí)候,需要考慮一個(gè)問(wèn)題:緩存被“擊穿”的問(wèn)題。

使用互斥鎖(mutex key)

業(yè)界比較常用的做法,是使用mutex。簡(jiǎn)單地來(lái)說(shuō),就是在緩存失效的時(shí)候(判斷拿出來(lái)的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者M(jìn)emcache的ADD)去set一個(gè)mutex key,當(dāng)操作返回成功時(shí),再進(jìn)行l(wèi)oad db的操作并回設(shè)緩存;否則,就重試整個(gè)get緩存的方法。

4.4 緩存雪崩解決方案

與緩存擊穿的區(qū)別在于這里針對(duì)很多key緩存,前者則是某一個(gè)key。緩存雪崩原因:

  • 緩存層出現(xiàn)了錯(cuò)誤,不能正常工作了。于是所有的請(qǐng)求都會(huì)達(dá)到存儲(chǔ)層,存儲(chǔ)層的調(diào)用量會(huì)暴增,造成存儲(chǔ)層也會(huì)掛掉的情況;

  • 大量緩存集中在某一個(gè)時(shí)間段失效。

這兩種情況都會(huì)造成緩存雪崩。其解決方案主要有以下幾種。我們選用哪種來(lái)解決需要我們針對(duì)我們具體的業(yè)務(wù)系統(tǒng),具體分析,選擇最合適的一種來(lái)使用。

4.4.1 redis高可用

部署redis集群,避免單機(jī)掛掉的風(fēng)險(xiǎn)。

4.4.2 限流降級(jí)

這個(gè)解決方案的思想是,在緩存失效后,通過(guò)加鎖或者隊(duì)列來(lái)控制讀數(shù)據(jù)庫(kù)寫(xiě)緩存的線程數(shù)量。比如對(duì)某個(gè)key只允許一個(gè)線程查詢數(shù)據(jù)和寫(xiě)緩存,其他線程等待。某一條線程寫(xiě)緩存成功后,其余線程則可以直接在緩存中查詢到數(shù)據(jù)。

  • 這種思路減輕了數(shù)據(jù)庫(kù)的壓力,避免了數(shù)據(jù)源的崩潰,但是在高并發(fā)下,緩存重建期間key是鎖著的,這是過(guò)來(lái)1000個(gè)請(qǐng)求999個(gè)都在阻塞的。同樣會(huì)導(dǎo)致用戶等待超時(shí),這是個(gè)治標(biāo)不治本的方法;

  • 加鎖排隊(duì)的解決方式分布式環(huán)境的并發(fā)問(wèn)題,有可能還要解決分布式鎖的問(wèn)題;線程還會(huì)被阻塞,用戶體驗(yàn)很差!因此,在真正的高并發(fā)場(chǎng)景下很少使用。

4.4.3 緩存標(biāo)記

偽代碼如下:

//偽代碼
public object GetProductListNew() {
 int cacheTime = 30;
 String cacheKey = "product_list";
 //緩存標(biāo)記
 String cacheSign = cacheKey + "_sign";
?
 String sign = CacheHelper.Get(cacheSign);
 //獲取緩存值
 String cacheValue = CacheHelper.Get(cacheKey);
 if (sign != null) {
 return cacheValue; //未過(guò)期,直接返回
 } else {
 CacheHelper.Add(cacheSign, "1", cacheTime);
 ThreadPool.QueueUserWorkItem((arg) -> {
 //這里一般是 sql查詢數(shù)據(jù)
 cacheValue = GetProductListFromDB(); 
 //日期設(shè)緩存時(shí)間的2倍,用于臟讀
 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); 
 });
 return cacheValue;
 }
} 

說(shuō)明:利用緩存標(biāo)記比實(shí)際緩存數(shù)據(jù)失效快,去提前更新緩存的方式去解決緩存雪崩。但這樣也需要緩存的key為原來(lái)的兩倍,即每個(gè)緩存都有緩存本身以及緩存標(biāo)記

  • 緩存標(biāo)記:記錄緩存數(shù)據(jù)是否過(guò)期,如果過(guò)期會(huì)觸發(fā)通知另外的線程在后臺(tái)去更新實(shí)際key的緩存;

  • 緩存數(shù)據(jù):它的過(guò)期時(shí)間比緩存標(biāo)記的時(shí)間延長(zhǎng)1倍,例:標(biāo)記緩存時(shí)間30分鐘,數(shù)據(jù)緩存設(shè)置為60分鐘。這樣,當(dāng)緩存標(biāo)記key過(guò)期后,實(shí)際緩存還能把舊數(shù)據(jù)返回給調(diào)用端,直到另外的線程在后臺(tái)更新完成后,才會(huì)返回新緩存。

4.4.4 為key設(shè)置不同的緩存失效時(shí)間

將緩存失效時(shí)間分散開(kāi),比如我們可以在原有的失效時(shí)間基礎(chǔ)上增加一個(gè)隨機(jī)值,比如1-5分鐘隨機(jī),這樣每一個(gè)緩存的過(guò)期時(shí)間的重復(fù)率就會(huì)降低,就很難引發(fā)集體失效的事件。

4.4.5 數(shù)據(jù)預(yù)熱

數(shù)據(jù)加熱的含義就是在高流量點(diǎn)到達(dá)之前,我先把可能的數(shù)據(jù)先預(yù)先訪問(wèn)一遍,這樣部分可能大量訪問(wèn)的數(shù)據(jù)就會(huì)加載到緩存中。在即將發(fā)生大并發(fā)訪問(wèn)前手動(dòng)觸發(fā)加載緩存不同的key,設(shè)置不同的過(guò)期時(shí)間,讓緩存失效的時(shí)間點(diǎn)盡量均勻。

5.總結(jié)

以上針對(duì)Redis的基本數(shù)據(jù)結(jié)構(gòu)及其應(yīng)用場(chǎng)景、過(guò)期策略、持久策略以及存在的問(wèn)題進(jìn)行了詳細(xì)介紹與講解,由于內(nèi)容過(guò)多,若有錯(cuò)誤之處望指出。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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