Redis 連環(huán)五十二問!三萬字+八十圖詳解!
這篇我們來搞定Redis——不會有人假期玩去了吧?不會吧?
基礎(chǔ)
1.說說什么是Redis?

Redis是一種基于鍵值對(key-value)的NoSQL數(shù)據(jù)庫。
比一般鍵值對數(shù)據(jù)庫強大的地方,Redis中的value支持string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位圖)、 HyperLogLog、GEO(地理信息定位)等多種數(shù)據(jù)結(jié)構(gòu),因此 Redis可以滿足很多的應(yīng)用場景。
而且因為Redis會將所有數(shù)據(jù)都存放在內(nèi)存中,所以它的讀寫性能非常出色。
不僅如此,Redis還可以將內(nèi)存的數(shù)據(jù)利用快照和日志的形式保存到硬盤上,這樣在發(fā)生類似斷電或者機器故障的時候,內(nèi)存中的數(shù)據(jù)不會“丟失”。
除了上述功能以外,Redis還提供了鍵過期、發(fā)布訂閱、事務(wù)、流水線、Lua腳本等附加功能。
總之,Redis是一款強大的性能利器。
2.Redis可以用來干什么?

緩存
這是Redis應(yīng)用最廣泛地方,基本所有的Web應(yīng)用都會使用Redis作為緩存,來降低數(shù)據(jù)源壓力,提高響應(yīng)速度。

計數(shù)器 Redis天然支持計數(shù)功能,而且計數(shù)性能非常好,可以用來記錄瀏覽量、點贊量等等。
排行榜 Redis提供了列表和有序集合數(shù)據(jù)結(jié)構(gòu),合理地使用這些數(shù)據(jù)結(jié)構(gòu)可以很方便地構(gòu)建各種排行榜系統(tǒng)。
社交網(wǎng)絡(luò) 贊/踩、粉絲、共同好友/喜好、推送、下拉刷新。
消息隊列 Redis提供了發(fā)布訂閱功能和阻塞隊列的功能,可以滿足一般消息隊列功能。
分布式鎖 分布式環(huán)境下,利用Redis實現(xiàn)分布式鎖,也是Redis常見的應(yīng)用。
Redis的應(yīng)用一般會結(jié)合項目去問,以一個電商項目的用戶服務(wù)為例:
Token存儲:用戶登錄成功之后,使用Redis存儲Token 登錄失敗次數(shù)計數(shù):使用Redis計數(shù),登錄失敗超過一定次數(shù),鎖定賬號 地址緩存:對省市區(qū)數(shù)據(jù)的緩存 分布式鎖:分布式環(huán)境下登錄、注冊等操作加分布式鎖 ……
3.Redis 有哪些數(shù)據(jù)結(jié)構(gòu)?
Redis有五種基本數(shù)據(jù)結(jié)構(gòu)。
string
字符串最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)。字符串類型的值實際可以是字符串(簡單的字符串、復雜的字符串(例如JSON、XML))、數(shù)字 (整數(shù)、浮點數(shù)),甚至是二進制(圖片、音頻、視頻),但是值最大不能超過512MB。
字符串主要有以下幾個典型使用場景:
緩存功能 計數(shù) 共享Session 限速
hash
哈希類型是指鍵值本身又是一個鍵值對結(jié)構(gòu)。
哈希主要有以下典型應(yīng)用場景:
緩存用戶信息 緩存對象
list
列表(list)類型是用來存儲多個有序的字符串。列表是一種比較靈活的數(shù)據(jù)結(jié)構(gòu),它可以充當棧和隊列的角色
列表主要有以下幾種使用場景:
消息隊列 文章列表
set
集合(set)類型也是用來保存多個的字符串元素,但和列表類型不一 樣的是,集合中不允許有重復元素,并且集合中的元素是無序的。
集合主要有如下使用場景:
標簽(tag) 共同關(guān)注
sorted set
有序集合中的元素可以排序。但是它和列表使用索引下標作為排序依據(jù)不同的是,它給每個元素設(shè)置一個權(quán)重(score)作為排序的依據(jù)。
有序集合主要應(yīng)用場景:
用戶點贊統(tǒng)計 用戶排序
4.Redis為什么快呢?
Redis的速度?常的快,單機的Redis就可以?撐每秒十幾萬的并發(fā),相對于MySQL來說,性能是MySQL的??倍。速度快的原因主要有?點:
完全基于內(nèi)存操作 使?單線程,避免了線程切換和競態(tài)產(chǎn)生的消耗 基于?阻塞的IO多路復?機制 C語?實現(xiàn),優(yōu)化過的數(shù)據(jù)結(jié)構(gòu),基于?種基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),redis做了?量的優(yōu)化,性能極? 
5.能說一下I/O多路復用嗎?
引用知乎上一個高贊的回答來解釋什么是I/O多路復用。假設(shè)你是一個老師,讓30個學生解答一道題目,然后檢查學生做的是否正確,你有下面幾個選擇:
第一種選擇:按順序逐個檢查,先檢查A,然后是B,之后是C、D。。。這中間如果有一個學生卡住,全班都會被耽誤。這種模式就好比,你用循環(huán)挨個處理socket,根本不具有并發(fā)能力。
第二種選擇:你創(chuàng)建30個分身,每個分身檢查一個學生的答案是否正確。這種類似于為每一個用戶創(chuàng)建一個進程或者- 線程處理連接。
第三種選擇,你站在講臺上等,誰解答完誰舉手。這時C、D舉手,表示他們解答問題完畢,你下去依次檢查C、D的答案,然后繼續(xù)回到講臺上等。此時E、A又舉手,然后去處理E和A。
第一種就是阻塞IO模型,第三種就是I/O復用模型。

Linux系統(tǒng)有三種方式實現(xiàn)IO多路復用:select、poll和epoll。
例如epoll方式是將用戶socket對應(yīng)的fd注冊進epoll,然后epoll幫你監(jiān)聽哪些socket上有消息到達,這樣就避免了大量的無用操作。此時的socket應(yīng)該采用非阻塞模式。
這樣,整個過程只在進行select、poll、epoll這些調(diào)用的時候才會阻塞,收發(fā)客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,這就是事件驅(qū)動,所謂的reactor模式。
6. Redis為什么早期選擇單線程?
官方解釋:https://redis.io/topics/faq
官方FAQ表示,因為Redis是基于內(nèi)存的操作,CPU成為Redis的瓶頸的情況很少見,Redis的瓶頸最有可能是內(nèi)存的大小或者網(wǎng)絡(luò)限制。
如果想要最大程度利用CPU,可以在一臺機器上啟動多個Redis實例。
PS:網(wǎng)上有這樣的回答,吐槽官方的解釋有些敷衍,其實就是歷史原因,開發(fā)者嫌多線程麻煩,后來這個CPU的利用問題就被拋給了使用者。
同時FAQ里還提到了, Redis 4.0 之后開始變成多線程,除了主線程外,它也有后臺線程在處理一些較為緩慢的操作,例如清理臟數(shù)據(jù)、無用連接的釋放、大 Key 的刪除等等。
7.Redis6.0使用多線程是怎么回事?
Redis不是說用單線程的嗎?怎么6.0成了多線程的?
Redis6.0的多線程是用多線程來處理數(shù)據(jù)的讀寫和協(xié)議解析,但是Redis執(zhí)行命令還是單線程的。
這樣做的?的是因為Redis的性能瓶頸在于?絡(luò)IO??CPU,使?多線程能提升IO讀寫的效率,從?整體提?Redis的性能。
持久化
8.Redis持久化?式有哪些?有什么區(qū)別?
Redis持久化?案分為RDB和AOF兩種。
RDB
RDB持久化是把當前進程數(shù)據(jù)生成快照保存到硬盤的過程,觸發(fā)RDB持久化過程分為手動觸發(fā)和自動觸發(fā)。
RDB?件是?個壓縮的?進制?件,通過它可以還原某個時刻數(shù)據(jù)庫的狀態(tài)。由于RDB?件是保存在硬盤上的,所以即使Redis崩潰或者退出,只要RDB?件存在,就可以?它來恢復還原數(shù)據(jù)庫的狀態(tài)。
手動觸發(fā)分別對應(yīng)save和bgsave命令:
save命令:阻塞當前Redis服務(wù)器,直到RDB過程完成為止,對于內(nèi)存比較大的實例會造成長時間阻塞,線上環(huán)境不建議使用。
bgsave命令:Redis進程執(zhí)行fork操作創(chuàng)建子進程,RDB持久化過程由子進程負責,完成后自動結(jié)束。阻塞只發(fā)生在fork階段,一般時間很短。
以下場景會自動觸發(fā)RDB持久化:
使用save相關(guān)配置,如“save m n”。表示m秒內(nèi)數(shù)據(jù)集存在n次修改時,自動觸發(fā)bgsave。 如果從節(jié)點執(zhí)行全量復制操作,主節(jié)點自動執(zhí)行bgsave生成RDB文件并發(fā)送給從節(jié)點 執(zhí)行debug reload命令重新加載Redis時,也會自動觸發(fā)save操作 默認情況下執(zhí)行shutdown命令時,如果沒有開啟AOF持久化功能則自動執(zhí)行bgsave。
AOF
AOF(append only file)持久化:以獨立日志的方式記錄每次寫命令, 重啟時再重新執(zhí)行AOF文件中的命令達到恢復數(shù)據(jù)的目的。AOF的主要作用是解決了數(shù)據(jù)持久化的實時性,目前已經(jīng)是Redis持久化的主流方式。
AOF的工作流程操作:命令寫入 (append)、文件同步(sync)、文件重寫(rewrite)、重啟加載 (load)
流程如下:
1)所有的寫入命令會追加到aof_buf(緩沖區(qū))中。
2)AOF緩沖區(qū)根據(jù)對應(yīng)的策略向硬盤做同步操作。
3)隨著AOF文件越來越大,需要定期對AOF文件進行重寫,達到壓縮 的目的。
4)當Redis服務(wù)器重啟時,可以加載AOF文件進行數(shù)據(jù)恢復。
9.RDB 和 AOF 各自有什么優(yōu)缺點?
RDB | 優(yōu)點
只有一個緊湊的二進制文件 dump.rdb,非常適合備份、全量復制的場景。容災(zāi)性好,可以把RDB文件拷貝道遠程機器或者文件系統(tǒng)張,用于容災(zāi)恢復。 恢復速度快,RDB恢復數(shù)據(jù)的速度遠遠快于AOF的方式
RDB | 缺點
實時性低,RDB 是間隔一段時間進行持久化,沒法做到實時持久化/秒級持久化。如果在這一間隔事件發(fā)生故障,數(shù)據(jù)會丟失。 存在兼容問題,Redis演進過程存在多個格式的RDB版本,存在老版本Redis無法兼容新版本RDB的問題。
AOF | 優(yōu)點
實時性好,aof 持久化可以配置 appendfsync屬性,有always,每進行一次命令操作就記錄到 aof 文件中一次。通過 append 模式寫文件,即使中途服務(wù)器宕機,可以通過 redis-check-aof 工具解決數(shù)據(jù)一致性問題。
AOF | 缺點
AOF 文件比 RDB 文件大,且 恢復速度慢。 數(shù)據(jù)集大 的時候,比 RDB 啟動效率低。
10.RDB和AOF如何選擇?
一般來說, 如果想達到足以媲美數(shù)據(jù)庫的 數(shù)據(jù)安全性,應(yīng)該 同時使用兩種持久化功能。在這種情況下,當 Redis 重啟的時候會優(yōu)先載入 AOF 文件來恢復原始的數(shù)據(jù),因為在通常情況下 AOF 文件保存的數(shù)據(jù)集要比 RDB 文件保存的數(shù)據(jù)集要完整。 如果 可以接受數(shù)分鐘以內(nèi)的數(shù)據(jù)丟失,那么可以 只使用 RDB 持久化。 有很多用戶都只使用 AOF 持久化,但并不推薦這種方式,因為定時生成 RDB 快照(snapshot)非常便于進行數(shù)據(jù)備份, 并且 RDB 恢復數(shù)據(jù)集的速度也要比 AOF 恢復的速度要快,除此之外,使用 RDB 還可以避免 AOF 程序的 bug。 如果只需要數(shù)據(jù)在服務(wù)器運行的時候存在,也可以不使用任何持久化方式。
11.Redis的數(shù)據(jù)恢復?
當Redis發(fā)生了故障,可以從RDB或者AOF中恢復數(shù)據(jù)。
恢復的過程也很簡單,把RDB或者AOF文件拷貝到Redis的數(shù)據(jù)目錄下,如果使用AOF恢復,配置文件開啟AOF,然后啟動redis-server即可。
Redis 啟動時加載數(shù)據(jù)的流程:
AOF持久化開啟且存在AOF文件時,優(yōu)先加載AOF文件。 AOF關(guān)閉或者AOF文件不存在時,加載RDB文件。 加載AOF/RDB文件成功后,Redis啟動成功。 AOF/RDB文件存在錯誤時,Redis啟動失敗并打印錯誤信息。
12.Redis 4.0 的混合持久化了解嗎?
重啟 Redis 時,我們很少使用 RDB 來恢復內(nèi)存狀態(tài),因為會丟失大量數(shù)據(jù)。我們通常使用 AOF 日志重放,但是重放 AOF 日志性能相對 RDB 來說要慢很多,這樣在 Redis 實例很大的情況下,啟動需要花費很長的時間。
Redis 4.0 為了解決這個問題,帶來了一個新的持久化選項——混合持久化。將 rdb 文件的內(nèi)容和增量的 AOF 日志文件存在一起。這里的 AOF 日志不再是全量的日志,而是 自持久化開始到持久化結(jié)束 的這段時間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很?。?img class="rich_pages wxw-img" data-ratio="0.4924698795180723" src="https://filescdn.proginn.com/ff4fda5046deb3718886a6bd1856e71f/f497dfe2e431fda6a37cbc24d571b75f.webp" data-type="png" data-w="664" style="display: block;margin-right: auto;margin-left: auto;border-radius: 4px;margin-bottom: 25px;">
于是在 Redis 重啟的時候,可以先加載 rdb 的內(nèi)容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。
高可用
Redis保證高可用主要有三種方式:主從、哨兵、集群。
13.主從復制了解嗎?

