2W字!詳解20道Redis經(jīng)典面試題!(珍藏版)
1. 什么是Redis?它主要用來什么的?
Redis,英文全稱是Remote Dictionary Server(遠(yuǎn)程字典服務(wù)),是一個(gè)開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,并提供多種語言的API。
與MySQL數(shù)據(jù)庫不同的是,Redis的數(shù)據(jù)是存在內(nèi)存中的。它的讀寫速度非常快,每秒可以處理超過10萬次讀寫操作。因此redis被廣泛應(yīng)用于緩存,另外,Redis也經(jīng)常用來做分布式鎖。除此之外,Redis支持事務(wù)、持久化、LUA 腳本、LRU 驅(qū)動(dòng)事件、多種集群方案。
2.說說Redis的基本數(shù)據(jù)結(jié)構(gòu)類型
大多數(shù)小伙伴都知道,Redis有以下這五種基本類型:
String(字符串) Hash(哈希) List(列表) Set(集合) zset(有序集合)
它還有三種特殊的數(shù)據(jù)結(jié)構(gòu)類型
Geospatial Hyperloglog Bitmap
2.1 Redis 的五種基本數(shù)據(jù)類型

String(字符串)
簡介:String是Redis最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)類型,它是二進(jìn)制安全的,可以存儲(chǔ)圖片或者序列化的對(duì)象,值最大存儲(chǔ)為512M 簡單使用舉例: set key value、get key等應(yīng)用場景:共享session、分布式鎖,計(jì)數(shù)器、限流。 內(nèi)部編碼有3種, int(8字節(jié)長整型)/embstr(小于等于39字節(jié)字符串)/raw(大于39個(gè)字節(jié)字符串)
C語言的字符串是char[]實(shí)現(xiàn)的,而Redis使用SDS(simple dynamic string) 封裝,sds源碼如下:
struct?sdshdr{
??unsigned?int?len;?//?標(biāo)記buf的長度
??unsigned?int?free;?//標(biāo)記buf中未使用的元素個(gè)數(shù)
??char?buf[];?//?存放元素的坑
}
SDS 結(jié)構(gòu)圖如下:
Redis為什么選擇SDS結(jié)構(gòu),而C語言原生的char[]不香嗎?
舉例其中一點(diǎn),SDS中,O(1)時(shí)間復(fù)雜度,就可以獲取字符串長度;而C 字符串,需要遍歷整個(gè)字符串,時(shí)間復(fù)雜度為O(n)
Hash(哈希)
簡介:在Redis中,哈希類型是指v(值)本身又是一個(gè)鍵值對(duì)(k-v)結(jié)構(gòu) 簡單使用舉例: hset key field value、hget key field內(nèi)部編碼: ziplist(壓縮列表)、hashtable(哈希表)應(yīng)用場景:緩存用戶信息等。 注意點(diǎn):如果開發(fā)使用hgetall,哈希元素比較多的話,可能導(dǎo)致Redis阻塞,可以使用hscan。而如果只是獲取部分field,建議使用hmget。
字符串和哈希類型對(duì)比如下圖:
List(列表)
簡介:列表(list)類型是用來存儲(chǔ)多個(gè)有序的字符串,一個(gè)列表最多可以存儲(chǔ)2^32-1個(gè)元素。 簡單實(shí)用舉例: lpush key value [value ...]、lrange key start end內(nèi)部編碼:ziplist(壓縮列表)、linkedlist(鏈表) 應(yīng)用場景:消息隊(duì)列,文章列表,
一圖看懂list類型的插入與彈出:
list應(yīng)用場景參考以下:
lpush+lpop=Stack(棧) lpush+rpop=Queue(隊(duì)列) lpsh+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息隊(duì)列)
Set(集合)

簡介:集合(set)類型也是用來保存多個(gè)的字符串元素,但是不允許重復(fù)元素 簡單使用舉例: sadd key element [element ...]、smembers key內(nèi)部編碼: intset(整數(shù)集合)、hashtable(哈希表)注意點(diǎn):smembers和lrange、hgetall都屬于比較重的命令,如果元素過多存在阻塞Redis的可能性,可以使用sscan來完成。 應(yīng)用場景:用戶標(biāo)簽,生成隨機(jī)數(shù)抽獎(jiǎng)、社交需求。
有序集合(zset)
簡介:已排序的字符串集合,同時(shí)元素不能重復(fù) 簡單格式舉例: zadd key score member [score member ...],zrank key member底層內(nèi)部編碼: ziplist(壓縮列表)、skiplist(跳躍表)應(yīng)用場景:排行榜,社交需求(如用戶點(diǎn)贊)。
2.2 Redis 的三種特殊數(shù)據(jù)類型
Geo:Redis3.2推出的,地理位置定位,用于存儲(chǔ)地理位置信息,并對(duì)存儲(chǔ)的信息進(jìn)行操作。 HyperLogLog:用來做基數(shù)統(tǒng)計(jì)算法的數(shù)據(jù)結(jié)構(gòu),如統(tǒng)計(jì)網(wǎng)站的UV。 Bitmaps :用一個(gè)比特位來映射某個(gè)元素的狀態(tài),在Redis中,它的底層是基于字符串類型實(shí)現(xiàn)的,可以把bitmaps成作一個(gè)以比特位為單位的數(shù)組
3. Redis為什么這么快?

3.1 基于內(nèi)存存儲(chǔ)實(shí)現(xiàn)
我們都知道內(nèi)存讀寫是比在磁盤快很多的,Redis基于內(nèi)存存儲(chǔ)實(shí)現(xiàn)的數(shù)據(jù)庫,相對(duì)于數(shù)據(jù)存在磁盤的MySQL數(shù)據(jù)庫,省去磁盤I/O的消耗。
3.2 高效的數(shù)據(jù)結(jié)構(gòu)
我們知道,Mysql索引為了提高效率,選擇了B+樹的數(shù)據(jù)結(jié)構(gòu)。其實(shí)合理的數(shù)據(jù)結(jié)構(gòu),就是可以讓你的應(yīng)用/程序更快。先看下Redis的數(shù)據(jù)結(jié)構(gòu)&內(nèi)部編碼圖:

SDS簡單動(dòng)態(tài)字符串

字符串長度處理:Redis獲取字符串長度,時(shí)間復(fù)雜度為O(1),而C語言中,需要從頭開始遍歷,復(fù)雜度為O(n); 空間預(yù)分配:字符串修改越頻繁的話,內(nèi)存分配越頻繁,就會(huì)消耗性能,而SDS修改和空間擴(kuò)充,會(huì)額外分配未使用的空間,減少性能損耗。 惰性空間釋放:SDS 縮短時(shí),不是回收多余的內(nèi)存空間,而是free記錄下多余的空間,后續(xù)有變更,直接使用free中記錄的空間,減少分配。 二進(jìn)制安全:Redis可以存儲(chǔ)一些二進(jìn)制數(shù)據(jù),在C語言中字符串遇到'\0'會(huì)結(jié)束,而 SDS中標(biāo)志字符串結(jié)束的是len屬性。
字典
Redis 作為 K-V 型內(nèi)存數(shù)據(jù)庫,所有的鍵值就是用字典來存儲(chǔ)。字典就是哈希表,比如HashMap,通過key就可以直接獲取到對(duì)應(yīng)的value。而哈希表的特性,在O(1)時(shí)間復(fù)雜度就可以獲得對(duì)應(yīng)的值。
跳躍表

跳躍表是Redis特有的數(shù)據(jù)結(jié)構(gòu),就是在鏈表的基礎(chǔ)上,增加多級(jí)索引提升查找效率。 跳躍表支持平均 O(logN),最壞 O(N)復(fù)雜度的節(jié)點(diǎn)查找,還可以通過順序性操作批量處理節(jié)點(diǎn)。
3.3 合理的數(shù)據(jù)編碼
Redis 支持多種數(shù)據(jù)數(shù)據(jù)類型,每種基本類型,可能對(duì)多種數(shù)據(jù)結(jié)構(gòu)。什么時(shí)候,使用什么樣數(shù)據(jù)結(jié)構(gòu),使用什么樣編碼,是redis設(shè)計(jì)者總結(jié)優(yōu)化的結(jié)果。
String:如果存儲(chǔ)數(shù)字的話,是用int類型的編碼;如果存儲(chǔ)非數(shù)字,小于等于39字節(jié)的字符串,是embstr;大于39個(gè)字節(jié),則是raw編碼。 List:如果列表的元素個(gè)數(shù)小于512個(gè),列表每個(gè)元素的值都小于64字節(jié)(默認(rèn)),使用ziplist編碼,否則使用linkedlist編碼 Hash:哈希類型元素個(gè)數(shù)小于512個(gè),所有值小于64字節(jié)的話,使用ziplist編碼,否則使用hashtable編碼。 Set:如果集合中的元素都是整數(shù)且元素個(gè)數(shù)小于512個(gè),使用intset編碼,否則使用hashtable編碼。 Zset:當(dāng)有序集合的元素個(gè)數(shù)小于128個(gè),每個(gè)元素的值小于64字節(jié)時(shí),使用ziplist編碼,否則使用skiplist(跳躍表)編碼
3.4 合理的線程模型
I/O 多路復(fù)用

