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可以。

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è)成員)。

應(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ò)程完成為止。具體流程如下:

執(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)求。具體流程如下:

具體操作是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ì)比:

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 持久化原理
原理如下圖:

每當(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ě)。

重寫(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的處理

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ò)程如下:

有一天網(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ì)持久層查詢。

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ù)源;

但是這種方法會(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ò)誤之處望指出。