主從復制,是指將一臺 Redis 服務(wù)器的數(shù)據(jù),復制到其他的 Redis 服務(wù)器。前者稱為 **主節(jié)點(master)**,后者稱為 **從節(jié)點(slave)**。且數(shù)據(jù)的復制是 單向 的,只能由主節(jié)點到從節(jié)點。Redis 主從復制支持 主從同步 和 從從同步 兩種,后者是 Redis 后續(xù)版本新增的功能,以減輕主節(jié)點的同步負擔。
主從復制主要的作用?
數(shù)據(jù)冗余: 主從復制實現(xiàn)了數(shù)據(jù)的熱備份,是持久化之外的一種數(shù)據(jù)冗余方式。 故障恢復: 當主節(jié)點出現(xiàn)問題時,可以由從節(jié)點提供服務(wù),實現(xiàn)快速的故障恢復 *(實際上是一種服務(wù)的冗余)*。 負載均衡: 在主從復制的基礎(chǔ)上,配合讀寫分離,可以由主節(jié)點提供寫服務(wù),由從節(jié)點提供讀服務(wù) (即寫 Redis 數(shù)據(jù)時應(yīng)用連接主節(jié)點,讀 Redis 數(shù)據(jù)時應(yīng)用連接從節(jié)點),分擔服務(wù)器負載。尤其是在寫少讀多的場景下,通過多個從節(jié)點分擔讀負載,可以大大提高 Redis 服務(wù)器的并發(fā)量。 高可用基石: 除了上述作用以外,主從復制還是哨兵和集群能夠?qū)嵤┑?基礎(chǔ),因此說主從復制是 Redis 高可用的基礎(chǔ)。
14.Redis主從有幾種常見的拓撲結(jié)構(gòu)?
Redis的復制拓撲結(jié)構(gòu)可以支持單層或多層復制關(guān)系,根據(jù)拓撲復雜性可以分為以下三種:一主一從、一主多從、樹狀主從結(jié)構(gòu)。
1.一主一從結(jié)構(gòu)
一主一從結(jié)構(gòu)是最簡單的復制拓撲結(jié)構(gòu),用于主節(jié)點出現(xiàn)宕機時從節(jié)點提供故障轉(zhuǎn)移支持。
2.一主多從結(jié)構(gòu)
一主多從結(jié)構(gòu)(又稱為星形拓撲結(jié)構(gòu))使得應(yīng)用端可以利用多個從節(jié)點實現(xiàn)讀寫分離(見圖6-5)。對于讀占比較大的場景,可以把讀命令發(fā)送到從節(jié)點來分擔主節(jié)點壓力。
3.樹狀主從結(jié)構(gòu)
樹狀主從結(jié)構(gòu)(又稱為樹狀拓撲結(jié)構(gòu))使得從節(jié)點不但可以復制主節(jié)點數(shù)據(jù),同時可以作為其他從節(jié)點的主節(jié)點繼續(xù)向下層復制。通過引入復制中間層,可以有效降低主節(jié)點負載和需要傳送給從節(jié)點的數(shù)據(jù)量。
15.Redis的主從復制原理了解嗎?
Redis主從復制的工作流程大概可以分為如下幾步:
保存主節(jié)點(master)信息 這一步只是保存主節(jié)點信息,保存主節(jié)點的ip和port。 主從建立連接 從節(jié)點(slave)發(fā)現(xiàn)新的主節(jié)點后,會嘗試和主節(jié)點建立網(wǎng)絡(luò)連接。 發(fā)送ping命令 連接建立成功后從節(jié)點發(fā)送ping請求進行首次通信,主要是檢測主從之間網(wǎng)絡(luò)套接字是否可用、主節(jié)點當前是否可接受處理命令。 權(quán)限驗證 如果主節(jié)點要求密碼驗證,從節(jié)點必須正確的密碼才能通過驗證。 同步數(shù)據(jù)集 主從復制連接正常通信后,主節(jié)點會把持有的數(shù)據(jù)全部發(fā)送給從節(jié)點。 命令持續(xù)復制 接下來主節(jié)點會持續(xù)地把寫命令發(fā)送給從節(jié)點,保證主從數(shù)據(jù)一致性。
16.說說主從數(shù)據(jù)同步的方式?
Redis在2.8及以上版本使用psync命令完成主從數(shù)據(jù)同步,同步過程分為:全量復制和部分復制。

全量復制一般用于初次復制場景,Redis早期支持的復制功能只有全量復制,它會把主節(jié)點全部數(shù)據(jù)一次性發(fā)送給從節(jié)點,當數(shù)據(jù)量較大時,會對主從節(jié)點和網(wǎng)絡(luò)造成很大的開銷。
全量復制的完整運行流程如下:
發(fā)送psync命令進行數(shù)據(jù)同步,由于是第一次進行復制,從節(jié)點沒有復制偏移量和主節(jié)點的運行ID,所以發(fā)送psync-1。 主節(jié)點根據(jù)psync-1解析出當前為全量復制,回復+FULLRESYNC響應(yīng)。 從節(jié)點接收主節(jié)點的響應(yīng)數(shù)據(jù)保存運行ID和偏移量offset 主節(jié)點執(zhí)行bgsave保存RDB文件到本地 主節(jié)點發(fā)送RDB文件給從節(jié)點,從節(jié)點把接收的RDB文件保存在本地并直接作為從節(jié)點的數(shù)據(jù)文件 對于從節(jié)點開始接收RDB快照到接收完成期間,主節(jié)點仍然響應(yīng)讀寫命令,因此主節(jié)點會把這期間寫命令數(shù)據(jù)保存在復制客戶端緩沖區(qū)內(nèi),當從節(jié)點加載完RDB文件后,主節(jié)點再把緩沖區(qū)內(nèi)的數(shù)據(jù)發(fā)送給從節(jié)點,保證主從之間數(shù)據(jù)一致性。 從節(jié)點接收完主節(jié)點傳送來的全部數(shù)據(jù)后會清空自身舊數(shù)據(jù) 從節(jié)點清空數(shù)據(jù)后開始加載RDB文件 從節(jié)點成功加載完RDB后,如果當前節(jié)點開啟了AOF持久化功能, 它會立刻做bgrewriteaof操作,為了保證全量復制后AOF持久化文件立刻可用。
部分復制部分復制主要是Redis針對全量復制的過高開銷做出的一種優(yōu)化措施, 使用psync{runId}{offset}命令實現(xiàn)。當從節(jié)點(slave)正在復制主節(jié)點 (master)時,如果出現(xiàn)網(wǎng)絡(luò)閃斷或者命令丟失等異常情況時,從節(jié)點會向 主節(jié)點要求補發(fā)丟失的命令數(shù)據(jù),如果主節(jié)點的復制積壓緩沖區(qū)內(nèi)存在這部分數(shù)據(jù)則直接發(fā)送給從節(jié)點,這樣就可以保持主從節(jié)點復制的一致性。
當主從節(jié)點之間網(wǎng)絡(luò)出現(xiàn)中斷時,如果超過repl-timeout時間,主節(jié)點會認為從節(jié)點故障并中斷復制連接 主從連接中斷期間主節(jié)點依然響應(yīng)命令,但因復制連接中斷命令無法發(fā)送給從節(jié)點,不過主節(jié)點內(nèi)部存在的復制積壓緩沖區(qū),依然可以保存最近一段時間的寫命令數(shù)據(jù),默認最大緩存1MB。 當主從節(jié)點網(wǎng)絡(luò)恢復后,從節(jié)點會再次連上主節(jié)點 當主從連接恢復后,由于從節(jié)點之前保存了自身已復制的偏移量和主節(jié)點的運行ID。因此會把它們當作psync參數(shù)發(fā)送給主節(jié)點,要求進行部分復制操作。 主節(jié)點接到psync命令后首先核對參數(shù)runId是否與自身一致,如果一 致,說明之前復制的是當前主節(jié)點;之后根據(jù)參數(shù)offset在自身復制積壓緩沖區(qū)查找,如果偏移量之后的數(shù)據(jù)存在緩沖區(qū)中,則對從節(jié)點發(fā)送+CONTINUE響應(yīng),表示可以進行部分復制。 主節(jié)點根據(jù)偏移量把復制積壓緩沖區(qū)里的數(shù)據(jù)發(fā)送給從節(jié)點,保證主從復制進入正常狀態(tài)。
17.主從復制存在哪些問題呢?
主從復制雖好,但也存在一些問題:
一旦主節(jié)點出現(xiàn)故障,需要手動將一個從節(jié)點晉升為主節(jié)點,同時需要修改應(yīng)用方的主節(jié)點地址,還需要命令其他從節(jié)點去復制新的主節(jié)點,整個過程都需要人工干預(yù)。 主節(jié)點的寫能力受到單機的限制。 主節(jié)點的存儲能力受到單機的限制。
第一個問題是Redis的高可用問題,第二、三個問題屬于Redis的分布式問題。
18.Redis Sentinel(哨兵)了解嗎?
主從復制存在一個問題,沒法完成自動故障轉(zhuǎn)移。所以我們需要一個方案來完成自動故障轉(zhuǎn)移,它就是Redis Sentinel(哨兵)。