多路I/O復(fù)用技術(shù)可以讓單個(gè)線程高效的處理多個(gè)連接請(qǐng)求,而Redis使用用epoll作為I/O多路復(fù)用技術(shù)的實(shí)現(xiàn)。并且,Redis自身的事件處理模型將epoll中的連接、讀寫、關(guān)閉都轉(zhuǎn)換為事件,不在網(wǎng)絡(luò)I/O上浪費(fèi)過多的時(shí)間。
什么是I/O多路復(fù)用?
I/O :網(wǎng)絡(luò) I/O 多路 :多個(gè)網(wǎng)絡(luò)連接 復(fù)用:復(fù)用同一個(gè)線程。 IO多路復(fù)用其實(shí)就是一種同步IO模型,它實(shí)現(xiàn)了一個(gè)線程可以監(jiān)視多個(gè)文件句柄;一旦某個(gè)文件句柄就緒,就能夠通知應(yīng)用程序進(jìn)行相應(yīng)的讀寫操作;而沒有文件句柄就緒時(shí),就會(huì)阻塞應(yīng)用程序,交出cpu。
單線程模型
Redis是單線程模型的,而單線程避免了CPU不必要的上下文切換和競爭鎖的消耗。也正因?yàn)槭菃尉€程,如果某個(gè)命令執(zhí)行過長(如hgetall命令),會(huì)造成阻塞。Redis是面向快速執(zhí)行場景的數(shù)據(jù)庫。,所以要慎用如smembers和lrange、hgetall等命令。 Redis 6.0 引入了多線程提速,它的執(zhí)行命令操作內(nèi)存的仍然是個(gè)單線程。
3.5 虛擬內(nèi)存機(jī)制
Redis直接自己構(gòu)建了VM機(jī)制 ,不會(huì)像一般的系統(tǒng)會(huì)調(diào)用系統(tǒng)函數(shù)處理,會(huì)浪費(fèi)一定的時(shí)間去移動(dòng)和請(qǐng)求。
Redis的虛擬內(nèi)存機(jī)制是啥呢?
虛擬內(nèi)存機(jī)制就是暫時(shí)把不經(jīng)常訪問的數(shù)據(jù)(冷數(shù)據(jù))從內(nèi)存交換到磁盤中,從而騰出寶貴的內(nèi)存空間用于其它需要訪問的數(shù)據(jù)(熱數(shù)據(jù))。通過VM功能可以實(shí)現(xiàn)冷熱數(shù)據(jù)分離,使熱數(shù)據(jù)仍在內(nèi)存中、冷數(shù)據(jù)保存到磁盤。這樣就可以避免因?yàn)閮?nèi)存不足而造成訪問速度下降的問題。
4. 什么是緩存擊穿、緩存穿透、緩存雪崩?
4.1 緩存穿透問題
先來看一個(gè)常見的緩存使用方式:讀請(qǐng)求來了,先查下緩存,緩存有值命中,就直接返回;緩存沒命中,就去查數(shù)據(jù)庫,然后把數(shù)據(jù)庫的值更新到緩存,再返回。

緩存穿透:指查詢一個(gè)一定不存在的數(shù)據(jù),由于緩存是不命中時(shí)需要從數(shù)據(jù)庫查詢,查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫去查詢,進(jìn)而給數(shù)據(jù)庫帶來壓力。
通俗點(diǎn)說,讀請(qǐng)求訪問時(shí),緩存和數(shù)據(jù)庫都沒有某個(gè)值,這樣就會(huì)導(dǎo)致每次對(duì)這個(gè)值的查詢請(qǐng)求都會(huì)穿透到數(shù)據(jù)庫,這就是緩存穿透。
緩存穿透一般都是這幾種情況產(chǎn)生的:
業(yè)務(wù)不合理的設(shè)計(jì),比如大多數(shù)用戶都沒開守護(hù),但是你的每個(gè)請(qǐng)求都去緩存,查詢某個(gè)userid查詢有沒有守護(hù)。 業(yè)務(wù)/運(yùn)維/開發(fā)失誤的操作,比如緩存和數(shù)據(jù)庫的數(shù)據(jù)都被誤刪除了。 黑客非法請(qǐng)求攻擊,比如黑客故意捏造大量非法請(qǐng)求,以讀取不存在的業(yè)務(wù)數(shù)據(jù)。
如何避免緩存穿透呢? 一般有三種方法。
1.如果是非法請(qǐng)求,我們?cè)贏PI入口,對(duì)參數(shù)進(jìn)行校驗(yàn),過濾非法值。 2.如果查詢數(shù)據(jù)庫為空,我們可以給緩存設(shè)置個(gè)空值,或者默認(rèn)值。但是如有有寫請(qǐng)求進(jìn)來的話,需要更新緩存哈,以保證緩存一致性,同時(shí),最后給緩存設(shè)置適當(dāng)?shù)倪^期時(shí)間。(業(yè)務(wù)上比較常用,簡單有效) 3.使用布隆過濾器快速判斷數(shù)據(jù)是否存在。即一個(gè)查詢請(qǐng)求過來時(shí),先通過布隆過濾器判斷值是否存在,存在才繼續(xù)往下查。
布隆過濾器原理:它由初始值為0的位圖數(shù)組和N個(gè)哈希函數(shù)組成。一個(gè)對(duì)一個(gè)key進(jìn)行N個(gè)hash算法獲取N個(gè)值,在比特?cái)?shù)組中將這N個(gè)值散列后設(shè)定為1,然后查的時(shí)候如果特定的這幾個(gè)位置都為1,那么布隆過濾器判斷該key存在。
4.2 緩存雪奔問題
緩存雪奔: 指緩存中數(shù)據(jù)大批量到過期時(shí)間,而查詢數(shù)據(jù)量巨大,請(qǐng)求都直接訪問數(shù)據(jù)庫,引起數(shù)據(jù)庫壓力過大甚至down機(jī)。
緩存雪奔一般是由于大量數(shù)據(jù)同時(shí)過期造成的,對(duì)于這個(gè)原因,可通過均勻設(shè)置過期時(shí)間解決,即讓過期時(shí)間相對(duì)離散一點(diǎn)。如采用一個(gè)較大固定值+一個(gè)較小的隨機(jī)值,5小時(shí)+0到1800秒醬紫。 Redis 故障宕機(jī)也可能引起緩存雪奔。這就需要構(gòu)造Redis高可用集群啦。
4.3 緩存擊穿問題
緩存擊穿: 指熱點(diǎn)key在某個(gè)時(shí)間點(diǎn)過期的時(shí)候,而恰好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)Key有大量的并發(fā)請(qǐng)求過來,從而大量的請(qǐng)求打到db。
緩存擊穿看著有點(diǎn)像,其實(shí)它兩區(qū)別是,緩存雪奔是指數(shù)據(jù)庫壓力過大甚至down機(jī),緩存擊穿只是大量并發(fā)請(qǐng)求到了DB數(shù)據(jù)庫層面。可以認(rèn)為擊穿是緩存雪奔的一個(gè)子集吧。有些文章認(rèn)為它倆區(qū)別,是區(qū)別在于擊穿針對(duì)某一熱點(diǎn)key緩存,雪奔則是很多key。
解決方案就有兩種:
1.使用互斥鎖方案。緩存失效時(shí),不是立即去加載db數(shù)據(jù),而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時(shí)候,再去加載db數(shù)據(jù)庫數(shù)據(jù)和設(shè)置緩存。否則就去重試獲取緩存。 2. “永不過期”,是指沒有設(shè)置過期時(shí)間,但是熱點(diǎn)數(shù)據(jù)快要過期時(shí),異步線程去更新和設(shè)置過期時(shí)間。
5. 什么是熱Key問題,如何解決熱key問題
什么是熱Key呢?在Redis中,我們把訪問頻率高的key,稱為熱點(diǎn)key。
如果某一熱點(diǎn)key的請(qǐng)求到服務(wù)器主機(jī)時(shí),由于請(qǐng)求量特別大,可能會(huì)導(dǎo)致主機(jī)資源不足,甚至宕機(jī),從而影響正常的服務(wù)。

