<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我畫了 19 張圖,幫你徹底搞懂 Redis

          共 7440字,需瀏覽 15分鐘

           ·

          2021-04-17 02:24

          又到了金三銀四跳槽季,好多同學已經(jīng)行動了。今天我來助力一把,送出這套redis面試題,助力大家通關。

          1 redis為什么響應快

          1.1數(shù)據(jù)保存在內存中

          redis數(shù)據(jù)保存在內存中,讀寫操作只要訪問內存,不需要磁盤IO。

          1.2.底層數(shù)據(jù)結構

          • redis的數(shù)據(jù)以key:value的格式存儲在散列表中,時間復雜度o(1)
          • redisvalue定義了豐富的數(shù)據(jù)結構,包括態(tài)字符串、雙向鏈表、壓縮列表、hash、跳表和整數(shù)數(shù)組,可以根據(jù)value的特性選擇選擇最高效的數(shù)據(jù)結構。

          1.3.單線程模型

          redis的網(wǎng)絡IO和數(shù)據(jù)讀取使用單線程模型,可以綁定CPU,這避免了線程上下文切換帶來的開銷。

          注意:redis6.0對網(wǎng)絡請求引入了多線程模型,讀寫操作還是用單線程。

          redis多線程網(wǎng)絡模型見下圖:

          1.4.IO多路復用

          redis采用epoll網(wǎng)絡模型,如下圖:

          內核會一直監(jiān)聽新的socket連接事件的和已建立socket連接的讀寫事件,把監(jiān)聽到的事件放到事件隊列,redis使用單線程不停的處理這個事件隊列。這避免了阻塞等待連接和讀寫事件到來。

          這些事件綁定了回調函數(shù),會調用redis的處理函數(shù)進行處理。

          2 redis底層數(shù)據(jù)結構

          redis有5種數(shù)據(jù)類型,包括字符串、列表、集合、有序集合和字典

          redis底層的數(shù)據(jù)結構有6種,包括動態(tài)字符串、雙向鏈表、壓縮列表(ziplist)、hash表、跳表(skip list)和整數(shù)數(shù)組。

          redis數(shù)據(jù)類型和底層數(shù)據(jù)結構有如下對應關系:

          2.1.字符串類型

          底層數(shù)據(jù)結構是動態(tài)字符串。

          2.2.列表

          如果同時滿足下面條件,就使用壓縮列表,否則使用雙向鏈表。

          • 列表中單個元素小于64字節(jié)
          • 列表中元素個數(shù)少于 512

          壓縮列表在內存中是一塊兒連續(xù)的內存空間,結構如下:

          壓縮列表查找時間復雜度是o(n)

          2.3.集合

          如果同時滿足下面條件,就使用有序整數(shù)數(shù)組,否則使用hash表。

          • 集合中元素都是整數(shù)類型
          • 集合中元素個數(shù)不超過512

          2.4.有序集合

          如果同時滿足下面2個條件,就使用壓縮列表,否則使用跳表。

          • 集合中元素都小于64字節(jié)
          • 集合中元素個數(shù)小于128

          注意:有序集合還有一個HASH表用于保存集合中元素的分數(shù),做ZSCORE操作時,查詢的就是這個HASH表,所以效率很高。

          跳表的結構如下:

          如果不加索引,查找10這個數(shù)字需要查詢10次,使用了二級索引,查找10這個數(shù)字需要5次,而使用一級索引,需要查詢3次。

          跳表的每一層都是一個有序鏈表,最下面一層保存了全部數(shù)據(jù)。跳表插入、刪除、查詢的時間復雜度是o(logN)。跳表需要存儲額外的索引節(jié)點,會增加額外的空間開銷。

          2.5.字典

          如果同時滿足下面2個條件,就使用壓縮列表,否則使用hash表。

          • 字典中每個entrykey/value都小于64字節(jié)
          • 字典中元素個數(shù)小于512

          3 redis緩存淘汰策略

          redis總共有8種淘汰策略,如下圖:

          volatile-lfuallkeys-lfu策略是4.0版本新增的。

          • lru是按照數(shù)據(jù)的最近最少訪問原則來淘汰數(shù)據(jù),可能存在的問題是如果大批量冷數(shù)據(jù)最近被訪問了一次,就會占用大量內存空間,如果緩存滿了,部分熱數(shù)據(jù)就會被淘汰掉。
          • lfu是按照數(shù)據(jù)的最小訪問頻率訪問次數(shù)原則來淘汰數(shù)據(jù),如果兩個數(shù)據(jù)的訪問次數(shù)相同,則把訪問時間較早的數(shù)據(jù)淘汰。

          4 redis數(shù)據(jù)持久化

          redis持久化的方式有2種,一種是寫后日志(AOF),一種是內存快照(RDB)。

          4.1.AOF日志

          AOF日志記錄了每一條收到的命令,redis故障宕機恢復時,可以加載AOF日志中的命令進行重放來進行故障恢復。AOF3種同步策略,如下圖:

          如果不是對丟失數(shù)據(jù)特別敏感的業(yè)務,推薦使用everysec,對主線程的阻塞少,故障后丟失數(shù)據(jù)只有1s。

          4.2.RDB快照

          RDB快照是一個內存快照,記錄了redis某一時刻的全部數(shù)據(jù)。

          4.3.混合日志

          redis4.0開始,AOF文件也可以保存RDB快照,AOF重寫的時候redis會把AOF文件內容清空,先記錄一份RDB快照,這份數(shù)據(jù)以"REDIS"開頭。記錄RDB內容后,AOF文件會接著記錄AOF命令。故障恢復時,先加載AOF文件中RDB快照,然后回放AOF文件中后面的命令。

          4.4.主從同步

          redis主從同步時,主節(jié)點會先生成一份RDB快照發(fā)送給從節(jié)點,把快照之后的命令寫入主從同步緩存區(qū)(replication buffer),從節(jié)點把RDB文件加載完成后,主節(jié)點把緩存區(qū)命令發(fā)送給從節(jié)點。

          4.5.AOF重寫

          AOF日志是用記錄命令的方式追加的,這樣可能存在對同一個key的多條命令,這些命令是可以合并成1條的。比如對同一個key的多個set操作日志,可以合成一條。

          4.6.阻塞點

          AOF重寫和RDB快照執(zhí)行的過程中,redis都會fork一個子進程來執(zhí)行操作,子進程執(zhí)行過程中是不是阻塞主線程的。

          但是要注意2點:

          • fork子進程的過程中,redis主線程會拷貝一份內存頁表(記錄了虛擬內存和物理內存的映射關系)給子進程,這個過程是阻塞的,redis主線程內存越大,阻塞時間越長;
          • 子進程和redis主線程共用一塊兒物理內存,如果新的請求到來,必須使用copy on write的方式,拷貝要修改的數(shù)據(jù)頁到新的內存空間進行修改。如下圖:

          注意:如果開啟了內存大頁,每次拷貝都需要分配2MB的內存。

          5 redis高可用

          下圖是一個一主二從三哨兵的架構圖:

          從圖我們可以看到哨兵之間、哨兵和主從節(jié)點之間、哨兵和客戶端之間都建立了連接。

          如果主節(jié)點掛了,哨兵集群需要完成主從切換,如下圖:

          下面我們依次來聊一下這4個步驟5.1~5.4。

          5.1.判斷主節(jié)點下線

          當一個哨兵監(jiān)控到主節(jié)點下線時,就會給其他哨兵發(fā)送確認命令,其他命令會根據(jù)自己的判斷回復"Y""N"

          如果有n/2 + 1以上數(shù)量的哨兵都認為主節(jié)點下線了,才會判定主節(jié)點下線。這里的n是哨兵集群的數(shù)量。

          n/2 + 1這個參數(shù)由quorum參數(shù)配置,比如有5個哨兵,這里一般配置成3。也可以配置成其他值。

          5.2.選舉新主節(jié)點

          主節(jié)點被判定下線后,哨兵集群會重新選擇新的主節(jié)點。

          5.2.1 淘汰不穩(wěn)定從節(jié)點

          根據(jù)配置參數(shù)down-after-milliseconds * 10來淘汰。

          down-after-milliseconds表示主從節(jié)點斷開時間,10表示次數(shù),如果從節(jié)點跟主節(jié)點斷開時間超過down-after-milliseconds的次數(shù)達到了10次以上,從 節(jié)點就被淘汰了。

          5.2.2 slave-priority參數(shù)

          slave-priority參數(shù)配置了從節(jié)點的優(yōu)先級,選擇從節(jié)點時哨兵會優(yōu)先選擇優(yōu)先級高的從節(jié)點。

          5.2.3 復制進度

          redis有一個記錄主從增量復制的緩存區(qū)叫repl_backlog_buffer,這是一個環(huán)形結構的緩沖區(qū),如下圖:主節(jié)點有一個寫偏移量master_repl_offset,從節(jié)點也有一個偏移量slave_repl_offset。優(yōu)先選擇slave_repl_offset最接近master_repl_offset的從節(jié)點作為新的主節(jié)點。

          所以,上圖中偏移量為114的從節(jié)點優(yōu)先被選為新的主節(jié)點。

          5.2.4 ID編號

          優(yōu)先級和參數(shù)都一樣的情況下,ID編號小的從節(jié)點優(yōu)先被選為新主節(jié)點。

          5.3.選舉哨兵leader

          第一個判斷主節(jié)點下線的哨兵節(jié)點收到其他節(jié)點的回復并確定主節(jié)點下線后,就會給其他哨兵發(fā)送命令申請成為哨兵leader。

          成為leader的條件如下:

          • 收到贊成票必須大于等quorum
          • 必須拿到半數(shù)以上的贊成票

          如果集群配置了5個哨兵,quorum的值設置為3,其中一個哨兵節(jié)點掛了,很有可能會判斷到主節(jié)點下線,但是因為選舉不出哨兵leader而不能切換。如果集群有2個哨兵,其中一個掛了,那必定選不出哨兵leader。

          下面的圖展示了哨兵一成功當選leader的過程:

          5.4.主節(jié)點切換

          選出新主節(jié)點和哨兵leader后,哨兵leader會執(zhí)行主從切換的操作。完成后會做一些事件通知

          • 通知其他哨兵新主節(jié)點地址
          • 通知所有從節(jié)點新的主節(jié)點地址,從節(jié)點收到請求主從同步
          • 通知客戶端連接新主節(jié)點

          5.5.主從切換過程中請求處理

          如果客戶端的讀請求會發(fā)送到從節(jié)點,可以正常處理。

          在客戶端收到新主節(jié)點通知前寫請求會失敗。

          客戶端可以采取一些應急措施應對主節(jié)點下線,比如緩存寫請求。

          為了能夠及時獲取到新主節(jié)點信息,客戶端可以訂閱哨兵的主節(jié)點下線事件和新主節(jié)點變更事件。

          6 redis為什么變慢了

          redis變慢了的原因有很多,總結一下有11個,見下圖:

          從圖中看出,redis變慢原因主要有兩類:阻塞主線程和操作系統(tǒng)限制

          6.1主線程阻塞

          6.1.1.AOF重寫和RDB快照

          前面已經(jīng)講過了,redisAOF重寫時,主線程會fork出一個bgrewriteaof子進程。

          redis進行RDB快照時主線程會fork出一個bgsave子進程。

          這兩個操作表面上看不阻塞主線程,但fork子進程的這個過程是在主線程完成的。fork子進程時redis需要拷貝內存頁表,如果redis實例很大,這個拷貝會耗費大量的CPU資源,阻塞主線程的時間也會變長。

          6.1.2.內存大頁

          redis默認支持內存大頁是2MB,使用內存大頁,一定程度上可以減少redis的內存分配次數(shù),但是對數(shù)據(jù)持久化會有一定影響。

          redisAOF重寫和RDB快照過程中,如果主線程收到新的寫請求,就需要CopyOnWrite。使用了內存大頁,即使redis只修改其中一個大小是1kbkey,也需要拷貝一整頁的數(shù)據(jù),即2MB。在寫入量較多時,大量拷貝就會導致redis性能下降。

          6.1.3.命令復雜度高

          執(zhí)行復雜度高的命令是造成redis阻塞的常見原因。比如對一個set或者list數(shù)據(jù)類型執(zhí)行SORT操作,復雜度是O(N+M*log(M))

          6.1.4.bigkey操作

          如果一個keyvalue非常大,創(chuàng)建的時候分配內存會很耗時,刪除的時候釋放內存也很耗時。

          redis4.0以后引入了layfree機制,可以使用子進程異步刪除,從而不影響主線程執(zhí)行。用UNLINK命令替代DEL命令,就可以使用子進程異步刪除。

          redis6.0增加了配置項lazyfree-lazy-user-del,配置成yes后,del命令也可以用子進程異步刪除。

          如果lazyfree-lazy-user-del不設置為yes,那redis是否采用異步刪除,是要看刪除的時機的。對于String類型和底層采用整數(shù)數(shù)組和壓縮列表的數(shù)據(jù)類型,redis是不會采用異步刪除的。

          6.1.5.從節(jié)點全量同步

          從節(jié)點全量同步過程中,需要先清除內存中的數(shù)據(jù),然后再加載RDB文件,這個過程中是阻塞的,如果有讀請求到來,只能等到加載RDB文件完成后才能處理請求,所以響應會很慢。

          另外,如果redis實例很大,也會造成RDB文件太大,從庫加載時間長。所以盡量保持redis實例不要太大,比如單個示例限制4G,如果超出就采用切片集群。

          6.1.6.AOF同步寫盤

          appendfsync策略有3種:always、everysec、no,如果采用always,每個命令都會同步寫盤,這個過程是阻塞的,等寫盤成功后才能處理下一條命令。

          除非是嚴格不能丟數(shù)據(jù)的場景,否則盡量不要選擇always策略,推薦盡量選擇everysec策略,如果對丟失數(shù)據(jù)不敏感,可以采用no。

          6.1.7.內存達到maxmemory

          內存達到maxmemory,需要使用淘汰策略來淘汰部分key。即使采用lazyfree異步刪除,選擇key的過程也是阻塞的。

          可以選擇較快的淘汰策略,比如用隨機淘汰來替換LRULFU算法淘汰。也可以擴大切片數(shù)量來減輕淘汰key的時間消耗。

          6.2操作系統(tǒng)限制

          6.2.1.使用了swap

          使用swap的原因是操作系統(tǒng)不能給redis分配足夠大的內存,如果操作其他開啟了swap,內存數(shù)據(jù)就需要不停地跟swap換入和換出,對性能影響非常大。

          操作系統(tǒng)沒有能力分配內存的原因也可能是其他進程使用了大量的內存。

          6.2.2.網(wǎng)絡問題

          如果網(wǎng)卡負載很大,對redis性能影響會很大。這一方面有可能redis的訪問量確實很高,另一方面也可能是有其他流量大的程序占用了帶寬。

          這個最好從運維層面進行監(jiān)控。

          6.2.3.線程上下文切換

          redis雖然是單線程的,但是在多核cpu的情況下,也可能會發(fā)生上下文切換。如果主線程從一個物理核切換到了另一個物理核,那就不能使用CPU高效的一級緩存和二級緩存了。如下圖所示:為防止這種情況,可以把redis綁定到一個CPU物理核。

          6.2.4.磁盤性能低

          對于AOF同步寫盤的使用場景,如果磁盤性能低,也會影響redis的響應。可以優(yōu)先采用性能更好的SSD硬盤。

          7 設計排行榜功能

          rediszset類型保存了分數(shù)值,可以方便的實現(xiàn)排行榜的功能。

          比如要統(tǒng)計10篇文章的排行榜,可以先建立一個存放10篇文章的zset,每當有讀者閱讀一篇文章時,就用ZINCRBY命令給這篇文章的分數(shù)加1,最后可以用range命令統(tǒng)計排行榜前幾位的文章。

          8 redis實現(xiàn)分布式鎖

          8.1.redis單節(jié)點的分布式鎖

          如下圖,一個服務的部署了2個客戶端,獲取分布式鎖時一個成功,另一個就失敗了。

          redis一般使用setnx實現(xiàn)分布式鎖,命令如下:

          SETNX KEY_NAME VALUE

          設置成功返回 1,設置失敗返回 0。

          使用單節(jié)點分布式鎖存在一些問題。

          8.1.1.客戶端1獲取鎖后發(fā)生了故障

          結果鎖就不能釋放了,其他客戶端永遠獲取不到鎖。解決方法是用下面命令對key設置過期時間:

          SET key value [EX seconds] [PX milliseconds] NX

          8.1.2 客戶端2誤刪除了鎖

          解決方法是對key設置value時加入一個客戶端表示,比如在客戶端1設置key時在value前拼接一個字符串application1,刪除的時候做一下判斷。

          8.2.redis紅鎖

          redis單節(jié)點會有可靠性問題,節(jié)點故障后鎖操作就會失敗。redis為了應對單點故障的問題,設計了多節(jié)點的分布式鎖,也叫紅鎖。主要思想是客戶端跟多個redis實例請求加鎖,只有超過半數(shù)的實例加鎖成功,才認為成功獲取了分布式鎖。

          如下圖,客戶端分別跟3個實例請求加鎖,有2個實例加鎖成功,所以獲取分布式鎖成功:

          9 緩存雪崩、擊穿、穿透

          9.1.緩存雪崩

          redis做緩存時,如果同一時間大量緩存數(shù)據(jù)失效,客戶端請求會大量發(fā)送到數(shù)據(jù)庫,導致數(shù)據(jù)庫壓力激增。如下圖:應對方法主要有3個:

          • 給key設置過期時間時加一個小的隨機數(shù)
          • 限流
          • 服務降級

          9.2.緩存擊穿

          某個熱點key,突然過期了,大量請求發(fā)送到了數(shù)據(jù)庫。解決方案是給熱點key不設置過期時間。

          9.3.緩存穿透

          某個熱點key,查詢緩存和查詢數(shù)據(jù)庫都沒有,就發(fā)生了緩存穿透。如下圖:

          應對方法主要有2個:

          • 緩存熱點的空值和缺省值
          • 查詢數(shù)據(jù)庫之前先查詢布隆過濾器

          10 數(shù)據(jù)傾斜

          什么是數(shù)據(jù)傾斜?看下面這個面試題:

          如果redis有一個熱點key,qps能達到100w,該如何存儲?

          如果這個熱點key被放到一個redis實例上,這個實例面臨的訪問壓力會非常大。如下圖,redis3這個實例保存了foo這個熱點key,訪問壓力會很大:解決方法主要有兩個:

          1.使用客戶端本地緩存來緩存key,這樣改造會有兩個問題:

          • 客戶端緩存的熱點key太多消耗大量內存
          • 客戶端需要保證本地緩存和redis緩存的一致性

          2.給熱點key加一個隨機前綴,讓它保存到不同的redis實例上,這樣也會存在兩個問題:

          • 客戶端在訪問的時候需要給這個key加前綴
          • 客戶端在刪除的時候需要根據(jù)所有前綴來刪除不同實例上保存的這個key

          11 bitmap使用

          有一道經(jīng)典的面試題,10億整數(shù)怎么在內存中去重排序?

          我們先算一下10億整數(shù)占的內存,java一個整數(shù)類型占四字節(jié),占用內存大小約

          10億 * 4 / 1024 / 1024 = 3.7G

          占得內存太大了,如果內存不夠,怎么辦呢?

          11.1.bitmap介紹

          bitmap類型使用的數(shù)據(jù)結構是String,底層存儲格式是二進制的bit數(shù)組。假如我們有1、4、6、9四個數(shù),保存在bit數(shù)組中如下圖:在這個bit數(shù)組中用10bit的空間保存了四個整數(shù),占用空間非常小。

          再回到面試題,我們使用bit數(shù)組長度是10億整數(shù)中 (最大值 - 最小值 + 1)。

          如果有負數(shù),需要進行一個轉化,所有數(shù)字加最小負數(shù)的絕對值。比如{-2, 0, 1, 3},我們轉換成{0, 2, 3, 5},因為數(shù)組下標必須從0開始

          11.2.使用場景

          11.2.1.員工打卡記錄

          在一個有100個員工的公司,要統(tǒng)計一個月內員工全勤的人數(shù),可以每天創(chuàng)建一個bitmap,簽到的員工bit位置為1。

          要統(tǒng)計當天簽到的員工只要用BITCOUNT命令就可以。

          要統(tǒng)計當月全勤的員工,只要對當月每天的bitmap做交集運算就可以,命令如下:

          BITOP AND srckey1 srckey2 srckey3 ... srckey30

          srckeyN表示第N天的打卡記錄bitmap

          11.2.2.統(tǒng)計網(wǎng)站日活躍用戶

          比如網(wǎng)站有10萬個用戶,這樣我們創(chuàng)建一個長度為10萬的bitmap,每個用戶id占一個位,如果用戶登錄,就把bit位置為1,日終的時候用BITCOUNT命令統(tǒng)計出當天登錄過的用戶總數(shù)。


          往期精選

          呵,Semaphore ,就這?

          Java 基礎面試題總結

          二維碼掃碼登錄的背后...

          25 張圖演示紅黑樹

          萬字總結,體系化帶你全面認識 Nginx !

          另外,cxuan 肝了六本 PDF,公號回復 cxuan ,領取作者全部 PDF 。

          瀏覽 21
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜夜操,天天操 | 狼友视频网 | 成人毛片18女人免费 | 天天爽夜夜爽夜夜爽 | 操逼的操逼的操逼的操逼的 |