Redis Sentinel ,它由兩部分組成,哨兵節(jié)點和數(shù)據(jù)節(jié)點:
哨兵節(jié)點: 哨兵系統(tǒng)由一個或多個哨兵節(jié)點組成,哨兵節(jié)點是特殊的 Redis 節(jié)點,不存儲數(shù)據(jù),對數(shù)據(jù)節(jié)點進行監(jiān)控。 數(shù)據(jù)節(jié)點: 主節(jié)點和從節(jié)點都是數(shù)據(jù)節(jié)點;
在復制的基礎(chǔ)上,哨兵實現(xiàn)了 自動化的故障恢復 功能,下面是官方對于哨兵功能的描述:
監(jiān)控(Monitoring): 哨兵會不斷地檢查主節(jié)點和從節(jié)點是否運作正常。 自動故障轉(zhuǎn)移(Automatic failover): 當 主節(jié)點 不能正常工作時,哨兵會開始 自動故障轉(zhuǎn)移操作,它會將失效主節(jié)點的其中一個 從節(jié)點升級為新的主節(jié)點,并讓其他從節(jié)點改為復制新的主節(jié)點。 配置提供者(Configuration provider): 客戶端在初始化時,通過連接哨兵來獲得當前 Redis 服務(wù)的主節(jié)點地址。 通知(Notification): 哨兵可以將故障轉(zhuǎn)移的結(jié)果發(fā)送給客戶端。
其中,監(jiān)控和自動故障轉(zhuǎn)移功能,使得哨兵可以及時發(fā)現(xiàn)主節(jié)點故障并完成轉(zhuǎn)移。而配置提供者和通知功能,則需要在與客戶端的交互中才能體現(xiàn)。
19.Redis Sentinel(哨兵)實現(xiàn)原理知道嗎?
哨兵模式是通過哨兵節(jié)點完成對數(shù)據(jù)節(jié)點的監(jiān)控、下線、故障轉(zhuǎn)移。
定時監(jiān)控
Redis Sentinel通過三個定時監(jiān)控任務(wù)完成對各個節(jié)點發(fā)現(xiàn)和監(jiān)控:每隔10秒,每個Sentinel節(jié)點會向主節(jié)點和從節(jié)點發(fā)送info命令獲取最新的拓撲結(jié)構(gòu) 每隔2秒,每個Sentinel節(jié)點會向Redis數(shù)據(jù)節(jié)點的__sentinel__:hello 頻道上發(fā)送該Sentinel節(jié)點對于主節(jié)點的判斷以及當前Sentinel節(jié)點的信息 每隔1秒,每個Sentinel節(jié)點會向主節(jié)點、從節(jié)點、其余Sentinel節(jié)點發(fā)送一條ping命令做一次心跳檢測,來確認這些節(jié)點當前是否可達 主觀下線和客觀下線主觀下線就是哨兵節(jié)點認為某個節(jié)點有問題,客觀下線就是超過一定數(shù)量的哨兵節(jié)點認為主節(jié)點有問題。 
主觀下線 每個Sentinel節(jié)點會每隔1秒對主節(jié)點、從節(jié)點、其他Sentinel節(jié)點發(fā)送ping命令做心跳檢測,當這些節(jié)點超過 down-after-milliseconds沒有進行有效回復,Sentinel節(jié)點就會對該節(jié)點做失敗判定,這個行為叫做主觀下線。
客觀下線 當Sentinel主觀下線的節(jié)點是主節(jié)點時,該Sentinel節(jié)點會通過sentinel is- master-down-by-addr命令向其他Sentinel節(jié)點詢問對主節(jié)點的判斷,當超過
個數(shù),Sentinel節(jié)點認為主節(jié)點確實有問題,這時該Sentinel節(jié)點會做出客觀下線的決定
領(lǐng)導者Sentinel節(jié)點選舉Sentinel節(jié)點之間會做一個領(lǐng)導者選舉的工作,選出一個Sentinel節(jié)點作為領(lǐng)導者進行故障轉(zhuǎn)移的工作。Redis使用了Raft算法實現(xiàn)領(lǐng)導者選舉。
故障轉(zhuǎn)移
領(lǐng)導者選舉出的Sentinel節(jié)點負責故障轉(zhuǎn)移,過程如下:

在從節(jié)點列表中選出一個節(jié)點作為新的主節(jié)點,這一步是相對復雜一些的一步 Sentinel領(lǐng)導者節(jié)點會對第一步選出來的從節(jié)點執(zhí)行slaveof no one命令讓其成為主節(jié)點 Sentinel領(lǐng)導者節(jié)點會向剩余的從節(jié)點發(fā)送命令,讓它們成為新主節(jié)點的從節(jié)點 Sentinel節(jié)點集合會將原來的主節(jié)點更新為從節(jié)點,并保持著對其關(guān)注,當其恢復后命令它去復制新的主節(jié)點
20.領(lǐng)導者Sentinel節(jié)點選舉了解嗎?
Redis使用了Raft算法實 現(xiàn)領(lǐng)導者選舉,大致流程如下:
每個在線的Sentinel節(jié)點都有資格成為領(lǐng)導者,當它確認主節(jié)點主觀 下線時候,會向其他Sentinel節(jié)點發(fā)送sentinel is-master-down-by-addr命令, 要求將自己設(shè)置為領(lǐng)導者。 收到命令的Sentinel節(jié)點,如果沒有同意過其他Sentinel節(jié)點的sentinel is-master-down-by-addr命令,將同意該請求,否則拒絕。 如果該Sentinel節(jié)點發(fā)現(xiàn)自己的票數(shù)已經(jīng)大于等于max(quorum, num(sentinels)/2+1),那么它將成為領(lǐng)導者。 如果此過程沒有選舉出領(lǐng)導者,將進入下一次選舉。
21.新的主節(jié)點是怎樣被挑選出來的?
選出新的主節(jié)點,大概分為這么幾步:
過濾:“不健康”(主觀下線、斷線)、5秒內(nèi)沒有回復過Sentinel節(jié) 點ping響應(yīng)、與主節(jié)點失聯(lián)超過down-after-milliseconds*10秒。 選擇slave-priority(從節(jié)點優(yōu)先級)最高的從節(jié)點列表,如果存在則返回,不存在則繼續(xù)。 選擇復制偏移量最大的從節(jié)點(復制的最完整),如果存在則返 回,不存在則繼續(xù)。 選擇runid最小的從節(jié)點。
22.Redis 集群了解嗎?
前面說到了主從存在高可用和分布式的問題,哨兵解決了高可用的問題,而集群就是終極方案,一舉解決高可用和分布式問題。
數(shù)據(jù)分區(qū): 數(shù)據(jù)分區(qū) (或稱數(shù)據(jù)分片) 是集群最核心的功能。集群將數(shù)據(jù)分散到多個節(jié)點,一方面 突破了 Redis 單機內(nèi)存大小的限制,存儲容量大大增加;另一方面 每個主節(jié)點都可以對外提供讀服務(wù)和寫服務(wù),大大提高了集群的響應(yīng)能力。
高可用: 集群支持主從復制和主節(jié)點的 自動故障轉(zhuǎn)移 (與哨兵類似),當任一節(jié)點發(fā)生故障時,集群仍然可以對外提供服務(wù)。
23.集群中數(shù)據(jù)如何分區(qū)?
分布式的存儲中,要把數(shù)據(jù)集按照分區(qū)規(guī)則映射到多個節(jié)點,常見的數(shù)據(jù)分區(qū)規(guī)則三種:
方案一:節(jié)點取余分區(qū)
節(jié)點取余分區(qū),非常好理解,使用特定的數(shù)據(jù),比如Redis的鍵,或者用戶ID之類,對響應(yīng)的hash值取余:hash(key)%N,來確定數(shù)據(jù)映射到哪一個節(jié)點上。
不過該方案最大的問題是,當節(jié)點數(shù)量變化時,如擴容或收縮節(jié)點,數(shù)據(jù)節(jié)點映射關(guān) 系需要重新計算,會導致數(shù)據(jù)的重新遷移。