而熱點(diǎn)Key是怎么產(chǎn)生的呢?主要原因有兩個(gè):
用戶消費(fèi)的數(shù)據(jù)遠(yuǎn)大于生產(chǎn)的數(shù)據(jù),如秒殺、熱點(diǎn)新聞等讀多寫少的場景。 請(qǐng)求分片集中,超過單Redi服務(wù)器的性能,比如固定名稱key,Hash落入同一臺(tái)服務(wù)器,瞬間訪問量極大,超過機(jī)器瓶頸,產(chǎn)生熱點(diǎn)Key問題。
那么在日常開發(fā)中,如何識(shí)別到熱點(diǎn)key呢?
憑經(jīng)驗(yàn)判斷哪些是熱Key; 客戶端統(tǒng)計(jì)上報(bào); 服務(wù)代理層上報(bào)
如何解決熱key問題?
Redis集群擴(kuò)容:增加分片副本,均衡讀流量; 將熱key分散到不同的服務(wù)器中; 使用二級(jí)緩存,即JVM本地緩存,減少Redis的讀請(qǐng)求。
6. Redis 過期策略和內(nèi)存淘汰策略

6.1 Redis的過期策略
我們?cè)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">set key的時(shí)候,可以給它設(shè)置一個(gè)過期時(shí)間,比如expire key 60。指定這key60s后過期,60s后,redis是如何處理的嘛?我們先來介紹幾種過期策略:
定時(shí)過期
每個(gè)設(shè)置過期時(shí)間的key都需要?jiǎng)?chuàng)建一個(gè)定時(shí)器,到過期時(shí)間就會(huì)立即對(duì)key進(jìn)行清除。該策略可以立即清除過期的數(shù)據(jù),對(duì)內(nèi)存很友好;但是會(huì)占用大量的CPU資源去處理過期的數(shù)據(jù),從而影響緩存的響應(yīng)時(shí)間和吞吐量。
惰性過期
只有當(dāng)訪問一個(gè)key時(shí),才會(huì)判斷該key是否已過期,過期則清除。該策略可以最大化地節(jié)省CPU資源,卻對(duì)內(nèi)存非常不友好。極端情況可能出現(xiàn)大量的過期key沒有再次被訪問,從而不會(huì)被清除,占用大量內(nèi)存。
定期過期
每隔一定的時(shí)間,會(huì)掃描一定數(shù)量的數(shù)據(jù)庫的expires字典中一定數(shù)量的key,并清除其中已過期的key。該策略是前兩者的一個(gè)折中方案。通過調(diào)整定時(shí)掃描的時(shí)間間隔和每次掃描的限定耗時(shí),可以在不同情況下使得CPU和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。
expires字典會(huì)保存所有設(shè)置了過期時(shí)間的key的過期時(shí)間數(shù)據(jù),其中,key是指向鍵空間中的某個(gè)鍵的指針,value是該鍵的毫秒精度的UNIX時(shí)間戳表示的過期時(shí)間。鍵空間是指該Redis集群中保存的所有鍵。
Redis中同時(shí)使用了惰性過期和定期過期兩種過期策略。
假設(shè)Redis當(dāng)前存放30萬個(gè)key,并且都設(shè)置了過期時(shí)間,如果你每隔100ms就去檢查這全部的key,CPU負(fù)載會(huì)特別高,最后可能會(huì)掛掉。 因此,redis采取的是定期過期,每隔100ms就隨機(jī)抽取一定數(shù)量的key來檢查和刪除的。 但是呢,最后可能會(huì)有很多已經(jīng)過期的key沒被刪除。這時(shí)候,redis采用惰性刪除。在你獲取某個(gè)key的時(shí)候,redis會(huì)檢查一下,這個(gè)key如果設(shè)置了過期時(shí)間并且已經(jīng)過期了,此時(shí)就會(huì)刪除。
但是呀,如果定期刪除漏掉了很多過期的key,然后也沒走惰性刪除。就會(huì)有很多過期key積在內(nèi)存內(nèi)存,直接會(huì)導(dǎo)致內(nèi)存爆的。或者有些時(shí)候,業(yè)務(wù)量大起來了,redis的key被大量使用,內(nèi)存直接不夠了,運(yùn)維小哥哥也忘記加大內(nèi)存了。難道redis直接這樣掛掉?不會(huì)的!Redis用8種內(nèi)存淘汰策略保護(hù)自己~
6.2 Redis 內(nèi)存淘汰策略
volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),從設(shè)置了過期時(shí)間的key中使用LRU(最近最少使用)算法進(jìn)行淘汰; allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),從所有key中使用LRU(最近最少使用)算法進(jìn)行淘汰。 volatile-lfu:4.0版本新增,當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在過期的key中,使用LFU算法進(jìn)行刪除key。 allkeys-lfu:4.0版本新增,當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),從所有key中使用LFU算法進(jìn)行淘汰; volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),從設(shè)置了過期時(shí)間的key中,隨機(jī)淘汰數(shù)據(jù);。 allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),從所有key中隨機(jī)淘汰數(shù)據(jù)。 volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的key中,根據(jù)過期時(shí)間進(jìn)行淘汰,越早過期的優(yōu)先被淘汰; noeviction:默認(rèn)策略,當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),新寫入操作會(huì)報(bào)錯(cuò)。
7.說說Redis的常用應(yīng)用場景
緩存 排行榜 計(jì)數(shù)器應(yīng)用 共享Session 分布式鎖 社交網(wǎng)絡(luò) 消息隊(duì)列 位操作
7.1 緩存
我們一提到redis,自然而然就想到緩存,國內(nèi)外中大型的網(wǎng)站都離不開緩存。合理的利用緩存,比如緩存熱點(diǎn)數(shù)據(jù),不僅可以提升網(wǎng)站的訪問速度,還可以降低數(shù)據(jù)庫DB的壓力。并且,Redis相比于memcached,還提供了豐富的數(shù)據(jù)結(jié)構(gòu),并且提供RDB和AOF等持久化機(jī)制,強(qiáng)的一批。
7.2 排行榜
當(dāng)今互聯(lián)網(wǎng)應(yīng)用,有各種各樣的排行榜,如電商網(wǎng)站的月度銷量排行榜、社交APP的禮物排行榜、小程序的投票排行榜等等。Redis提供的zset數(shù)據(jù)類型能夠?qū)崿F(xiàn)這些復(fù)雜的排行榜。
比如,用戶每天上傳視頻,獲得點(diǎn)贊的排行榜可以這樣設(shè)計(jì):
1.用戶Jay上傳一個(gè)視頻,獲得6個(gè)贊,可以醬紫:
zadd?user:ranking:2021-03-03?Jay?3
2.過了一段時(shí)間,再獲得一個(gè)贊,可以這樣:
zincrby?user:ranking:2021-03-03?Jay?1
3.如果某個(gè)用戶John作弊,需要?jiǎng)h除該用戶:
zrem?user:ranking:2021-03-03?John
4.展示獲取贊數(shù)最多的3個(gè)用戶
zrevrangebyrank?user:ranking:2021-03-03?0?2
7.3 計(jì)數(shù)器應(yīng)用
各大網(wǎng)站、APP應(yīng)用經(jīng)常需要計(jì)數(shù)器的功能,如短視頻的播放數(shù)、電商網(wǎng)站的瀏覽數(shù)。這些播放數(shù)、瀏覽數(shù)一般要求實(shí)時(shí)的,每一次播放和瀏覽都要做加1的操作,如果并發(fā)量很大對(duì)于傳統(tǒng)關(guān)系型數(shù)據(jù)的性能是一種挑戰(zhàn)。Redis天然支持計(jì)數(shù)功能而且計(jì)數(shù)的性能也非常好,可以說是計(jì)數(shù)器系統(tǒng)的重要選擇。
7.4 共享Session
如果一個(gè)分布式Web服務(wù)將用戶的Session信息保存在各自服務(wù)器,用戶刷新一次可能就需要重新登錄了,這樣顯然有問題。實(shí)際上,可以使用Redis將用戶的Session進(jìn)行集中管理,每次用戶更新或者查詢登錄信息都直接從Redis中集中獲取。
7.5 分布式鎖
幾乎每個(gè)互聯(lián)網(wǎng)公司中都使用了分布式部署,分布式服務(wù)下,就會(huì)遇到對(duì)同一個(gè)資源的并發(fā)訪問的技術(shù)難題,如秒殺、下單減庫存等場景。
用synchronize或者reentrantlock本地鎖肯定是不行的。 如果是并發(fā)量不大話,使用數(shù)據(jù)庫的悲觀鎖、樂觀鎖來實(shí)現(xiàn)沒啥問題。 但是在并發(fā)量高的場合中,利用數(shù)據(jù)庫鎖來控制資源的并發(fā)訪問,會(huì)影響數(shù)據(jù)庫的性能。 實(shí)際上,可以用Redis的setnx來實(shí)現(xiàn)分布式的鎖。
7.6 社交網(wǎng)絡(luò)
贊/踩、粉絲、共同好友/喜好、推送、下拉刷新等是社交網(wǎng)站的必備功能,由于社交網(wǎng)站訪問量通常比較大,而且傳統(tǒng)的關(guān)系型數(shù)據(jù)不太適保存 這種類型的數(shù)據(jù),Redis提供的數(shù)據(jù)結(jié)構(gòu)可以相對(duì)比較容易地實(shí)現(xiàn)這些功能。
7.7 消息隊(duì)列
消息隊(duì)列是大型網(wǎng)站必用中間件,如ActiveMQ、RabbitMQ、Kafka等流行的消息隊(duì)列中間件,主要用于業(yè)務(wù)解耦、流量削峰及異步處理實(shí)時(shí)性低的業(yè)務(wù)。Redis提供了發(fā)布/訂閱及阻塞隊(duì)列功能,能實(shí)現(xiàn)一個(gè)簡單的消息隊(duì)列系統(tǒng)。另外,這個(gè)不能和專業(yè)的消息中間件相比。
7.8 位操作
用于數(shù)據(jù)量上億的場景下,例如幾億用戶系統(tǒng)的簽到,去重登錄次數(shù)統(tǒng)計(jì),某用戶是否在線狀態(tài)等等。騰訊10億用戶,要幾個(gè)毫秒內(nèi)查詢到某個(gè)用戶是否在線,能怎么做?千萬別說給每個(gè)用戶建立一個(gè)key,然后挨個(gè)記(你可以算一下需要的內(nèi)存會(huì)很恐怖,而且這種類似的需求很多。這里要用到位操作——使用setbit、getbit、bitcount命令。原理是:redis內(nèi)構(gòu)建一個(gè)足夠長的數(shù)組,每個(gè)數(shù)組元素只能是0和1兩個(gè)值,然后這個(gè)數(shù)組的下標(biāo)index用來表示用戶id(必須是數(shù)字哈),那么很顯然,這個(gè)幾億長的大數(shù)組就能通過下標(biāo)和元素值(0和1)來構(gòu)建一個(gè)記憶系統(tǒng)。
8. Redis 的持久化機(jī)制有哪些?優(yōu)缺點(diǎn)說說
Redis是基于內(nèi)存的非關(guān)系型K-V數(shù)據(jù)庫,既然它是基于內(nèi)存的,如果Redis服務(wù)器掛了,數(shù)據(jù)就會(huì)丟失。為了避免數(shù)據(jù)丟失了,Redis提供了持久化,即把數(shù)據(jù)保存到磁盤。
Redis提供了RDB和AOF兩種持久化機(jī)制,它持久化文件加載流程如下:

8.1 RDB
RDB,就是把內(nèi)存數(shù)據(jù)以快照的形式保存到磁盤上。
什么是快照?可以這樣理解,給當(dāng)前時(shí)刻的數(shù)據(jù),拍一張照片,然后保存下來。
RDB持久化,是指在指定的時(shí)間間隔內(nèi),執(zhí)行指定次數(shù)的寫操作,將內(nèi)存中的數(shù)據(jù)集快照寫入磁盤中,它是Redis默認(rèn)的持久化方式。執(zhí)行完操作后,在指定目錄下會(huì)生成一個(gè)dump.rdb文件,Redis 重啟的時(shí)候,通過加載dump.rdb文件來恢復(fù)數(shù)據(jù)。RDB觸發(fā)機(jī)制主要有以下幾種:

RDB 的優(yōu)點(diǎn)
適合大規(guī)模的數(shù)據(jù)恢復(fù)場景,如備份,全量復(fù)制等
RDB缺點(diǎn)
沒辦法做到實(shí)時(shí)持久化/秒級(jí)持久化。 新老版本存在RDB格式兼容問題
AOF
AOF(append only file) 持久化,采用日志的形式來記錄每個(gè)寫操作,追加到文件中,重啟時(shí)再重新執(zhí)行AOF文件中的命令來恢復(fù)數(shù)據(jù)。它主要解決數(shù)據(jù)持久化的實(shí)時(shí)性問題。默認(rèn)是不開啟的。
AOF的工作流程如下:

AOF的優(yōu)點(diǎn)
數(shù)據(jù)的一致性和完整性更高
AOF的缺點(diǎn)
AOF記錄的內(nèi)容越多,文件越大,數(shù)據(jù)恢復(fù)變慢。
9.怎么實(shí)現(xiàn)Redis的高可用?
我們?cè)陧?xiàng)目中使用Redis,肯定不會(huì)是單點(diǎn)部署Redis服務(wù)的。因?yàn)椋瑔吸c(diǎn)部署一旦宕機(jī),就不可用了。為了實(shí)現(xiàn)高可用,通常的做法是,將數(shù)據(jù)庫復(fù)制多個(gè)副本以部署在不同的服務(wù)器上,其中一臺(tái)掛了也可以繼續(xù)提供服務(wù)。Redis 實(shí)現(xiàn)高可用有三種部署模式:主從模式,哨兵模式,集群模式。
9.1 主從模式
主從模式中,Redis部署了多臺(tái)機(jī)器,有主節(jié)點(diǎn),負(fù)責(zé)讀寫操作,有從節(jié)點(diǎn),只負(fù)責(zé)讀操作。從節(jié)點(diǎn)的數(shù)據(jù)來自主節(jié)點(diǎn),實(shí)現(xiàn)原理就是主從復(fù)制機(jī)制
主從復(fù)制包括全量復(fù)制,增量復(fù)制兩種。一般當(dāng)slave第一次啟動(dòng)連接master,或者認(rèn)為是第一次連接,就采用全量復(fù)制,全量復(fù)制流程如下:

1.slave發(fā)送sync命令到master。 2.master接收到SYNC命令后,執(zhí)行bgsave命令,生成RDB全量文件。 3.master使用緩沖區(qū),記錄RDB快照生成期間的所有寫命令。 4.master執(zhí)行完bgsave后,向所有slave發(fā)送RDB快照文件。 5.slave收到RDB快照文件后,載入、解析收到的快照。 6.master使用緩沖區(qū),記錄RDB同步期間生成的所有寫的命令。 7.master快照發(fā)送完畢后,開始向slave發(fā)送緩沖區(qū)中的寫命令; 8.salve接受命令請(qǐng)求,并執(zhí)行來自master緩沖區(qū)的寫命令
redis2.8版本之后,已經(jīng)使用psync來替代sync,因?yàn)閟ync命令非常消耗系統(tǒng)資源,psync的效率更高。
slave與master全量同步之后,master上的數(shù)據(jù),如果再次發(fā)生更新,就會(huì)觸發(fā)增量復(fù)制。
當(dāng)master節(jié)點(diǎn)發(fā)生數(shù)據(jù)增減時(shí),就會(huì)觸發(fā)replicationFeedSalves()函數(shù),接下來在 Master節(jié)點(diǎn)上調(diào)用的每一個(gè)命令會(huì)使用replicationFeedSlaves()來同步到Slave節(jié)點(diǎn)。執(zhí)行此函數(shù)之前呢,master節(jié)點(diǎn)會(huì)判斷用戶執(zhí)行的命令是否有數(shù)據(jù)更新,如果有數(shù)據(jù)更新的話,并且slave節(jié)點(diǎn)不為空,就會(huì)執(zhí)行此函數(shù)。這個(gè)函數(shù)作用就是:把用戶執(zhí)行的命令發(fā)送到所有的slave節(jié)點(diǎn),讓slave節(jié)點(diǎn)執(zhí)行。流程如下:

9.2 哨兵模式
主從模式中,一旦主節(jié)點(diǎn)由于故障不能提供服務(wù),需要人工將從節(jié)點(diǎn)晉升為主節(jié)點(diǎn),同時(shí)還要通知應(yīng)用方更新主節(jié)點(diǎn)地址。顯然,多數(shù)業(yè)務(wù)場景都不能接受這種故障處理方式。Redis從2.8開始正式提供了Redis Sentinel(哨兵)架構(gòu)來解決這個(gè)問題。
哨兵模式,由一個(gè)或多個(gè)Sentinel實(shí)例組成的Sentinel系統(tǒng),它可以監(jiān)視所有的Redis主節(jié)點(diǎn)和從節(jié)點(diǎn),并在被監(jiān)視的主節(jié)點(diǎn)進(jìn)入下線狀態(tài)時(shí),自動(dòng)將下線主服務(wù)器屬下的某個(gè)從節(jié)點(diǎn)升級(jí)為新的主節(jié)點(diǎn)。但是呢,一個(gè)哨兵進(jìn)程對(duì)Redis節(jié)點(diǎn)進(jìn)行監(jiān)控,就可能會(huì)出現(xiàn)問題(單點(diǎn)問題),因此,可以使用多個(gè)哨兵來進(jìn)行監(jiān)控Redis節(jié)點(diǎn),并且各個(gè)哨兵之間還會(huì)進(jìn)行監(jiān)控。