方案二:一致性哈希分區(qū)
將整個 Hash 值空間組織成一個虛擬的圓環(huán),然后將緩存節(jié)點的 IP 地址或者主機名做 Hash 取值后,放置在這個圓環(huán)上。當我們需要確定某一個 Key 需 要存取到哪個節(jié)點上的時候,先對這個 Key 做同樣的 Hash 取值,確定在環(huán)上的位置,然后按照順時針方向在環(huán)上“行走”,遇到的第一個緩存節(jié)點就是要訪問的節(jié)點。
比如說下面 這張圖里面,Key 1 和 Key 2 會落入到 Node 1 中,Key 3、Key 4 會落入到 Node 2 中,Key 5 落入到 Node 3 中,Key 6 落入到 Node 4 中。
這種方式相比節(jié)點取余最大的好處在于加入和刪除節(jié)點只影響哈希環(huán)中 相鄰的節(jié)點,對其他節(jié)點無影響。
但它還是存在問題:
緩存節(jié)點在圓環(huán)上分布不平均,會造成部分緩存節(jié)點的壓力較大 當某個節(jié)點故障時,這個節(jié)點所要承擔的所有訪問都會被順移到另一個節(jié)點上,會對后面這個節(jié)點造成力。
方案三:虛擬槽分區(qū)
這個方案 一致性哈希分區(qū)的基礎(chǔ)上,引入了 虛擬節(jié)點 的概念。Redis 集群使用的便是該方案,其中的虛擬節(jié)點稱為 槽(slot)。槽是介于數(shù)據(jù)和實際節(jié)點之間的虛擬概念,每個實際節(jié)點包含一定數(shù)量的槽,每個槽包含哈希值在一定范圍內(nèi)的數(shù)據(jù)。
在使用了槽的一致性哈希分區(qū)中,槽是數(shù)據(jù)管理和遷移的基本單位。槽解耦了數(shù)據(jù)和實際節(jié)點 之間的關(guān)系,增加或刪除節(jié)點對系統(tǒng)的影響很小。仍以上圖為例,系統(tǒng)中有 4 個實際節(jié)點,假設(shè)為其分配 16 個槽(0-15);
槽 0-3 位于 node1;4-7 位于 node2;以此類推....
如果此時刪除 node2,只需要將槽 4-7 重新分配即可,例如槽 4-5 分配給 node1,槽 6 分配給 node3,槽 7 分配給 node4,數(shù)據(jù)在其他節(jié)點的分布仍然較為均衡。
24.能說說Redis集群的原理嗎?
Redis集群通過數(shù)據(jù)分區(qū)來實現(xiàn)數(shù)據(jù)的分布式存儲,通過自動故障轉(zhuǎn)移實現(xiàn)高可用。
集群創(chuàng)建
數(shù)據(jù)分區(qū)是在集群創(chuàng)建的時候完成的。
設(shè)置節(jié)點Redis集群一般由多個節(jié)點組成,節(jié)點數(shù)量至少為6個才能保證組成完整高可用的集群。每個節(jié)點需要開啟配置cluster-enabled yes,讓Redis運行在集群模式下。
節(jié)點握手節(jié)點握手是指一批運行在集群模式下的節(jié)點通過Gossip協(xié)議彼此通信, 達到感知對方的過程。節(jié)點握手是集群彼此通信的第一步,由客戶端發(fā)起命 令:cluster meet{ip}{port}。完成節(jié)點握手之后,一個個的Redis節(jié)點就組成了一個多節(jié)點的集群。
分配槽(slot)Redis集群把所有的數(shù)據(jù)映射到16384個槽中。每個節(jié)點對應(yīng)若干個槽,只有當節(jié)點分配了槽,才能響應(yīng)和這些槽關(guān)聯(lián)的鍵命令。通過 cluster addslots命令為節(jié)點分配槽。

故障轉(zhuǎn)移
Redis集群的故障轉(zhuǎn)移和哨兵的故障轉(zhuǎn)移類似,但是Redis集群中所有的節(jié)點都要承擔狀態(tài)維護的任務(wù)。
故障發(fā)現(xiàn)Redis集群內(nèi)節(jié)點通過ping/pong消息實現(xiàn)節(jié)點通信,集群中每個節(jié)點都會定期向其他節(jié)點發(fā)送ping消息,接收節(jié)點回復pong 消息作為響應(yīng)。如果在cluster-node-timeout時間內(nèi)通信一直失敗,則發(fā)送節(jié) 點會認為接收節(jié)點存在故障,把接收節(jié)點標記為主觀下線(pfail)狀態(tài)。
當某個節(jié)點判斷另一個節(jié)點主觀下線后,相應(yīng)的節(jié)點狀態(tài)會跟隨消息在集群內(nèi)傳播。通過Gossip消息傳播,集群內(nèi)節(jié)點不斷收集到故障節(jié)點的下線報告。當 半數(shù)以上持有槽的主節(jié)點都標記某個節(jié)點是主觀下線時。觸發(fā)客觀下線流程。
故障恢復
故障節(jié)點變?yōu)榭陀^下線后,如果下線節(jié)點是持有槽的主節(jié)點則需要在它 的從節(jié)點中選出一個替換它,從而保證集群的高可用。

資格檢查 每個從節(jié)點都要檢查最后與主節(jié)點斷線時間,判斷是否有資格替換故障 的主節(jié)點。 準備選舉時間 當從節(jié)點符合故障轉(zhuǎn)移資格后,更新觸發(fā)故障選舉的時間,只有到達該 時間后才能執(zhí)行后續(xù)流程。 發(fā)起選舉 當從節(jié)點定時任務(wù)檢測到達故障選舉時間(failover_auth_time)到達后,發(fā)起選舉流程。 選舉投票 持有槽的主節(jié)點處理故障選舉消息。投票過程其實是一個領(lǐng)導者選舉的過程,如集群內(nèi)有N個持有槽的主節(jié) 點代表有N張選票。由于在每個配置紀元內(nèi)持有槽的主節(jié)點只能投票給一個 從節(jié)點,因此只能有一個從節(jié)點獲得N/2+1的選票,保證能夠找出唯一的從節(jié)點。 
替換主節(jié)點 當從節(jié)點收集到足夠的選票之后,觸發(fā)替換主節(jié)點操作。
部署Redis集群至少需要幾個物理節(jié)點?
在投票選舉的環(huán)節(jié),故障主節(jié)點也算在投票數(shù)內(nèi),假設(shè)集群內(nèi)節(jié)點規(guī)模是3主3從,其中有2 個主節(jié)點部署在一臺機器上,當這臺機器宕機時,由于從節(jié)點無法收集到 3/2+1個主節(jié)點選票將導致故障轉(zhuǎn)移失敗。這個問題也適用于故障發(fā)現(xiàn)環(huán)節(jié)。因此部署集群時所有主節(jié)點最少需要部署在3臺物理機上才能避免單點問題。
25.說說集群的伸縮?
Redis集群提供了靈活的節(jié)點擴容和收縮方案,可以在不影響集群對外服務(wù)的情況下,為集群添加節(jié)點進行擴容也可以下線部分節(jié)點進行縮容。
其實,集群擴容和縮容的關(guān)鍵點,就在于槽和節(jié)點的對應(yīng)關(guān)系,擴容和縮容就是將一部分槽和數(shù)據(jù)遷移給新節(jié)點。
例如下面一個集群,每個節(jié)點對應(yīng)若干個槽,每個槽對應(yīng)一定的數(shù)據(jù),如果希望加入1個節(jié)點希望實現(xiàn)集群擴容時,需要通過相關(guān)命令把一部分槽和內(nèi)容遷移給新節(jié)點。
縮容也是類似,先把槽和數(shù)據(jù)遷移到其它節(jié)點,再把對應(yīng)的節(jié)點下線。
緩存設(shè)計
26.什么是緩存擊穿、緩存穿透、緩存雪崩?
PS:這是多年黃歷的老八股了,一定要理解清楚。
緩存擊穿
一個并發(fā)訪問量比較大的key在某個時間過期,導致所有的請求直接打在DB上。
解決?案:
加鎖更新,?如請求查詢A,發(fā)現(xiàn)緩存中沒有,對A這個key加鎖,同時去數(shù)據(jù)庫查詢數(shù)據(jù),寫?緩存,再返回給?戶,這樣后?的請求就可以從緩存中拿到數(shù)據(jù)了。

將過期時間組合寫在value中,通過異步的?式不斷的刷新過期時間,防?此類現(xiàn)象。
緩存穿透
緩存穿透指的查詢緩存和數(shù)據(jù)庫中都不存在的數(shù)據(jù),這樣每次請求直接打到數(shù)據(jù)庫,就好像緩存不存在一樣。
緩存穿透將導致不存在的數(shù)據(jù)每次請求都要到存儲層去查詢,失去了緩存保護后端存儲的意義。
緩存穿透可能會使后端存儲負載加大,如果發(fā)現(xiàn)大量存儲層空命中,可能就是出現(xiàn)了緩存穿透問題。
緩存穿透可能有兩種原因:
自身業(yè)務(wù)代碼問題 惡意攻擊,爬蟲造成空命中
它主要有兩種解決辦法:
緩存空值/默認值
一種方式是在數(shù)據(jù)庫不命中之后,把一個空對象或者默認值保存到緩存,之后再訪問這個數(shù)據(jù),就會從緩存中獲取,這樣就保護了數(shù)據(jù)庫。

緩存空值有兩大問題:
空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的內(nèi)存空間(如果是攻擊,問題更嚴重),比較有效的方法是針對這類數(shù)據(jù)設(shè)置一個較短的過期時間,讓其自動剔除。
緩存層和存儲層的數(shù)據(jù)會有一段時間窗口的不一致,可能會對業(yè)務(wù)有一定影響。例如過期時間設(shè)置為5分鐘,如果此時存儲層添加了這個數(shù)據(jù),那此段時間就會出現(xiàn)緩存層和存儲層數(shù)據(jù)的不一致。這時候可以利用消息隊列或者其它異步方式清理緩存中的空對象。
布隆過濾器除了緩存空對象,我們還可以在存儲和緩存之前,加一個布隆過濾器,做一層過濾。
布隆過濾器里會保存數(shù)據(jù)是否存在,如果判斷數(shù)據(jù)不不能再,就不會訪問存儲。
兩種解決方案的對比:
緩存雪崩
某?時刻發(fā)??規(guī)模的緩存失效的情況,例如緩存服務(wù)宕機、大量key在同一時間過期,這樣的后果就是?量的請求進來直接打到DB上,可能導致整個系統(tǒng)的崩潰,稱為雪崩。
緩存雪崩是三大緩存問題里最嚴重的一種,我們來看看怎么預(yù)防和處理。
提高緩存可用性
集群部署:通過集群來提升緩存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。 多級緩存:設(shè)置多級緩存,第一級緩存失效的基礎(chǔ)上,訪問二級緩存,每一級緩存的失效時間都不同。
過期時間
均勻過期:為了避免大量的緩存在同一時間過期,可以把不同的 key 過期時間隨機生成,避免過期時間太過集中。 熱點數(shù)據(jù)永不過期。
熔斷降級
服務(wù)熔斷:當緩存服務(wù)器宕機或超時響應(yīng)時,為了防止整個系統(tǒng)出現(xiàn)雪崩,暫時停止業(yè)務(wù)服務(wù)訪問緩存系統(tǒng)。 服務(wù)降級:當出現(xiàn)大量緩存失效,而且處在高并發(fā)高負荷的情況下,在業(yè)務(wù)系統(tǒng)內(nèi)部暫時舍棄對一些非核心的接口和數(shù)據(jù)的請求,而直接返回一個提前準備好的 fallback(退路)錯誤處理信息。
27.能說說布隆過濾器嗎?
布隆過濾器,它是一個連續(xù)的數(shù)據(jù)結(jié)構(gòu),每個存儲位存儲都是一個bit,即0或者1, 來標識數(shù)據(jù)是否存在。
存儲數(shù)據(jù)的時時候,使用K個不同的哈希函數(shù)將這個變量映射為bit列表的的K個點,把它們置為1。
我們判斷緩存key是否存在,同樣,K個哈希函數(shù),映射到bit列表上的K個點,判斷是不是1:
如果全不是1,那么key不存在; 如果都是1,也只是表示key可能存在。
布隆過濾器也有一些缺點:
它在判斷元素是否在集合中時是有一定錯誤幾率,因為哈希算法有一定的碰撞的概率。 不支持刪除元素。
28.如何保證緩存和數(shù)據(jù)庫數(shù)據(jù)的?致性?
根據(jù)CAP理論,在保證可用性和分區(qū)容錯性的前提下,無法保證一致性,所以緩存和數(shù)據(jù)庫的絕對一致是不可能實現(xiàn)的,只能盡可能保存緩存和數(shù)據(jù)庫的最終一致性。
選擇合適的緩存更新策略
1. 刪除緩存而不是更新緩存
當一個線程對緩存的key進行寫操作的時候,如果其它線程進來讀數(shù)據(jù)庫的時候,讀到的就是臟數(shù)據(jù),產(chǎn)生了數(shù)據(jù)不一致問題。
相比較而言,刪除緩存的速度比更新緩存的速度快很多,所用時間相對也少很多,讀臟數(shù)據(jù)的概率也小很多。
先更數(shù)據(jù),后刪緩存先更數(shù)據(jù)庫還是先刪緩存?這是一個問題。
更新數(shù)據(jù),耗時可能在刪除緩存的百倍以上。在緩存中不存在對應(yīng)的key,數(shù)據(jù)庫又沒有完成更新的時候,如果有線程進來讀取數(shù)據(jù),并寫入到緩存,那么在更新成功之后,這個key就是一個臟數(shù)據(jù)。
毫無疑問,先刪緩存,再更數(shù)據(jù)庫,緩存中key不存在的時間的時間更長,有更大的概率會產(chǎn)生臟數(shù)據(jù)。
目前最流行的緩存讀寫策略cache-aside-pattern就是采用先更數(shù)據(jù)庫,再刪緩存的方式。
緩存不一致處理
如果不是并發(fā)特別高,對緩存依賴性很強,其實一定程序的不一致是可以接受的。
但是如果對一致性要求比較高,那就得想辦法保證緩存和數(shù)據(jù)庫中數(shù)據(jù)一致。
緩存和數(shù)據(jù)庫數(shù)據(jù)不一致常見的兩種原因:
緩存key刪除失敗 并發(fā)導致寫入了臟數(shù)據(jù)

消息隊列保證key被刪除可以引入消息隊列,把要刪除的key或者刪除失敗的key丟盡消息隊列,利用消息隊列的重試機制,重試刪除對應(yīng)的key。
這種方案看起來不錯,缺點是對業(yè)務(wù)代碼有一定的侵入性。
數(shù)據(jù)庫訂閱+消息隊列保證key被刪除可以用一個服務(wù)(比如阿里的 canal)去監(jiān)聽數(shù)據(jù)庫的binlog,獲取需要操作的數(shù)據(jù)。
然后用一個公共的服務(wù)獲取訂閱程序傳來的信息,進行緩存刪除操作。這種方式降低了對業(yè)務(wù)的侵入,但其實整個系統(tǒng)的復雜度是提升的,適合基建完善的大廠。
延時雙刪防止臟數(shù)據(jù)還有一種情況,是在緩存不存在的時候,寫入了臟數(shù)據(jù),這種情況在先刪緩存,再更數(shù)據(jù)庫的緩存更新策略下發(fā)生的比較多,解決方案是延時雙刪。
簡單說,就是在第一次刪除緩存之后,過了一段時間之后,再次刪除緩存。

這種方式的延時時間設(shè)置需要仔細考量和測試。
設(shè)置緩存過期時間兜底
這是一個樸素但是有用的辦法,給緩存設(shè)置一個合理的過期時間,即使發(fā)生了緩存數(shù)據(jù)不一致的問題,它也不會永遠不一致下去,緩存過期的時候,自然又會恢復一致。
29.如何保證本地緩存和分布式緩存的一致?
PS:這道題面試很少問,但實際工作中很常見。
在日常的開發(fā)中,我們常常采用兩級緩存:本地緩存+分布式緩存。
所謂本地緩存,就是對應(yīng)服務(wù)器的內(nèi)存緩存,比如Caffeine,分布式緩存基本就是采用Redis。
那么問題來了,本地緩存和分布式緩存怎么保持數(shù)據(jù)一致?
Redis緩存,數(shù)據(jù)庫發(fā)生更新,直接刪除緩存的key即可,因為對于應(yīng)用系統(tǒng)而言,它是一種中心化的緩存。
但是本地緩存,它是非中心化的,散落在分布式服務(wù)的各個節(jié)點上,沒法通過客戶端的請求刪除本地緩存的key,所以得想辦法通知集群所有節(jié)點,刪除對應(yīng)的本地緩存key。
可以采用消息隊列的方式:
采用Redis本身的Pub/Sub機制,分布式集群的所有節(jié)點訂閱刪除本地緩存頻道,刪除Redis緩存的節(jié)點,同事發(fā)布刪除本地緩存消息,訂閱者們訂閱到消息后,刪除對應(yīng)的本地key。但是Redis的發(fā)布訂閱不是可靠的,不能保證一定刪除成功。 引入專業(yè)的消息隊列,比如RocketMQ,保證消息的可靠性,但是增加了系統(tǒng)的復雜度。 設(shè)置適當?shù)倪^期時間兜底,本地緩存可以設(shè)置相對短一些的過期時間。
30.怎么處理熱key?
什么是熱Key?所謂的熱key,就是訪問頻率比較的key。
比如,熱門新聞事件或商品,這類key通常有大流量的訪問,對存儲這類信息的 Redis來說,是不小的壓力。
假如Redis集群部署,熱key可能會造成整體流量的不均衡,個別節(jié)點出現(xiàn)OPS過大的情況,極端情況下熱點key甚至會超過 Redis本身能夠承受的OPS。
怎么處理熱key?
對熱key的處理,最關(guān)鍵的是對熱點key的監(jiān)控,可以從這些端來監(jiān)控熱點key:
客戶端 客戶端其實是距離key“最近”的地方,因為Redis命令就是從客戶端發(fā)出的,例如在客戶端設(shè)置全局字典(key和調(diào)用次數(shù)),每次調(diào)用Redis命令時,使用這個字典進行記錄。
代理端 像Twemproxy、Codis這些基于代理的Redis分布式架構(gòu),所有客戶端的請求都是通過代理端完成的,可以在代理端進行收集統(tǒng)計。
Redis服務(wù)端 使用monitor命令統(tǒng)計熱點key是很多開發(fā)和運維人員首先想到,monitor命令可以監(jiān)控到Redis執(zhí)行的所有命令。
只要監(jiān)控到了熱key,對熱key的處理就簡單了:
把熱key打散到不同的服務(wù)器,降低壓?
加??級緩存,提前加載熱key數(shù)據(jù)到內(nèi)存中,如果redis宕機,?內(nèi)存查詢
31.緩存預(yù)熱怎么做呢?
所謂緩存預(yù)熱,就是提前把數(shù)據(jù)庫里的數(shù)據(jù)刷到緩存里,通常有這些方法:
1、直接寫個緩存刷新頁面或者接口,上線時手動操作
2、數(shù)據(jù)量不大,可以在項目啟動的時候自動進行加載
3、定時任務(wù)刷新緩存.
32.熱點key重建?問題?解決?
開發(fā)的時候一般使用“緩存+過期時間”的策略,既可以加速數(shù)據(jù)讀寫,又保證數(shù)據(jù)的定期更新,這種模式基本能夠滿足絕大部分需求。
但是有兩個問題如果同時出現(xiàn),可能就會出現(xiàn)比較大的問題:
當前key是一個熱點key(例如一個熱門的娛樂新聞),并發(fā)量非常大。
重建緩存不能在短時間完成,可能是一個復雜計算,例如復雜的 SQL、多次IO、多個依賴等。在緩存失效的瞬間,有大量線程來重建緩存,造成后端負載加大,甚至可能會讓應(yīng)用崩潰。
怎么處理呢?
要解決這個問題也不是很復雜,解決問題的要點在于:
減少重建緩存的次數(shù)。 數(shù)據(jù)盡可能一致。 較少的潛在危險。
所以一般采用如下方式:
互斥鎖(mutex key) 這種方法只允許一個線程重建緩存,其他線程等待重建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)即可。 永遠不過期 “永遠不過期”包含兩層意思:
從緩存層面來看,確實沒有設(shè)置過期時間,所以不會出現(xiàn)熱點key過期后產(chǎn)生的問題,也就是“物理”不過期。 從功能層面來看,為每個value設(shè)置一個邏輯過期時間,當發(fā)現(xiàn)超過邏輯過期時間后,會使用單獨的線程去構(gòu)建緩存。
33.無底洞問題嗎?如何解決?
什么是無底洞問題?
2010年,F(xiàn)acebook的Memcache節(jié)點已經(jīng)達到了3000個,承載著TB級別的緩存數(shù)據(jù)。但開發(fā)和運維人員發(fā)現(xiàn)了一個問題,為了滿足業(yè)務(wù)要求添加了大量新Memcache節(jié)點,但是發(fā)現(xiàn)性能不但沒有好轉(zhuǎn)反而下降了,當時將這 種現(xiàn)象稱為緩存的“無底洞”現(xiàn)象。
那么為什么會產(chǎn)生這種現(xiàn)象呢?
通常來說添加節(jié)點使得Memcache集群 性能應(yīng)該更強了,但事實并非如此。鍵值數(shù)據(jù)庫由于通常采用哈希函數(shù)將 key映射到各個節(jié)點上,造成key的分布與業(yè)務(wù)無關(guān),但是由于數(shù)據(jù)量和訪問量的持續(xù)增長,造成需要添加大量節(jié)點做水平擴容,導致鍵值分布到更多的 節(jié)點上,所以無論是Memcache還是Redis的分布式,批量操作通常需要從不同節(jié)點上獲取,相比于單機批量操作只涉及一次網(wǎng)絡(luò)操作,分布式批量操作會涉及多次網(wǎng)絡(luò)時間。
無底洞問題如何優(yōu)化呢?
先分析一下無底洞問題:
客戶端一次批量操作會涉及多次網(wǎng)絡(luò)操作,也就意味著批量操作會隨著節(jié)點的增多,耗時會不斷增大。
網(wǎng)絡(luò)連接數(shù)變多,對節(jié)點的性能也有一定影響。
常見的優(yōu)化思路如下:
命令本身的優(yōu)化,例如優(yōu)化操作語句等。
減少網(wǎng)絡(luò)通信次數(shù)。
降低接入成本,例如客戶端使用長連/連接池、NIO等。
Redis運維
34.Redis報內(nèi)存不足怎么處理?
Redis 內(nèi)存不足有這么幾種處理方式:
修改配置文件 redis.conf 的 maxmemory 參數(shù),增加 Redis 可用內(nèi)存 也可以通過命令set maxmemory動態(tài)設(shè)置內(nèi)存上限 修改內(nèi)存淘汰策略,及時釋放內(nèi)存空間 使用 Redis 集群模式,進行橫向擴容。
35.Redis的過期數(shù)據(jù)回收策略有哪些?
Redis主要有2種過期數(shù)據(jù)回收策略:
惰性刪除
惰性刪除指的是當我們查詢key的時候才對key進?檢測,如果已經(jīng)達到過期時間,則刪除。顯然,他有?個缺點就是如果這些過期的key沒有被訪問,那么他就?直?法被刪除,?且?直占?內(nèi)存。
定期刪除
定期刪除指的是Redis每隔?段時間對數(shù)據(jù)庫做?次檢查,刪除??的過期key。由于不可能對所有key去做輪詢來刪除,所以Redis會每次隨機取?些key去做檢查和刪除。
36.Redis有哪些內(nèi)存溢出控制/內(nèi)存淘汰策略?
Redis所用內(nèi)存達到maxmemory上限時會觸發(fā)相應(yīng)的溢出控制策略,Redis支持六種策略:
noeviction:默認策略,不會刪除任何數(shù)據(jù),拒絕所有寫入操作并返 回客戶端錯誤信息,此 時Redis只響應(yīng)讀操作。 volatile-lru:根據(jù)LRU算法刪除設(shè)置了超時屬性(expire)的鍵,直 到騰出足夠空間為止。如果沒有可刪除的鍵對象,回退到noeviction策略。 allkeys-lru:根據(jù)LRU算法刪除鍵,不管數(shù)據(jù)有沒有設(shè)置超時屬性, 直到騰出足夠空間為止。 allkeys-random:隨機刪除所有鍵,直到騰出足夠空間為止。 volatile-random:隨機刪除過期鍵,直到騰出足夠空間為止。 volatile-ttl:根據(jù)鍵值對象的ttl屬性,刪除最近將要過期數(shù)據(jù)。如果 沒有,回退到noeviction策略。
37.Redis阻塞?怎么解決?
Redis發(fā)生阻塞,可以從以下幾個方面排查:
API或數(shù)據(jù)結(jié)構(gòu)使用不合理
通常Redis執(zhí)行命令速度非???,但是不合理地使用命令,可能會導致執(zhí)行速度很慢,導致阻塞,對于高并發(fā)的場景,應(yīng)該盡量避免在大對象上執(zhí)行算法復雜 度超過O(n)的命令。
對慢查詢的處理分為兩步:
發(fā)現(xiàn)慢查詢:slowlog get{n}命令可以獲取最近 的n條慢查詢命令; 發(fā)現(xiàn)慢查詢后,可以從兩個方向去優(yōu)化慢查詢:1)修改為低算法復雜度的命令,如hgetall改為hmget等,禁用keys、sort等命 令 2)調(diào)整大對象:縮減大對象數(shù)據(jù)或把大對象拆分為多個小對象,防止一次命令操作過多的數(shù)據(jù)。 CPU飽和的問題
單線程的Redis處理命令時只能使用一個CPU。而CPU飽和是指Redis單核CPU使用率跑到接近100%。
針對這種情況,處理步驟一般如下:
判斷當前Redis并發(fā)量是否已經(jīng)達到極限,可以使用統(tǒng)計命令redis-cli-h{ip}-p{port}--stat獲取當前 Redis使用情況 如果Redis的請求幾萬+,那么大概就是Redis的OPS已經(jīng)到了極限,應(yīng)該做集群化水品擴展來分攤OPS壓力 如果只有幾百幾千,那么就得排查命令和內(nèi)存的使用 持久化相關(guān)的阻塞
對于開啟了持久化功能的Redis節(jié)點,需要排查是否是持久化導致的阻塞。
fork阻塞 fork操作發(fā)生在RDB和AOF重寫時,Redis主線程調(diào)用fork操作產(chǎn)生共享 內(nèi)存的子進程,由子進程完成持久化文件重寫工作。如果fork操作本身耗時過長,必然會導致主線程的阻塞。 AOF刷盤阻塞 當我們開啟AOF持久化功能時,文件刷盤的方式一般采用每秒一次,后臺線程每秒對AOF文件做fsync操作。當硬盤壓力過大時,fsync操作需要等 待,直到寫入完成。如果主線程發(fā)現(xiàn)距離上一次的fsync成功超過2秒,為了 數(shù)據(jù)安全性它會阻塞直到后臺線程執(zhí)行fsync操作完成。 HugePage寫操作阻塞 對于開啟Transparent HugePages的 操作系統(tǒng),每次寫命令引起的復制內(nèi)存頁單位由4K變?yōu)?MB,放大了512 倍,會拖慢寫操作的執(zhí)行時間,導致大量寫操作慢查詢。
38.大key問題了解嗎?
Redis使用過程中,有時候會出現(xiàn)大key的情況, 比如:
單個簡單的key存儲的value很大,size超過10KB hash, set,zset,list 中存儲過多的元素(以萬為單位)
大key會造成什么問題呢?
客戶端耗時增加,甚至超時 對大key進行IO操作時,會嚴重占用帶寬和CPU 造成Redis集群中數(shù)據(jù)傾斜 主動刪除、被動刪等,可能會導致阻塞
如何找到大key?
bigkeys命令:使用bigkeys命令以遍歷的方式分析Redis實例中的所有Key,并返回整體統(tǒng)計信息與每個數(shù)據(jù)類型中Top1的大Key redis-rdb-tools:redis-rdb-tools是由Python寫的用來分析Redis的rdb快照文件用的工具,它可以把rdb快照文件生成json文件或者生成報表用來分析Redis的使用詳情。
如何處理大key?