簡單來說,哨兵模式就三個(gè)作用:
發(fā)送命令,等待Redis服務(wù)器(包括主服務(wù)器和從服務(wù)器)返回監(jiān)控其運(yùn)行狀態(tài); 哨兵監(jiān)測(cè)到主節(jié)點(diǎn)宕機(jī),會(huì)自動(dòng)將從節(jié)點(diǎn)切換成主節(jié)點(diǎn),然后通過發(fā)布訂閱模式通知其他的從節(jié)點(diǎn),修改配置文件,讓它們切換主機(jī); 哨兵之間還會(huì)相互監(jiān)控,從而達(dá)到高可用。
故障切換的過程是怎樣的呢
假設(shè)主服務(wù)器宕機(jī),哨兵1先檢測(cè)到這個(gè)結(jié)果,系統(tǒng)并不會(huì)馬上進(jìn)行 failover 過程,僅僅是哨兵1主觀的認(rèn)為主服務(wù)器不可用,這個(gè)現(xiàn)象成為主觀下線。當(dāng)后面的哨兵也檢測(cè)到主服務(wù)器不可用,并且數(shù)量達(dá)到一定值時(shí),那么哨兵之間就會(huì)進(jìn)行一次投票,投票的結(jié)果由一個(gè)哨兵發(fā)起,進(jìn)行 failover 操作。切換成功后,就會(huì)通過發(fā)布訂閱模式,讓各個(gè)哨兵把自己監(jiān)控的從服務(wù)器實(shí)現(xiàn)切換主機(jī),這個(gè)過程稱為客觀下線。這樣對(duì)于客戶端而言,一切都是透明的。
哨兵的工作模式如下:
每個(gè)Sentinel以每秒鐘一次的頻率向它所知的Master,Slave以及其他Sentinel實(shí)例發(fā)送一個(gè) PING命令。 如果一個(gè)實(shí)例(instance)距離最后一次有效回復(fù) PING 命令的時(shí)間超過 down-after-milliseconds 選項(xiàng)所指定的值, 則這個(gè)實(shí)例會(huì)被 Sentinel標(biāo)記為主觀下線。 如果一個(gè)Master被標(biāo)記為主觀下線,則正在監(jiān)視這個(gè)Master的所有 Sentinel 要以每秒一次的頻率確認(rèn)Master的確進(jìn)入了主觀下線狀態(tài)。 當(dāng)有足夠數(shù)量的 Sentinel(大于等于配置文件指定的值)在指定的時(shí)間范圍內(nèi)確認(rèn)Master的確進(jìn)入了主觀下線狀態(tài), 則Master會(huì)被標(biāo)記為客觀下線。 在一般情況下, 每個(gè) Sentinel 會(huì)以每10秒一次的頻率向它已知的所有Master,Slave發(fā)送 INFO 命令。 當(dāng)Master被 Sentinel 標(biāo)記為客觀下線時(shí),Sentinel 向下線的 Master 的所有 Slave 發(fā)送 INFO 命令的頻率會(huì)從 10 秒一次改為每秒一次 若沒有足夠數(shù)量的 Sentinel同意Master已經(jīng)下線, Master的客觀下線狀態(tài)就會(huì)被移除;若Master 重新向 Sentinel 的 PING 命令返回有效回復(fù), Master 的主觀下線狀態(tài)就會(huì)被移除。
9.3 Cluster集群模式
哨兵模式基于主從模式,實(shí)現(xiàn)讀寫分離,它還可以自動(dòng)切換,系統(tǒng)可用性更高。但是它每個(gè)節(jié)點(diǎn)存儲(chǔ)的數(shù)據(jù)是一樣的,浪費(fèi)內(nèi)存,并且不好在線擴(kuò)容。因此,Cluster集群應(yīng)運(yùn)而生,它在Redis3.0加入的,實(shí)現(xiàn)了Redis的分布式存儲(chǔ)。對(duì)數(shù)據(jù)進(jìn)行分片,也就是說每臺(tái)Redis節(jié)點(diǎn)上存儲(chǔ)不同的內(nèi)容,來解決在線擴(kuò)容的問題。并且,它也提供復(fù)制和故障轉(zhuǎn)移的功能。
Cluster集群節(jié)點(diǎn)的通訊
一個(gè)Redis集群由多個(gè)節(jié)點(diǎn)組成,各個(gè)節(jié)點(diǎn)之間是怎么通信的呢?通過Gossip協(xié)議!
Redis Cluster集群通過Gossip協(xié)議進(jìn)行通信,節(jié)點(diǎn)之前不斷交換信息,交換的信息內(nèi)容包括節(jié)點(diǎn)出現(xiàn)故障、新節(jié)點(diǎn)加入、主從節(jié)點(diǎn)變更信息、slot信息等等。常用的Gossip消息分為4種,分別是:ping、pong、meet、fail。

meet消息:通知新節(jié)點(diǎn)加入。消息發(fā)送者通知接收者加入到當(dāng)前集群,meet消息通信正常完成后,接收節(jié)點(diǎn)會(huì)加入到集群中并進(jìn)行周期性的ping、pong消息交換。 ping消息:集群內(nèi)交換最頻繁的消息,集群內(nèi)每個(gè)節(jié)點(diǎn)每秒向多個(gè)其他節(jié)點(diǎn)發(fā)送ping消息,用于檢測(cè)節(jié)點(diǎn)是否在線和交換彼此狀態(tài)信息。 pong消息:當(dāng)接收到ping、meet消息時(shí),作為響應(yīng)消息回復(fù)給發(fā)送方確認(rèn)消息正常通信。pong消息內(nèi)部封裝了自身狀態(tài)數(shù)據(jù)。節(jié)點(diǎn)也可以向集群內(nèi)廣播自身的pong消息來通知整個(gè)集群對(duì)自身狀態(tài)進(jìn)行更新。 fail消息:當(dāng)節(jié)點(diǎn)判定集群內(nèi)另一個(gè)節(jié)點(diǎn)下線時(shí),會(huì)向集群內(nèi)廣播一個(gè)fail消息,其他節(jié)點(diǎn)接收到fail消息之后把對(duì)應(yīng)節(jié)點(diǎn)更新為下線狀態(tài)。
特別的,每個(gè)節(jié)點(diǎn)是通過集群總線(cluster bus) 與其他的節(jié)點(diǎn)進(jìn)行通信的。通訊時(shí),使用特殊的端口號(hào),即對(duì)外服務(wù)端口號(hào)加10000。例如如果某個(gè)node的端口號(hào)是6379,那么它與其它nodes通信的端口號(hào)是 16379。nodes 之間的通信采用特殊的二進(jìn)制協(xié)議。
Hash Slot插槽算法
既然是分布式存儲(chǔ),Cluster集群使用的分布式算法是一致性Hash嘛?并不是,而是Hash Slot插槽算法。
插槽算法把整個(gè)數(shù)據(jù)庫被分為16384個(gè)slot(槽),每個(gè)進(jìn)入Redis的鍵值對(duì),根據(jù)key進(jìn)行散列,分配到這16384插槽中的一個(gè)。使用的哈希映射也比較簡單,用CRC16算法計(jì)算出一個(gè)16 位的值,再對(duì)16384取模。數(shù)據(jù)庫中的每個(gè)鍵都屬于這16384個(gè)槽的其中一個(gè),集群中的每個(gè)節(jié)點(diǎn)都可以處理這16384個(gè)槽。
集群中的每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分的hash槽,比如當(dāng)前集群有A、B、C個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)上的哈希槽數(shù) =16384/3,那么就有:
節(jié)點(diǎn)A負(fù)責(zé)0~5460號(hào)哈希槽 節(jié)點(diǎn)B負(fù)責(zé)5461~10922號(hào)哈希槽 節(jié)點(diǎn)C負(fù)責(zé)10923~16383號(hào)哈希槽
Redis Cluster集群
Redis Cluster集群中,需要確保16384個(gè)槽對(duì)應(yīng)的node都正常工作,如果某個(gè)node出現(xiàn)故障,它負(fù)責(zé)的slot也會(huì)失效,整個(gè)集群將不能工作。
因此為了保證高可用,Cluster集群引入了主從復(fù)制,一個(gè)主節(jié)點(diǎn)對(duì)應(yīng)一個(gè)或者多個(gè)從節(jié)點(diǎn)。當(dāng)其它主節(jié)點(diǎn) ping 一個(gè)主節(jié)點(diǎn) A 時(shí),如果半數(shù)以上的主節(jié)點(diǎn)與 A 通信超時(shí),那么認(rèn)為主節(jié)點(diǎn) A 宕機(jī)了。如果主節(jié)點(diǎn)宕機(jī)時(shí),就會(huì)啟用從節(jié)點(diǎn)。
在Redis的每一個(gè)節(jié)點(diǎn)上,都有兩個(gè)玩意,一個(gè)是插槽(slot),它的取值范圍是0~16383。另外一個(gè)是cluster,可以理解為一個(gè)集群管理的插件。當(dāng)我們存取的key到達(dá)時(shí),Redis 會(huì)根據(jù)CRC16算法得出一個(gè)16 bit的值,然后把結(jié)果對(duì)16384取模。醬紫每個(gè)key都會(huì)對(duì)應(yīng)一個(gè)編號(hào)在 0~16383 之間的哈希槽,通過這個(gè)值,去找到對(duì)應(yīng)的插槽所對(duì)應(yīng)的節(jié)點(diǎn),然后直接自動(dòng)跳轉(zhuǎn)到這個(gè)對(duì)應(yīng)的節(jié)點(diǎn)上進(jìn)行存取操作。
雖然數(shù)據(jù)是分開存儲(chǔ)在不同節(jié)點(diǎn)上的,但是對(duì)客戶端來說,整個(gè)集群Cluster,被看做一個(gè)整體。客戶端端連接任意一個(gè)node,看起來跟操作單實(shí)例的Redis一樣。當(dāng)客戶端操作的key沒有被分配到正確的node節(jié)點(diǎn)時(shí),Redis會(huì)返回轉(zhuǎn)向指令,最后指向正確的node,這就有點(diǎn)像瀏覽器頁面的302 重定向跳轉(zhuǎn)。