刪除大key
當Redis版本大于4.0時,可使用UNLINK命令安全地刪除大Key,該命令能夠以非阻塞的方式,逐步地清理傳入的Key。 當Redis版本小于4.0時,避免使用阻塞式命令KEYS,而是建議通過SCAN命令執(zhí)行增量迭代掃描key,然后判斷進行刪除。 壓縮和拆分key
當vaule是string時,比較難拆分,則使用序列化、壓縮算法將key的大小控制在合理范圍內(nèi),但是序列化和反序列化都會帶來更多時間上的消耗。 當value是string,壓縮之后仍然是大key,則需要進行拆分,一個大key分為不同的部分,記錄每個部分的key,使用multiget等操作實現(xiàn)事務(wù)讀取。 當value是list/set等集合類型時,根據(jù)預(yù)估的數(shù)據(jù)規(guī)模來進行分片,不同的元素計算后分到不同的片。
39.Redis常見性能問題和解決方案?
Master 最好不要做任何持久化工作,包括內(nèi)存快照和 AOF 日志文件,特別是不要啟用內(nèi)存快照做持久化。 如果數(shù)據(jù)比較關(guān)鍵,某個 Slave 開啟 AOF 備份數(shù)據(jù),策略為每秒同步一次。 為了主從復制的速度和連接的穩(wěn)定性,Slave 和 Master 最好在同一個局域網(wǎng)內(nèi)。 盡量避免在壓力較大的主庫上增加從庫。 Master 調(diào)用 BGREWRITEAOF 重寫 AOF 文件,AOF 在重寫的時候會占大量的 CPU 和內(nèi)存資源,導致服務(wù) load 過高,出現(xiàn)短暫服務(wù)暫停現(xiàn)象。 為了 Master 的穩(wěn)定性,主從復制不要用圖狀結(jié)構(gòu),用單向鏈表結(jié)構(gòu)更穩(wěn)定,即主從關(guān)為:Master<–Slave1<–Slave2<–Slave3…,這樣的結(jié)構(gòu)也方便解決單點故障問題,實現(xiàn) Slave 對 Master 的替換,也即,如果 Master 掛了,可以立馬啟用 Slave1 做 Master,其他不變。
Redis應(yīng)用
40.使用Redis 如何實現(xiàn)異步隊列?
我們知道redis支持很多種結(jié)構(gòu)的數(shù)據(jù),那么如何使用redis作為異步隊列使用呢?一般有以下幾種方式:
使用list作為隊列,lpush生產(chǎn)消息,rpop消費消息
這種方式,消費者死循環(huán)rpop從隊列中消費消息。但是這樣,即使隊列里沒有消息,也會進行rpop,會導致Redis CPU的消耗。
可以通過讓消費者休眠的方式的方式來處理,但是這樣又會又消息的延遲問題。
-使用list作為隊列,lpush生產(chǎn)消息,brpop消費消息
brpop是rpop的阻塞版本,list為空的時候,它會一直阻塞,直到list中有值或者超時。
這種方式只能實現(xiàn)一對一的消息隊列。
使用Redis的pub/sub來進行消息的發(fā)布/訂閱
發(fā)布/訂閱模式可以1:N的消息發(fā)布/訂閱。發(fā)布者將消息發(fā)布到指定的頻道頻道(channel),訂閱相應(yīng)頻道的客戶端都能收到消息。
但是這種方式不是可靠的,它不保證訂閱者一定能收到消息,也不進行消息的存儲。
所以,一般的異步隊列的實現(xiàn)還是交給專業(yè)的消息隊列。
41.Redis 如何實現(xiàn)延時隊列?
使用zset,利用排序?qū)崿F(xiàn)
可以使用 zset這個結(jié)構(gòu),用設(shè)置好的時間戳作為score進行排序,使用 zadd score1 value1 ....命令就可以一直往內(nèi)存中生產(chǎn)消息。再利用 zrangebysocre 查詢符合條件的所有待處理的任務(wù),通過循環(huán)執(zhí)行隊列任務(wù)即可。
42.Redis 支持事務(wù)嗎?
Redis提供了簡單的事務(wù),但它對事務(wù)ACID的支持并不完備。
multi命令代表事務(wù)開始,exec命令代表事務(wù)結(jié)束,它們之間的命令是原子順序執(zhí)行的:
127.0.0.1:6379>?multi?
OK
127.0.0.1:6379>?sadd?user:a:follow?user:b?
QUEUED?
127.0.0.1:6379>?sadd?user:b:fans?user:a?
QUEUED
127.0.0.1:6379>?sismember?user:a:follow?user:b?
(integer)?0
127.0.0.1:6379>?exec?1)?(integer)?1
2)?(integer)?1
Redis事務(wù)的原理,是所有的指令在 exec 之前不執(zhí)行,而是緩存在
服務(wù)器的一個事務(wù)隊列中,服務(wù)器一旦收到 exec 指令,才開執(zhí)行整個事務(wù)隊列,執(zhí)行完畢后一次性返回所有指令的運行結(jié)果。
因為Redis執(zhí)行命令是單線程的,所以這組命令順序執(zhí)行,而且不會被其它線程打斷。
Redis事務(wù)的注意點有哪些?
需要注意的點有:
Redis 事務(wù)是不支持回滾的,不像 MySQL 的事務(wù)一樣,要么都執(zhí)行要么都不執(zhí)行;
Redis 服務(wù)端在執(zhí)行事務(wù)的過程中,不會被其他客戶端發(fā)送來的命令請求打斷。直到事務(wù)命令全部執(zhí)行完畢才會執(zhí)行其他客戶端的命令。
Redis 事務(wù)為什么不支持回滾?
Redis 的事務(wù)不支持回滾。
如果執(zhí)行的命令有語法錯誤,Redis 會執(zhí)行失敗,這些問題可以從程序?qū)用娌东@并解決。但是如果出現(xiàn)其他問題,則依然會繼續(xù)執(zhí)行余下的命令。
這樣做的原因是因為回滾需要增加很多工作,而不支持回滾則可以保持簡單、快速的特性。
43.Redis和Lua腳本的使用了解嗎?
Redis的事務(wù)功能比較簡單,平時的開發(fā)中,可以利用Lua腳本來增強Redis的命令。
Lua腳本能給開發(fā)人員帶來這些好處:
Lua腳本在Redis中是原子執(zhí)行的,執(zhí)行過程中間不會插入其他命令。 Lua腳本可以幫助開發(fā)和運維人員創(chuàng)造出自己定制的命令,并可以將這 些命令常駐在Redis內(nèi)存中,實現(xiàn)復用的效果。 Lua腳本可以將多條命令一次性打包,有效地減少網(wǎng)絡(luò)開銷。
比如這一段很(爛)經(jīng)(大)典(街)的秒殺系統(tǒng)利用lua扣減Redis庫存的腳本:
???--?庫存未預(yù)熱
???if?(redis.call('exists',?KEYS[2])?==?1)?then
????????return?-9;
????end;
????--?秒殺商品庫存存在
????if?(redis.call('exists',?KEYS[1])?==?1)?then
????????local?stock?=?tonumber(redis.call('get',?KEYS[1]));
????????local?num?=?tonumber(ARGV[1]);
????????--?剩余庫存少于請求數(shù)量
????????if?(stock?????????????return?-3
????????end;
????????--?扣減庫存
????????if?(stock?>=?num)?then
????????????redis.call('incrby',?KEYS[1],?0?-?num);
????????????--?扣減成功
????????????return?1
????????end;
????????return?-2;
????end;
????--?秒殺商品庫存不存在
????return?-1;
44.Redis的管道了解嗎?
Redis 提供三種將客戶端多條命令打包發(fā)送給服務(wù)端執(zhí)行的方式:
Pipelining(管道) 、 Transactions(事務(wù)) 和 Lua Scripts(Lua 腳本) 。
Pipelining(管道)
Redis 管道是三者之中最簡單的,當客戶端需要執(zhí)行多條 redis 命令時,可以通過管道一次性將要執(zhí)行的多條命令發(fā)送給服務(wù)端,其作用是為了降低 RTT(Round Trip Time) 對性能的影響,比如我們使用 nc 命令將兩條指令發(fā)送給 redis 服務(wù)端。
Redis 服務(wù)端接收到管道發(fā)送過來的多條命令后,會一直執(zhí)命令,并將命令的執(zhí)行結(jié)果進行緩存,直到最后一條命令執(zhí)行完成,再所有命令的執(zhí)行結(jié)果一次性返回給客戶端 。
Pipelining的優(yōu)勢
在性能方面, Pipelining 有下面兩個優(yōu)勢:
節(jié)省了RTT:將多條命令打包一次性發(fā)送給服務(wù)端,減少了客戶端與服務(wù)端之間的網(wǎng)絡(luò)調(diào)用次數(shù) 減少了上下文切換:當客戶端/服務(wù)端需要從網(wǎng)絡(luò)中讀寫數(shù)據(jù)時,都會產(chǎn)生一次系統(tǒng)調(diào)用,系統(tǒng)調(diào)用是非常耗時的操作,其中設(shè)計到程序由用戶態(tài)切換到內(nèi)核態(tài),再從內(nèi)核態(tài)切換回用戶態(tài)的過程。當我們執(zhí)行 10 條 redis 命令的時候,就會發(fā)生 10 次用戶態(tài)到內(nèi)核態(tài)的上下文切換,但如果我們使用 Pipeining 將多條命令打包成一條一次性發(fā)送給服務(wù)端,就只會產(chǎn)生一次上下文切換。
45.Redis實現(xiàn)分布式鎖了解嗎?
Redis是分布式鎖本質(zhì)上要實現(xiàn)的目標就是在 Redis 里面占一個“茅坑”,當別的進程也要來占時,發(fā)現(xiàn)已經(jīng)有人蹲在那里了,就只好放棄或者稍后再試。
V1:setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允許被一個客戶端占坑。先來先占, 用完了,再調(diào)用 del 指令釋放茅坑。
>?setnx?lock:fighter?true
OK
...?do?something?critical?...
>?del?lock:fighter
(integer)?1
但是有個問題,如果邏輯執(zhí)行到中間出現(xiàn)異常了,可能會導致 del 指令沒有被調(diào)用,這樣就會陷入死鎖,鎖永遠得不到釋放。
V2:鎖超時釋放
所以在拿到鎖之后,再給鎖加上一個過期時間,比如 5s,這樣即使中間出現(xiàn)異常也可以保證 5 秒之后鎖會自動釋放。
>?setnx?lock:fighter?true
OK
>?expire?lock:fighter?5
...?do?something?critical?...
>?del?lock:fighter
(integer)?1
但是以上邏輯還有問題。如果在 setnx 和 expire 之間服務(wù)器進程突然掛掉了,可能是因為機器掉電或者是被人為殺掉的,就會導致 expire 得不到執(zhí)行,也會造成死鎖。
這種問題的根源就在于 setnx 和 expire 是兩條指令而不是原子指令。如果這兩條指令可以一起執(zhí)行就不會出現(xiàn)問題。
V3:set指令
這個問題在Redis 2.8 版本中得到了解決,這個版本加入了 set 指令的擴展參數(shù),使得 setnx 和expire 指令可以一起執(zhí)行。
set?lock:fighter3?true?ex?5?nx?OK?...?do?something?critical?...?>?del?lock:codehole
上面這個指令就是 setnx 和 expire 組合在一起的原子指令,這個就算是比較完善的分布式鎖了。
當然實際的開發(fā),沒人會去自己寫分布式鎖的命令,因為有專業(yè)的輪子——Redisson。
底層結(jié)構(gòu)
這一部分就比較深了,如果不是簡歷上寫了精通Redis,應(yīng)該不會怎么問。
46.說說Redis底層數(shù)據(jù)結(jié)構(gòu)?
Redis有動態(tài)字符串(sds)、鏈表(list)、字典(ht)、跳躍表(skiplist)、整數(shù)集合(intset)、壓縮列表(ziplist) 等底層數(shù)據(jù)結(jié)構(gòu)。
Redis并沒有使用這些數(shù)據(jù)結(jié)構(gòu)來直接實現(xiàn)鍵值對數(shù)據(jù)庫,而是基于這些數(shù)據(jù)結(jié)構(gòu)創(chuàng)建了一個對象系統(tǒng),來表示所有的key-value。
我們常用的數(shù)據(jù)類型和編碼對應(yīng)的映射關(guān)系:
簡單看一下底層數(shù)據(jù)結(jié)構(gòu),如果對數(shù)據(jù)結(jié)構(gòu)掌握不錯的話,理解這些結(jié)構(gòu)應(yīng)該不是特別難:
字符串:redis沒有直接使?C語?傳統(tǒng)的字符串表示,?是??實現(xiàn)的叫做簡單動態(tài)字符串SDS的抽象類型。
C語?的字符串不記錄?身的?度信息,?SDS則保存了?度信息,這樣將獲取字符串?度的時間由O(N)降低到了O(1),同時可以避免緩沖區(qū)溢出和減少修改字符串?度時所需的內(nèi)存重分配次數(shù)。

鏈表linkedlist:redis鏈表是?個雙向?環(huán)鏈表結(jié)構(gòu),很多發(fā)布訂閱、慢查詢、監(jiān)視器功能都是使?到了鏈表來實現(xiàn),每個鏈表的節(jié)點由?個listNode結(jié)構(gòu)來表示,每個節(jié)點都有指向前置節(jié)點和后置節(jié)點的指針,同時表頭節(jié)點的前置和后置節(jié)點都指向NULL。

字典dict:?于保存鍵值對的抽象數(shù)據(jù)結(jié)構(gòu)。Redis使?hash表作為底層實現(xiàn),一個哈希表里可以有多個哈希表節(jié)點,而每個哈希表節(jié)點就保存了字典里中的一個鍵值對。每個字典帶有兩個hash表,供平時使?和rehash時使?,hash表使?鏈地址法來解決鍵沖突,被分配到同?個索引位置的多個鍵值對會形成?個單向鏈表,在對hash表進?擴容或者縮容的時候,為了服務(wù)的可?性,rehash的過程不是?次性完成的,?是漸進式的。

跳躍表skiplist:跳躍表是有序集合的底層實現(xiàn)之?,Redis中在實現(xiàn)有序集合鍵和集群節(jié)點的內(nèi)部結(jié)構(gòu)中都是?到了跳躍表。Redis跳躍表由zskiplist和zskiplistNode組成,zskiplist?于保存跳躍表信息(表頭、表尾節(jié)點、?度等),zskiplistNode?于表示表跳躍節(jié)點,每個跳躍表節(jié)點的層?都是1-32的隨機數(shù),在同?個跳躍表中,多個節(jié)點可以包含相同的分值,但是每個節(jié)點的成員對象必須是唯?的,節(jié)點按照分值??排序,如果分值相同,則按照成員對象的??排序。

整數(shù)集合intset:?于保存整數(shù)值的集合抽象數(shù)據(jù)結(jié)構(gòu),不會出現(xiàn)重復元素,底層實現(xiàn)為數(shù)組。

壓縮列表ziplist:壓縮列表是為節(jié)約內(nèi)存?開發(fā)的順序性數(shù)據(jù)結(jié)構(gòu),它可以包含任意多個節(jié)點,每個節(jié)點可以保存?個字節(jié)數(shù)組或者整數(shù)值。

47.Redis 的 SDS 和 C 中字符串相比有什么優(yōu)勢?
C 語言使用了一個長度為 N+1 的字符數(shù)組來表示長度為 N 的字符串,并且字符數(shù)組最后一個元素總是 \0,這種簡單的字符串表示方式 不符合 Redis 對字符串在安全性、效率以及功能方面的要求。

C語言的字符串可能有什么問題?
這樣簡單的數(shù)據(jù)結(jié)構(gòu)可能會造成以下一些問題:
獲取字符串長度復雜度高 :因為 C 不保存數(shù)組的長度,每次都需要遍歷一遍整個數(shù)組,時間復雜度為O(n); 不能杜絕 緩沖區(qū)溢出/內(nèi)存泄漏 的問題 : C字符串不記錄自身長度帶來的另外一個問題是容易造成緩存區(qū)溢出(buffer overflow),例如在字符串拼接的時候,新的 C 字符串 只能保存文本數(shù)據(jù) → 因為 C 語言中的字符串必須符合某種編碼(比如 ASCII),例如中間出現(xiàn)的 '\0'可能會被判定為提前結(jié)束的字符串而識別不了;
Redis如何解決?優(yōu)勢?

簡單來說一下 Redis 如何解決的:
多增加 len 表示當前字符串的長度:這樣就可以直接獲取長度了,復雜度 O(1); 自動擴展空間:當 SDS 需要對字符串進行修改時,首先借助于 len和alloc檢查空間是否滿足修改所需的要求,如果空間不夠的話,SDS 會自動擴展空間,避免了像 C 字符串操作中的溢出情況;有效降低內(nèi)存分配次數(shù):C 字符串在涉及增加或者清除操作時會改變底層數(shù)組的大小造成重新分配,SDS 使用了 空間預(yù)分配 和 惰性空間釋放 機制,簡單理解就是每次在擴展時是成倍的多分配的,在縮容是也是先留著并不正式歸還給 OS; 二進制安全:C 語言字符串只能保存 ascii碼,對于圖片、音頻等信息無法保存,SDS 是二進制安全的,寫入什么讀取就是什么,不做任何過濾和限制;
48.字典是如何實現(xiàn)的?Rehash 了解嗎?
字典是 Redis 服務(wù)器中出現(xiàn)最為頻繁的復合型數(shù)據(jù)結(jié)構(gòu)。除了 hash 結(jié)構(gòu)的數(shù)據(jù)會用到字典外,整個 Redis 數(shù)據(jù)庫的所有 key 和 value 也組成了一個 全局字典,還有帶過期時間的 key 也是一個字典。*(存儲在 RedisDb 數(shù)據(jù)結(jié)構(gòu)中)*
字典結(jié)構(gòu)是什么樣的呢?
Redis 中的字典相當于 Java 中的 HashMap,內(nèi)部實現(xiàn)也差不多類似,采用哈希與運算計算下標位置;通過 "數(shù)組 + 鏈表" 的鏈地址法 來解決哈希沖突,同時這樣的結(jié)構(gòu)也吸收了兩種不同數(shù)據(jù)結(jié)構(gòu)的優(yōu)點。
字典是怎么擴容的?
字典結(jié)構(gòu)內(nèi)部包含 兩個 hashtable,通常情況下只有一個哈希表 ht[0] 有值,在擴容的時候,把ht[0]里的值rehash到ht[1],然后進行 漸進式rehash ——所謂漸進式rehash,指的是這個rehash的動作并不是一次性、集中式地完成的,而是分多次、漸進式地完成的。
待搬遷結(jié)束后,h[1]就取代h[0]存儲字典的元素。
49.跳躍表是如何實現(xiàn)的?原理?
PS:跳躍表是比較常問的一種結(jié)構(gòu)。
跳躍表(skiplist)是一種有序數(shù)據(jù)結(jié)構(gòu),它通過在每個節(jié)點中維持多個指向其它節(jié)點的指針,從而達到快速訪問節(jié)點的目的。
為什么使用跳躍表?
首先,因為 zset 要支持隨機的插入和刪除,所以它 不宜使用數(shù)組來實現(xiàn),關(guān)于排序問題,我們也很容易就想到 紅黑樹/ 平衡樹 這樣的樹形結(jié)構(gòu),為什么 Redis 不使用這樣一些結(jié)構(gòu)呢?
性能考慮: 在高并發(fā)的情況下,樹形結(jié)構(gòu)需要執(zhí)行一些類似于 rebalance 這樣的可能涉及整棵樹的操作,相對來說跳躍表的變化只涉及局部; 實現(xiàn)考慮: 在復雜度與紅黑樹相同的情況下,跳躍表實現(xiàn)起來更簡單,看起來也更加直觀;
基于以上的一些考慮,Redis 基于 William Pugh 的論文做出一些改進后采用了 跳躍表 這樣的結(jié)構(gòu)。
本質(zhì)是解決查找問題。
跳躍表是怎么實現(xiàn)的?
跳躍表的節(jié)點里有這些元素:
層跳躍表節(jié)點的level數(shù)組可以包含多個元素,每個元素都包含一個指向其它節(jié)點的指針,程序可以通過這些層來加快訪問其它節(jié)點的速度,一般來說,層的數(shù)量月多,訪問其它節(jié)點的速度就越快。
每次創(chuàng)建一個新的跳躍表節(jié)點的時候,程序都根據(jù)冪次定律,隨機生成一個介于1和32之間的值作為level數(shù)組的大小,這個大小就是層的“高度”
前進指針每個層都有一個指向表尾的前進指針(level[i].forward屬性),用于從表頭向表尾方向訪問節(jié)點。
我們看一下跳躍表從表頭到表尾,遍歷所有節(jié)點的路徑:

跨度層的跨度用于記錄兩個節(jié)點之間的距離??缍仁怯脕碛嬎闩盼唬╮ank)的:在查找某個節(jié)點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結(jié)果就是目標節(jié)點在跳躍表中的排位。
例如查找,分值為3.0、成員對象為o3的節(jié)點時,沿途經(jīng)歷的層:查找的過程只經(jīng)過了一個層,并且層的跨度為3,所以目標節(jié)點在跳躍表中的排位為3。