故障轉(zhuǎn)移
Redis集群實(shí)現(xiàn)了高可用,當(dāng)集群內(nèi)節(jié)點(diǎn)出現(xiàn)故障時(shí),通過故障轉(zhuǎn)移,以保證集群正常對(duì)外提供服務(wù)。
redis集群通過ping/pong消息,實(shí)現(xiàn)故障發(fā)現(xiàn)。這個(gè)環(huán)境包括主觀下線和客觀下線。
主觀下線: 某個(gè)節(jié)點(diǎn)認(rèn)為另一個(gè)節(jié)點(diǎn)不可用,即下線狀態(tài),這個(gè)狀態(tài)并不是最終的故障判定,只能代表一個(gè)節(jié)點(diǎn)的意見,可能存在誤判情況。

客觀下線: 指標(biāo)記一個(gè)節(jié)點(diǎn)真正的下線,集群內(nèi)多個(gè)節(jié)點(diǎn)都認(rèn)為該節(jié)點(diǎn)不可用,從而達(dá)成共識(shí)的結(jié)果。如果是持有槽的主節(jié)點(diǎn)故障,需要為該節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移。
假如節(jié)點(diǎn)A標(biāo)記節(jié)點(diǎn)B為主觀下線,一段時(shí)間后,節(jié)點(diǎn)A通過消息把節(jié)點(diǎn)B的狀態(tài)發(fā)到其它節(jié)點(diǎn),當(dāng)節(jié)點(diǎn)C接受到消息并解析出消息體時(shí),如果發(fā)現(xiàn)節(jié)點(diǎn)B的pfail狀態(tài)時(shí),會(huì)觸發(fā)客觀下線流程; 當(dāng)下線為主節(jié)點(diǎn)時(shí),此時(shí)Redis Cluster集群為統(tǒng)計(jì)持有槽的主節(jié)點(diǎn)投票,看投票數(shù)是否達(dá)到一半,當(dāng)下線報(bào)告統(tǒng)計(jì)數(shù)大于一半時(shí),被標(biāo)記為客觀下線狀態(tài)。
流程如下:

故障恢復(fù):故障發(fā)現(xiàn)后,如果下線節(jié)點(diǎn)的是主節(jié)點(diǎn),則需要在它的從節(jié)點(diǎn)中選一個(gè)替換它,以保證集群的高可用。流程如下:

資格檢查:檢查從節(jié)點(diǎn)是否具備替換故障主節(jié)點(diǎn)的條件。 準(zhǔn)備選舉時(shí)間:資格檢查通過后,更新觸發(fā)故障選舉時(shí)間。 發(fā)起選舉:到了故障選舉時(shí)間,進(jìn)行選舉。 選舉投票:只有持有槽的主節(jié)點(diǎn)才有票,從節(jié)點(diǎn)收集到足夠的選票(大于一半),觸發(fā)替換主節(jié)點(diǎn)操作
10. 使用過Redis分布式鎖嘛?有哪些注意點(diǎn)呢?
分布式鎖,是控制分布式系統(tǒng)不同進(jìn)程共同訪問共享資源的一種鎖的實(shí)現(xiàn)。秒殺下單、搶紅包等等業(yè)務(wù)場景,都需要用到分布式鎖,我們項(xiàng)目中經(jīng)常使用Redis作為分布式鎖。
選了Redis分布式鎖的幾種實(shí)現(xiàn)方法,大家來討論下,看有沒有啥問題哈。
命令setnx + expire分開寫 setnx + value值是過期時(shí)間 set的擴(kuò)展命令(set ex px nx) set ex px nx + 校驗(yàn)唯一隨機(jī)值,再刪除
10.1 命令setnx + expire分開寫
if(jedis.setnx(key,lock_value)?==?1){?//加鎖
????expire(key,100);?//設(shè)置過期時(shí)間
????try?{
????????do?something??//業(yè)務(wù)請(qǐng)求
????}catch(){
??}
??finally?{
???????jedis.del(key);?//釋放鎖
????}
}
如果執(zhí)行完setnx加鎖,正要執(zhí)行expire設(shè)置過期時(shí)間時(shí),進(jìn)程crash掉或者要重啟維護(hù)了,那這個(gè)鎖就“長生不老”了,別的線程永遠(yuǎn)獲取不到鎖啦,所以分布式鎖不能這么實(shí)現(xiàn)。
10.2 setnx + value值是過期時(shí)間
long?expires?=?System.currentTimeMillis()?+?expireTime;?//系統(tǒng)時(shí)間+設(shè)置的過期時(shí)間
String?expiresStr?=?String.valueOf(expires);
//?如果當(dāng)前鎖不存在,返回加鎖成功
if?(jedis.setnx(key,?expiresStr)?==?1)?{
????????return?true;
}?
//?如果鎖已經(jīng)存在,獲取鎖的過期時(shí)間
String?currentValueStr?=?jedis.get(key);
//?如果獲取到的過期時(shí)間,小于系統(tǒng)當(dāng)前時(shí)間,表示已經(jīng)過期
if?(currentValueStr?!=?null?&&?Long.parseLong(currentValueStr)?
?????//?鎖已過期,獲取上一個(gè)鎖的過期時(shí)間,并設(shè)置現(xiàn)在鎖的過期時(shí)間(不了解redis的getSet命令的小伙伴,可以去官網(wǎng)看下哈)
????String?oldValueStr?=?jedis.getSet(key_resource_id,?expiresStr);
????
????if?(oldValueStr?!=?null?&&?oldValueStr.equals(currentValueStr))?{
?????????//?考慮多線程并發(fā)的情況,只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,它才可以加鎖
?????????return?true;
????}
}
????????
//其他情況,均返回加鎖失敗
return?false;
}
筆者看過有開發(fā)小伙伴是這么實(shí)現(xiàn)分布式鎖的,但是這種方案也有這些缺點(diǎn):
過期時(shí)間是客戶端自己生成的,分布式環(huán)境下,每個(gè)客戶端的時(shí)間必須同步。 沒有保存持有者的唯一標(biāo)識(shí),可能被別的客戶端釋放/解鎖。 鎖過期的時(shí)候,并發(fā)多個(gè)客戶端同時(shí)請(qǐng)求過來,都執(zhí)行了 jedis.getSet(),最終只能有一個(gè)客戶端加鎖成功,但是該客戶端鎖的過期時(shí)間,可能被別的客戶端覆蓋。
10.3:set的擴(kuò)展命令(set ex px nx)(注意可能存在的問題)
if(jedis.set(key,?lock_value,?"NX",?"EX",?100s)?==?1){?//加鎖
????try?{
????????do?something??//業(yè)務(wù)處理
????}catch(){
??}
??finally?{
???????jedis.del(key);?//釋放鎖
????}
}
這個(gè)方案可能存在這樣的問題:
鎖過期釋放了,業(yè)務(wù)還沒執(zhí)行完。 鎖被別的線程誤刪。
10.4 set ex px nx + 校驗(yàn)唯一隨機(jī)值,再刪除
if(jedis.set(key,?uni_request_id,?"NX",?"EX",?100s)?==?1){?//加鎖
????try?{
????????do?something??//業(yè)務(wù)處理
????}catch(){
??}
??finally?{
???????//判斷是不是當(dāng)前線程加的鎖,是才釋放
???????if?(uni_request_id.equals(jedis.get(key)))?{
????????jedis.del(key);?//釋放鎖
????????}
????}
}
在這里,判斷當(dāng)前線程加的鎖和釋放鎖是不是一個(gè)原子操作。如果調(diào)用jedis.del()釋放鎖的時(shí)候,可能這把鎖已經(jīng)不屬于當(dāng)前客戶端,會(huì)解除他人加的鎖。