分值和成員節(jié)點的分值(score屬性)是一個double類型的浮點數(shù),跳躍表中所有的節(jié)點都按分值從小到大來排序。
節(jié)點的成員對象(obj屬性)是一個指針,它指向一個字符串對象,而字符串對象則保存這一個SDS值。
50.壓縮列表了解嗎?
壓縮列表是 Redis 為了節(jié)約內(nèi)存 而使用的一種數(shù)據(jù)結(jié)構(gòu),是由一系列特殊編碼的連續(xù)內(nèi)存快組成的順序型數(shù)據(jù)結(jié)構(gòu)。
一個壓縮列表可以包含任意多個節(jié)點(entry),每個節(jié)點可以保存一個字節(jié)數(shù)組或者一個整數(shù)值。
壓縮列表由這么幾部分組成:
zlbyttes:記錄整個壓縮列表占用的內(nèi)存字節(jié)數(shù) zltail:記錄壓縮列表表尾節(jié)點距離壓縮列表的起始地址有多少字節(jié) zllen:記錄壓縮列表包含的節(jié)點數(shù)量 entryX:列表節(jié)點 zlend:用于標記壓縮列表的末端

51.快速列表 quicklist 了解嗎?
Redis 早期版本存儲 list 列表數(shù)據(jù)結(jié)構(gòu)使用的是壓縮列表 ziplist 和普通的雙向鏈表 linkedlist,也就是說當元素少時使用 ziplist,當元素多時用 linkedlist。
但考慮到鏈表的附加空間相對較高,prev 和 next 指針就要占去 16 個字節(jié)(64 位操作系統(tǒng)占用 8 個字節(jié)),另外每個節(jié)點的內(nèi)存都是單獨分配,會家具內(nèi)存的碎片化,影響內(nèi)存管理效率。
后來 Redis 新版本(3.2)對列表數(shù)據(jù)結(jié)構(gòu)進行了改造,使用 quicklist 代替了 ziplist 和 linkedlist,quicklist是綜合考慮了時間效率與空間效率引入的新型數(shù)據(jù)結(jié)構(gòu)。
quicklist由list和ziplist結(jié)合而成,它是一個由ziplist充當節(jié)點的雙向鏈表。
其他問題
52.假如Redis里面有1億個key,其中有10w個key是以某個固定的已知的前綴開頭的,如何將它們?nèi)空页鰜恚?/span>
使用 keys 指令可以掃出指定模式的 key 列表。但是要注意 keys 指令會導致線程阻塞一段時間,線上服務(wù)會停頓,直到指令執(zhí)行完畢,服務(wù)才能恢復。這個時候可以使用 scan 指令,scan 指令可以無阻塞的提取出指定模式的 key 列表,但是會有一定的重復概率,在客戶端做一次去重就可以了,但是整體所花費的時間會比直接用 keys 指令長。
參考:
[1].《Redis開發(fā)與實戰(zhàn)》
[2].《Redis設(shè)計與實現(xiàn)》
[3].《Redis深度歷險》
[4]. 艾小仙《我要進大廠》
[5].田維?!逗蠖嗣嬖囆」P記》
[6]. [美團二面:Redis與MySQL雙寫一致性如何保證?](https://juejin.cn/post/6964531365643550751)
[7]. [媽媽再也不擔心我面試被Redis問得臉都綠了 ](https://mp.weixin.qq.com/s/vXBFscXqDcXS_VaIERplMQ)
[8]. [面試官:緩存一致性問題怎么解決?](https://mp.weixin.qq.com/s/dYvM8_6SQnYRB6KjPsprbw)
[9]. [高并發(fā)場景下,到底先更新緩存還是先更新數(shù)據(jù)庫?](https://mp.weixin.qq.com/s/bewlUcHL2e6fw2vDrhEDCw)
[10] .[【Redis破障之路】三:Redis單線程架構(gòu)](https://fighter3.blog.csdn.net/article/details/116166827)
[11]. Redis官網(wǎng)
[12]. [解決了Redis大key問題,同事們都夸他牛皮](https://www.51cto.com/article/701990.html)
[13].[Redis 分布式鎖原理看這篇就夠了, 循循漸進 ](https://juejin.cn/post/6897414205071163400)
[14]. 《Redis5設(shè)計與源碼分析》
老板,點贊、在看、轉(zhuǎn)發(fā)三連安排一波!