一般也是用lua腳本代替。lua腳本如下:
if?redis.call('get',KEYS[1])?==?ARGV[1]?then?
???return?redis.call('del',KEYS[1])?
else
???return?0
end;
這種方式比較不錯(cuò)了,一般情況下,已經(jīng)可以使用這種實(shí)現(xiàn)方式。但是存在鎖過期釋放了,業(yè)務(wù)還沒執(zhí)行完的問題(實(shí)際上,估算個(gè)業(yè)務(wù)處理的時(shí)間,一般沒啥問題了)。
11. 使用過Redisson嘛?說說它的原理
分布式鎖可能存在鎖過期釋放,業(yè)務(wù)沒執(zhí)行完的問題。有些小伙伴認(rèn)為,稍微把鎖過期時(shí)間設(shè)置長一些就可以啦。其實(shí)我們?cè)O(shè)想一下,是否可以給獲得鎖的線程,開啟一個(gè)定時(shí)守護(hù)線程,每隔一段時(shí)間檢查鎖是否還存在,存在則對(duì)鎖的過期時(shí)間延長,防止鎖過期提前釋放。
當(dāng)前開源框架Redisson就解決了這個(gè)分布式鎖問題。我們一起來看下Redisson底層原理是怎樣的吧:

只要線程一加鎖成功,就會(huì)啟動(dòng)一個(gè)watch dog看門狗,它是一個(gè)后臺(tái)線程,會(huì)每隔10秒檢查一下,如果線程1還持有鎖,那么就會(huì)不斷的延長鎖key的生存時(shí)間。因此,Redisson就是使用Redisson解決了鎖過期釋放,業(yè)務(wù)沒執(zhí)行完問題。
12. 什么是Redlock算法
Redis一般都是集群部署的,假設(shè)數(shù)據(jù)在主從同步過程,主節(jié)點(diǎn)掛了,Redis分布式鎖可能會(huì)有哪些問題呢?一起來看些這個(gè)流程圖:

如果線程一在Redis的master節(jié)點(diǎn)上拿到了鎖,但是加鎖的key還沒同步到slave節(jié)點(diǎn)。恰好這時(shí),master節(jié)點(diǎn)發(fā)生故障,一個(gè)slave節(jié)點(diǎn)就會(huì)升級(jí)為master節(jié)點(diǎn)。線程二就可以獲取同個(gè)key的鎖啦,但線程一也已經(jīng)拿到鎖了,鎖的安全性就沒了。
為了解決這個(gè)問題,Redis作者 antirez提出一種高級(jí)的分布式鎖算法:Redlock。Redlock核心思想是這樣的:
搞多個(gè)Redis master部署,以保證它們不會(huì)同時(shí)宕掉。并且這些master節(jié)點(diǎn)是完全相互獨(dú)立的,相互之間不存在數(shù)據(jù)同步。同時(shí),需要確保在這多個(gè)master實(shí)例上,是與在Redis單實(shí)例,使用相同方法來獲取和釋放鎖。
我們假設(shè)當(dāng)前有5個(gè)Redis master節(jié)點(diǎn),在5臺(tái)服務(wù)器上面運(yùn)行這些Redis實(shí)例。

RedLock的實(shí)現(xiàn)步驟:如下
1.獲取當(dāng)前時(shí)間,以毫秒為單位。 2.按順序向5個(gè)master節(jié)點(diǎn)請(qǐng)求加鎖。客戶端設(shè)置網(wǎng)絡(luò)連接和響應(yīng)超時(shí)時(shí)間,并且超時(shí)時(shí)間要小于鎖的失效時(shí)間。(假設(shè)鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間一般在5-50毫秒之間,我們就假設(shè)超時(shí)時(shí)間是50ms吧)。如果超時(shí),跳過該master節(jié)點(diǎn),盡快去嘗試下一個(gè)master節(jié)點(diǎn)。 3.客戶端使用當(dāng)前時(shí)間減去開始獲取鎖時(shí)間(即步驟1記錄的時(shí)間),得到獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)超過一半(N/2+1,這里是5/2+1=3個(gè)節(jié)點(diǎn))的Redis master節(jié)點(diǎn)都獲得鎖,并且使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。(如上圖,10s> 30ms+40ms+50ms+4m0s+50ms) 如果取到了鎖,key的真正有效時(shí)間就變啦,需要減去獲取鎖所使用的時(shí)間。 如果獲取鎖失敗(沒有在至少N/2+1個(gè)master實(shí)例取到鎖,有或者獲取鎖時(shí)間已經(jīng)超過了有效時(shí)間),客戶端要在所有的master節(jié)點(diǎn)上解鎖(即便有些master節(jié)點(diǎn)根本就沒有加鎖成功,也需要解鎖,以防止有些漏網(wǎng)之魚)。
簡化下步驟就是:
按順序向5個(gè)master節(jié)點(diǎn)請(qǐng)求加鎖 根據(jù)設(shè)置的超時(shí)時(shí)間來判斷,是不是要跳過該master節(jié)點(diǎn)。 如果大于等于三個(gè)節(jié)點(diǎn)加鎖成功,并且使用的時(shí)間小于鎖的有效期,即可認(rèn)定加鎖成功啦。 如果獲取鎖失敗,解鎖!
13. Redis的跳躍表

跳躍表是有序集合zset的底層實(shí)現(xiàn)之一 跳躍表支持平均O(logN),最壞 O(N)復(fù)雜度的節(jié)點(diǎn)查找,還可以通過順序性操作批量處理節(jié)點(diǎn)。 跳躍表實(shí)現(xiàn)由zskiplist和zskiplistNode兩個(gè)結(jié)構(gòu)組成,其中zskiplist用于保存跳躍表信息(如表頭節(jié)點(diǎn)、表尾節(jié)點(diǎn)、長度),而zskiplistNode則用于表示跳躍表節(jié)點(diǎn)。 跳躍表就是在鏈表的基礎(chǔ)上,增加多級(jí)索引提升查找效率。
14. MySQL與Redis 如何保證雙寫一致性
緩存延時(shí)雙刪 刪除緩存重試機(jī)制 讀取biglog異步刪除緩存
14.1 延時(shí)雙刪?
什么是延時(shí)雙刪呢?流程圖如下:

先刪除緩存 再更新數(shù)據(jù)庫 休眠一會(huì)(比如1秒),再次刪除緩存。
這個(gè)休眠一會(huì),一般多久呢?都是1秒?
這個(gè)休眠時(shí)間 = ?讀業(yè)務(wù)邏輯數(shù)據(jù)的耗時(shí) + 幾百毫秒。為了確保讀請(qǐng)求結(jié)束,寫請(qǐng)求可以刪除讀請(qǐng)求可能帶來的緩存臟數(shù)據(jù)。
這種方案還算可以,只有休眠那一會(huì)(比如就那1秒),可能有臟數(shù)據(jù),一般業(yè)務(wù)也會(huì)接受的。但是如果第二次刪除緩存失敗呢?緩存和數(shù)據(jù)庫的數(shù)據(jù)還是可能不一致,對(duì)吧?給Key設(shè)置一個(gè)自然的expire過期時(shí)間,讓它自動(dòng)過期怎樣?那業(yè)務(wù)要接受過期時(shí)間內(nèi),數(shù)據(jù)的不一致咯?還是有其他更佳方案呢?
14.2 刪除緩存重試機(jī)制
因?yàn)檠訒r(shí)雙刪可能會(huì)存在第二步的刪除緩存失敗,導(dǎo)致的數(shù)據(jù)不一致問題。可以使用這個(gè)方案優(yōu)化:刪除失敗就多刪除幾次呀,保證刪除緩存成功就可以了呀~ 所以可以引入刪除緩存重試機(jī)制

寫請(qǐng)求更新數(shù)據(jù)庫 緩存因?yàn)槟承┰颍瑒h除失敗 把刪除失敗的key放到消息隊(duì)列 消費(fèi)消息隊(duì)列的消息,獲取要?jiǎng)h除的key 重試刪除緩存操作
14.3 讀取biglog異步刪除緩存
重試刪除緩存機(jī)制還可以吧,就是會(huì)造成好多業(yè)務(wù)代碼入侵。其實(shí),還可以這樣優(yōu)化:通過數(shù)據(jù)庫的binlog來異步淘汰key。

以mysql為例吧
可以使用阿里的canal將binlog日志采集發(fā)送到MQ隊(duì)列里面 然后通過ACK機(jī)制確認(rèn)處理這條更新消息,刪除緩存,保證數(shù)據(jù)緩存一致性
15. 為什么Redis 6.0 之后改多線程呢?
Redis6.0之前,Redis在處理客戶端的請(qǐng)求時(shí),包括讀socket、解析、執(zhí)行、寫socket等都由一個(gè)順序串行的主線程處理,這就是所謂的“單線程”。 Redis6.0之前為什么一直不使用多線程?使用Redis時(shí),幾乎不存在CPU成為瓶頸的情況, Redis主要受限于內(nèi)存和網(wǎng)絡(luò)。例如在一個(gè)普通的Linux系統(tǒng)上,Redis通過使用pipelining每秒可以處理100萬個(gè)請(qǐng)求,所以如果應(yīng)用程序主要使用O(N)或O(log(N))的命令,它幾乎不會(huì)占用太多CPU。
redis使用多線程并非是完全摒棄單線程,redis還是使用單線程模型來處理客戶端的請(qǐng)求,只是使用多線程來處理數(shù)據(jù)的讀寫和協(xié)議解析,執(zhí)行命令還是使用單線程。
這樣做的目的是因?yàn)閞edis的性能瓶頸在于網(wǎng)絡(luò)IO而非CPU,使用多線程能提升IO讀寫的效率,從而整體提高redis的性能。
16. 聊聊Redis 事務(wù)機(jī)制
Redis通過MULTI、EXEC、WATCH等一組命令集合,來實(shí)現(xiàn)事務(wù)機(jī)制。事務(wù)支持一次執(zhí)行多個(gè)命令,一個(gè)事務(wù)中所有命令都會(huì)被序列化。在事務(wù)執(zhí)行過程,會(huì)按照順序串行化執(zhí)行隊(duì)列中的命令,其他客戶端提交的命令請(qǐng)求不會(huì)插入到事務(wù)執(zhí)行命令序列中。
簡言之,Redis事務(wù)就是順序性、一次性、排他性的執(zhí)行一個(gè)隊(duì)列中的一系列命令。
Redis執(zhí)行事務(wù)的流程如下:
開始事務(wù)(MULTI) 命令入隊(duì) 執(zhí)行事務(wù)(EXEC)、撤銷事務(wù)(DISCARD )
| 命令 | 描述 |
|---|---|
| EXEC | 執(zhí)行所有事務(wù)塊內(nèi)的命令 |
| DISCARD | 取消事務(wù),放棄執(zhí)行事務(wù)塊內(nèi)的所有命令 |
| MULTI | 標(biāo)記一個(gè)事務(wù)塊的開始 |
| UNWATCH | 取消 WATCH 命令對(duì)所有 key 的監(jiān)視。 |
| WATCH | 監(jiān)視key ,如果在事務(wù)執(zhí)行之前,該key 被其他命令所改動(dòng),那么事務(wù)將被打斷。 |
17. Redis的Hash 沖突怎么辦
Redis 作為一個(gè)K-V的內(nèi)存數(shù)據(jù)庫,它使用用一張全局的哈希來保存所有的鍵值對(duì)。這張哈希表,有多個(gè)哈希桶組成,哈希桶中的entry元素保存了key和value指針,其中*key指向了實(shí)際的鍵,*value指向了實(shí)際的值。
哈希表查找速率很快的,有點(diǎn)類似于Java中的HashMap,它讓我們?cè)贠(1) 的時(shí)間復(fù)雜度快速找到鍵值對(duì)。首先通過key計(jì)算哈希值,找到對(duì)應(yīng)的哈希桶位置,然后定位到entry,在entry找到對(duì)應(yīng)的數(shù)據(jù)。
什么是哈希沖突?
哈希沖突:通過不同的key,計(jì)算出一樣的哈希值,導(dǎo)致落在同一個(gè)哈希桶中。
Redis為了解決哈希沖突,采用了鏈?zhǔn)焦?/strong>。鏈?zhǔn)焦J侵竿粋€(gè)哈希桶中,多個(gè)元素用一個(gè)鏈表來保存,它們之間依次用指針連接。

有些讀者可能還會(huì)有疑問:哈希沖突鏈上的元素只能通過指針逐一查找再操作。當(dāng)往哈希表插入數(shù)據(jù)很多,沖突也會(huì)越多,沖突鏈表就會(huì)越長,那查詢效率就會(huì)降低了。
為了保持高效,Redis 會(huì)對(duì)哈希表做rehash操作,也就是增加哈希桶,減少?zèng)_突。為了rehash更高效,Redis還默認(rèn)使用了兩個(gè)全局哈希表,一個(gè)用于當(dāng)前使用,稱為主哈希表,一個(gè)用于擴(kuò)容,稱為備用哈希表。
18. 在生成 RDB期間,Redis 可以同時(shí)處理寫請(qǐng)求么?
可以的,Redis提供兩個(gè)指令生成RDB,分別是save和bgsave。
如果是save指令,會(huì)阻塞,因?yàn)槭侵骶€程執(zhí)行的。 如果是bgsave指令,是fork一個(gè)子進(jìn)程來寫入RDB文件的,快照持久化完全交給子進(jìn)程來處理,父進(jìn)程則可以繼續(xù)處理客戶端的請(qǐng)求。
19. Redis底層,使用的什么協(xié)議?
RESP,英文全稱是Redis Serialization Protocol,它是專門為redis設(shè)計(jì)的一套序列化協(xié)議. 這個(gè)協(xié)議其實(shí)在redis的1.2版本時(shí)就已經(jīng)出現(xiàn)了,但是到了redis2.0才最終成為redis通訊協(xié)議的標(biāo)準(zhǔn)。
RESP主要有實(shí)現(xiàn)簡單、解析速度快、可讀性好等優(yōu)點(diǎn)。
20. 布隆過濾器
應(yīng)對(duì)緩存穿透問題,我們可以使用布隆過濾器。布隆過濾器是什么呢?
布隆過濾器是一種占用空間很小的數(shù)據(jù)結(jié)構(gòu),它由一個(gè)很長的二進(jìn)制向量和一組Hash映射函數(shù)組成,它用于檢索一個(gè)元素是否在一個(gè)集合中,空間效率和查詢時(shí)間都比一般的算法要好的多,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難。
布隆過濾器原理是?假設(shè)我們有個(gè)集合A,A中有n個(gè)元素。利用k個(gè)哈希散列函數(shù),將A中的每個(gè)元素映射到一個(gè)長度為a位的數(shù)組B中的不同位置上,這些位置上的二進(jìn)制數(shù)均設(shè)置為1。如果待檢查的元素,經(jīng)過這k個(gè)哈希散列函數(shù)的映射后,發(fā)現(xiàn)其k個(gè)位置上的二進(jìn)制數(shù)全部為1,這個(gè)元素很可能屬于集合A,反之,一定不屬于集合A。
來看個(gè)簡單例子吧,假設(shè)集合A有3個(gè)元素,分別為{d1,d2,d3}。有1個(gè)哈希函數(shù),為Hash1。現(xiàn)在將A的每個(gè)元素映射到長度為16位數(shù)組B。

我們現(xiàn)在把d1映射過來,假設(shè)Hash1(d1)= 2,我們就把數(shù)組B中,下標(biāo)為2的格子改成1,如下:

我們現(xiàn)在把d2也映射過來,假設(shè)Hash1(d2)= 5,我們把數(shù)組B中,下標(biāo)為5的格子也改成1,如下:

接著我們把d3也映射過來,假設(shè)Hash1(d3)也等于 2,它也是把下標(biāo)為2的格子標(biāo)1:

因此,我們要確認(rèn)一個(gè)元素dn是否在集合A里,我們只要算出Hash1(dn)得到的索引下標(biāo),只要是0,那就表示這個(gè)元素不在集合A,如果索引下標(biāo)是1呢?那該元素可能是A中的某一個(gè)元素。因?yàn)槟憧矗琩1和d3得到的下標(biāo)值,都可能是1,還可能是其他別的數(shù)映射的,布隆過濾器是存在這個(gè)缺點(diǎn)的:會(huì)存在hash碰撞導(dǎo)致的假陽性,判斷存在誤差。
如何減少這種誤差呢?
搞多幾個(gè)哈希函數(shù)映射,降低哈希碰撞的概率 同時(shí)增加B數(shù)組的bit長度,可以增大hash函數(shù)生成的數(shù)據(jù)的范圍,也可以降低哈希碰撞的概率
我們又增加一個(gè)Hash2哈希映射函數(shù),假設(shè)Hash2(d1)=6,Hash2(d3)=8,它倆不就不沖突了嘛,如下:

即使存在誤差,我們可以發(fā)現(xiàn),布隆過濾器并沒有存放完整的數(shù)據(jù),它只是運(yùn)用一系列哈希映射函數(shù)計(jì)算出位置,然后填充二進(jìn)制向量。如果數(shù)量很大的話,布隆過濾器通過極少的錯(cuò)誤率,換取了存儲(chǔ)空間的極大節(jié)省,還是挺劃算的。
目前布隆過濾器已經(jīng)有相應(yīng)實(shí)現(xiàn)的開源類庫啦,如Google的Guava類庫,Twitter的 Algebird 類庫,信手拈來即可,或者基于Redis自帶的Bitmaps自行實(shí)現(xiàn)設(shè)計(jì)也是可以的。
Redis 高可用解決方案總結(jié): https://www.jianshu.com/p/5de2ab291696
[2]Redia系列九:redis集群高可用: https://www.cnblogs.com/leeSmall/p/8414687.html
