Redis高频面试题完整版
文章目錄:
-
Redis概述
-
什么是Redis?
-
Redis的優缺點?
-
Redis為什么常常用做緩存?相比于guava有什么優勢?
-
Redis和Memcached的區別與共同點?
-
Redis是單線程還是多線程?Redis為什么這么快?
-
Redis6.0之后為什么引入了多線程?
-
Redis的數據類型有哪些?
-
Redis的數據結構有哪些?
-
Redis的應用場景有哪些?
-
Redis是單線程的,如何提高CPU的利用率?
-
-
過期鍵的刪除策略
-
鍵的過期刪除策略
-
Redis的內存淘汰機制是什么樣的?
-
-
Redis的持久化
-
什么是Redis的持久化?
-
Redis常見的持久化機制有哪些?有什么有優缺點?
-
-
Redis的事務
-
什么是Redis的事務
-
Redis事務的相關命令
-
Redis事務執行的三個階段
-
Redis事務的特性
-
Redis事務為什么不支持回滾?
-
-
Redis的集群、主從、哨兵
-
Redis集群的實現方案有哪些?
-
Redis主從架構中數據丟失嗎
-
如何解決主從架構數據丟失問題?
-
Redis集群的主從復制過程是什么樣的?
-
Redis是如何保證主從服務器一致處于連接狀態以及命令是否丟失?
-
因為網絡原因在主從復制過程中停止復制會怎么樣?
-
了解Redis哈希槽嗎?
-
Redi集群最大的節點個數是多少?為什么?
-
Redis集群是如何選擇數據庫的?
-
Redis高可用方案如何實現?
-
-
Redis的分區
-
Redis的分區作用是什么?
-
Redis分區有哪些實現方案?
-
Redis分區的缺點?
-
-
Redis的分布式問題
-
什么是分布式鎖?
-
分布式鎖具有哪些特性?
-
分布式鎖的實現方法?
-
Redis如何實現分布式鎖?
-
Redis并發競爭key問題應該如何解決?
-
什么是RedLock
-
-
Redis的緩存問題
-
說下什么是緩存雪崩、緩存穿透、緩存擊穿,及它們的解決方案
-
如何保證緩存與數據庫雙寫時的數據一致性?
-
-
Redis其他高頻面試題
-
一個字符串類型的值能存儲最大容量是多少?
-
Redis如何實現大量數據插入?
-
如何通過Redis實現異步隊列?
-
如何通過Redis實現延時隊列?
-
Redis回收使用什么算法?
-
Redis 里面有1億個 key,其中有 10 個 key 是包含 java,如何將它們全部找出來?
-
生產環境中的Redis是如何部署的
-
Redis概述
什么是Redis?
Redis是一個高性能的非關系型的鍵值對數據庫,使用C編寫實現的。與傳統的數據庫不同的是Redis是存在內存中的,所以讀寫速度非常快,每秒可以處理超過10萬次的讀寫操作,這也是Redis常常被用作緩存的原因。
Redis的優缺點?
優點:
-
讀寫性能好,讀的速度可達110000次/s,寫的速度可達81000次/s。
-
支持數據持久化,有AOF和RDB兩中持久化方式
-
數據結構豐富,支持String、List、Set、Hash等結構
-
支持事務,Redis所有的操作都是原子性的,并且還支持幾個操作合并后的原子性執行,原子性指操作要么成功執行,要么失敗不執行,不會執行一部分。
-
支持主從復制,主機可以自動將數據同步到從機,進行讀寫分離。
缺點:
-
因為Redis是將數據存到內存中的,所以會受到內存大小的限制,不能用作海量數據的讀寫
-
Redis不具備自動容錯和恢復功能,主機或從機宕機會導致前端部分讀寫請求失敗,需要重啟機器或者手動切換前端的IP才能切換
Redis為什么常常用做緩存?相比于guava有什么優勢?
緩存的定義是訪問速度比一般隨機存取存儲器快的一種高速存儲器,而因為Redis是基于內存提供了高性能的數據存取功能,其比較顯著的優勢就是非常地快。
緩存可以分為本地緩存或者分布式緩存,比較常用的guava緩存就是一種本地緩存,其主要特點是輕量并且快速,生命周期隨著JVM的銷毀而結束,缺點是在多實例的情況下,每個實例都要自己保存一份緩存,這樣會導致緩存的一致性出現問題。
Redis則是分布式緩存,在多實例情況下,每個實例都共享一份緩存數據,緩存具備一致性。缺點是要保持Redis的高可用整體架構會比較復雜。
Redis和Memcached的區別與共同點?
相同點:
-
兩者的讀寫性能都比較高
-
都是基于內存的數據庫,通常被當作緩存使用
-
都有過期策略
-
都是基于C語言實現
不同點:
| 是否支持復制 | 支持主從復制 | 不支持復制 |
| key長度 | 長度最大為 2GB | 長度最多為 250 個字節 |
| 數據類型 | 不僅支持key-value類型的數據,還支持hash、list、set、zset等數據等數據類型的數據 | 僅支持key-value類型的數據 |
| 數據持久化 | 支持數據持久化,可以將數據保存到磁盤 | 不支持數據持久化 |
| 網絡IO模型 | 單線程的多路 IO 復用模型 | 多線程的非阻塞IO模式 |
| 集群 | 原生支持cluster 模式集群 | 無原生 |
Redis是單線程還是多線程?Redis為什么這么快?
Redis6.0之前是單線程的,為什么Redis6.0之前采用單線程而不采用多線程呢?
簡單來說,就是Redis官方認為沒必要,單線程的Redis的瓶頸通常在CPU的IO,而在使用Redis時幾乎不存在CPU成為瓶頸的情況。使用Redis主要的瓶頸在內存和網絡,并且使用單線程也存在一些優點,比如系統的復雜度較低,可為維護性較高,避免了并發讀寫所帶來的一系列問題。
Redis為什么這么快主要有以下幾個原因:
-
運行在內存中
-
數據結構簡單
-
使用多路IO復用技術
-
單線程實現,單線程避免了線程切換、鎖等造成的性能開銷。
Redis6.0之后為什么引入了多線程?
前面說了那么多Redis使用單線程的原因,但從Redis6.0后開始支持多線程了,簡直打臉有點快。那么為什么較新的Redis版本又開始支持多線程了呢?
前面也說了Redis的瓶頸在內存和網絡,Redis6.0引入多線程主要是為了解決網路IO讀寫這個瓶頸,執行命令還是單線程執行的,所以也不存在線程安全問題。
Redis6.0默認是否開啟了多線程呢?
默認是沒有開啟的,如需開啟,需要修改配置文件redis.conf:io-threads-do-reads ?no,no改為yes
Redis的數據類型有哪些?
Redis的常見的數據類型有String、Hash、Set、List、ZSet。還有三種不那么常見的數據類型:Bitmap、HyperLogLog、Geospatial。
| STRING | 字符串、整數、浮點數 | 對整數或浮點數可以進行自增、自減操作 對字符串操作 | 鍵值對緩存及常規計數: 微博數, 粉絲數 |
| LIST | 列表(內部使用雙向列表實現) | 向列表兩端添加元素,或者獲得列表的某一個片段 | 存儲文章ID列表、存儲評論列表等 |
| SET | 無序集合(內部使用值為空的散列表) | 增加/刪除元素、獲取集合中元素、取交集并集等等 | 共同好友、共同關注等 |
| ZSET | 有序集合(內部使用散列表和跳表) | 添加、獲取、刪除元素 根據分值范圍或者成員來獲取元素 計算一個鍵的排名 | 去重、獲取排名前幾的用戶 |
| HASH | 包含鍵值對的無序散列表 | 添加、獲取、移除單個鍵值對 獲取所有鍵值對 檢查某個鍵是否存在 | 常用于存儲對象 |
Bitmap:位圖,是一個以位為單位的數組,數組中只能存儲1或0,數組的下標在Bitmap中叫做偏移量。Bitmap實現統計功能,更省空間。面試中常問的布隆過濾器就有用到這種數據結構,布隆過濾器可以判斷出哪些數據一定不在數據庫中,所以常被用來解決Redis緩存穿透問題。
Hyperloglog:HyperLogLog 是一種用于統計基數的數據集合類型,每個 HyperLogLog 鍵只需要花費 12 KB 內存,就可以計算接近 2^64 個不同元素的基數。HyperLogLog 的優點是,在輸入元素的數量或者體積非常大時,計算基數所需的空間總是固定 的、并且是很小的。缺點是 HyperLogLog 的統計規則是基于概率完成的,所以它給出的統計結果是有一定誤差的,標準誤算率是 0.81%。常見的應用場景:統計網站的UV
Geospatial:主要用于存儲地理位置信息,常用于定位附近的人,打車距離的計算等。
Redis的數據結構有哪些?
很多人都會把數據結構和數據類型混為一談,包括很多面試官問的時候也沒有刻意區分這兩個。Redis的數據結構比較多,篇幅有限,這里只重點介紹面試常問的跳躍表。
Redis的數據結構有簡單動態字符串、鏈表、字典、跳躍表、整數集合、壓縮列表等。
簡單動態字符串:大家都知道,Redis的底層是用C語言編寫,但Redis并沒有直接使用C語言傳統的字符串表示,而是構建了一種名為簡單動態字符串的抽象類型。
鏈表:鏈表提供了高效的節點重排能力,以及順序性的節點訪問方式,并且可以通過增刪節點來靈活地調整鏈表的長度。鏈表是列表的底層實現之一。
字典:字典,又稱為符號表(symbol table)、關聯數組(associativearray)或映射(map),是一種用于保存鍵值對(key-value pair)的抽象數據結構。字典在Redis中的應用相當廣泛,比如Redis的數據庫就是使用字典來作為底層實現的,對數據庫的增、刪、查、改操作也是構建在對字典的操作之上的。
整數集合:?整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數值元素,并且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現。
壓縮列表(ziplist):壓縮列表是Redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。
對象:可能看到這里,很多人在想Redis的數據結構和數據類型的區別,其實上面介紹的是Redis的底層數據結構,但Redis并沒有直接使用這些數據結構來實現鍵值對數據庫,而是基于這些數據結構創建了一個對象系統,這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象,每種對象都用到了至少一種我們前面所介紹的數據結構,是不是這就和前面對上了。
看到這里很多人會好奇,為什么不直接使用這些底層數據結構,而是要創建對象系統。對象系統主要有以下優點:
-
通過這五種不同類型的對象,Redis可以在執行命令之前,根據對象的類型來判斷一個對象是否可以執行給定的命令。
-
我們可以針對不同的使用場景,為對象設置多種不同的數據結構實現,從而優化對象在不同場景下的使用效率。
-
實現了基于引用計數技術的內存回收機制,當程序不再使用某個對象的時候,這個對象所占用的內存就會被自動釋放,了解Java虛擬機的垃圾回收機制看到這里是不是很熟悉。
-
edis還通過引用計數技術實現了對象共享機制,這一機制可以在適當的條件下,通過讓多個數據庫鍵共享同一個對象來節約內存。
對象這部分占了比較大的篇幅,其實面試中問的也不多,但為了更方便理解,介紹地多些。順便看下這些底層數據結構和對象系統的對應關系。
?
最后介紹下面試中常問的跳躍表。
跳躍表(skiplist):跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳躍表支持平均O(logN)、最壞O(N)復雜度的節點查找,還可以通過順序性操作來批量處理節點。跳躍表作是序集合鍵的底層實現之一。
和鏈表、字典等數據結構被廣泛地應用在Redis內部不同,Redis只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構,除此之外,跳躍表在Redis里面沒有其他用途。
跳躍表本質上采用的是一種空間換時間的策略,是一種可以可以進行二分查找的有序鏈表,跳表在原有的有序鏈表上增加了多級索引,通過索引來實現快速查詢。跳表不僅能提高搜索性能,同時也可以提高插入和刪除操作的性能。
這是一個原始的有序列表,時間復雜度為O(n)。
為了提高查找效率,可以對鏈表建立一級索引,如下圖,在之前找到11這個元素需要遍歷6個節點,現在需要5個。鏈表越長,效率提升越明顯。
?
為了繼續提高查找效率可以繼續增加索引
?
對于理想的跳表,每向上一層索引節點數量都是下一層的1/2,跳表的**時間復雜度為o(logn),空間復雜度為o(n)**,雖然是空間換時間的策略,這里舉例存儲的只是數字,如果是存儲比較大的對象,浪費的空間就不值得一提了,因為索引結點只需要存儲關鍵值和幾個指針,并不需要存儲對象。
跳表相比于紅黑樹的優點(redis為什么用跳表不同紅黑樹):
-
內存占用更少,自定義參數化決定使用多少內存
-
查詢性能至少不比紅黑樹差
-
簡單更容易實現和維護
上面這三個優點是我在一篇博客中看到,這個問題redis作者本人也回應過。我這蹩腳的英文水平就不翻譯了,以免跑偏了。
-
They are not very memory intensive. It's up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
-
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
-
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
最后,說下Redis中的跳躍表和普通的跳躍表有什么區別?
-
Redis中的跳躍表分數(score)允許重復,即跳躍表的key允許重復,如果分數重復,還需要根據數據內容來進字典排序。普通的跳躍表是不支持的。
-
第1層鏈表不是一個單向鏈表,而是一個雙向鏈表。這是為了方便以倒序方式獲取一個范圍內的元素。
-
在Redis的跳躍表中可以很方便地計算出每個元素的排名。
篇幅有限,關于跳表的實現細節就不過多介紹了,有興趣的同學可以自行了解,本小節部分內容參考《Redis設計與實現》
Redis的應用場景有哪些?
-
緩存:Redis基于內存,讀寫速度非常快,并且有鍵過期功能和鍵淘汰策略,可以作為緩存使用。
-
排行榜:Redis提供的有序集合可以很方便地實現排行榜。
-
分布式鎖:Redis的setnx功能來實現分布式的鎖。
-
社交功能:實現共同好友、共同關注等
-
計數器:通過String進行自增自減實現計數功能
-
消息隊列:Redis提供了發布、訂閱、阻塞隊列等功能,可以實現一個簡單的消息隊列。
Redis是單線程的,如何提高CPU的利用率?
可以在一個服務器上部署多個Redis實例,把他們當作不同的服務器使用。
過期鍵的刪除策略
鍵的過期刪除策略
常見的過期刪除策略是惰性刪除、定期刪除、定時刪除。
-
惰性刪除:只有訪問這個鍵時才會檢查它是否過期,如果過期則清除。優點:最大化地節約CPU資源。缺點:如果大量過期鍵沒有被訪問,會一直占用大量內存。
-
定時刪除:為每個設置過期時間的key都創造一個定時器,到了過期時間就清除。優點:該策略可以立即清除過期的鍵。缺點:會占用大量的CPU資源去處理過期的數據。
-
定期刪除:每隔一段時間就對一些鍵進行檢查,刪除其中過期的鍵。該策略是惰性刪除和定時刪除的一個折中,既避免了占用大量CPU資源又避免了出現大量過期鍵不被清除占用內存的情況。
Redis中同時使用了惰性刪除和定期刪除兩種。
Redis的內存淘汰機制是什么樣的?
Redis是基于內存的,所以容量肯定是有限的,有效的內存淘汰機制對Redis是非常重要的。
當存入的數據超過Redis最大允許內存后,會觸發Redis的內存淘汰策略。在Redis4.0前一共有6種淘汰策略。
-
volatile-lru:當Redis內存不足時,會在設置了過期時間的鍵中使用LRU算法移除那些最少使用的鍵。(注:在面試中,手寫LRU算法也是個高頻題,使用雙向鏈表和哈希表作為數據結構)
-
volatile-ttl:從設置了過期時間的鍵中移除將要過期的
-
volatile-random:從設置了過期時間的鍵中隨機淘汰一些
-
allkeys-lru:當內存空間不足時,根據LRU算法移除一些鍵
-
allkeys-random:當內存空間不足時,隨機移除某些鍵
-
noeviction:當內存空間不足時,新的寫入操作會報錯
前三個是在設置了過期時間的鍵的空間進行移除,后三個是在全局的空間進行移除
在Redis4.0后可以增加兩個
-
volatile-lfu:從設置過期時間的鍵中移除一些最不經常使用的鍵(LFU算法:Least Frequently Used))
-
allkeys-lfu:當內存不足時,從所有的鍵中移除一些最不經常使用的鍵
這兩個也是一個是在設置了過期時間的鍵的空間,一個是在全局空間。
Redis的持久化
什么是Redis的持久化?
因為Redis是基于內存的,為了讓防止一些意外情況導致數據丟失,需要將數據持久化到磁盤上。
Redis常見的持久化機制有哪些?有什么有優缺點?
Redis提供了兩種不同的持久化方式,一種是RDB,一種是AOF。
RDB
RDB是redis默認的持久化方式,按照一定的時間間隔將內存的數據以快照的形式保存到硬盤,恢復時是將快照讀取到內存中。RDB持久化實際操作過程是fork一個子進程,先將數據集寫入臨時文件,寫入成功后,再替換之前的文件,用二進制壓縮存儲。如下圖
?
優點:
-
適合對大規模的數據恢復,比AOF的啟動效率高
-
只有一個文件 dump.rdb,方便持久化
-
性能最大化,在開始持久化時,它唯一需要做的只是fork出子進程,之后再由子進程完成這些持久化的工作,這樣就可以極大的避免服務進程執行IO操作了。
缺點:
-
數據安全性低,在一定間隔時間內做一次備份,如果Redis突然宕機,會丟失最后一次快照的修改
-
由于RDB是通過fork子進程來協助完成數據持久化工作的,因此當數據集較大時,可能會導致整個服務器停止服務幾百毫秒,甚至是1秒鐘。
AOF
AOF持久化以日志的形式記錄服務器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文本的方式記錄,可以打開文件看到詳細的操作記錄。
?
優點:
-
具備更高的安全性,Redis提供了3種同步策略,分別是每秒同步、每修改同步和不同步。相比RDB突然宕機丟失的數據會更少,每秒同步會丟失一秒種的數據,每修改同步會不會丟失數據。
-
由于該機制對日志文件的寫入操作采用的是append模式,因此在寫入過程中即使出現宕機現象,也不會破壞日志文件中已經存在的內容。
-
AOF包含一個格式清晰、易于理解的日志文件用于記錄所有的修改操作,可以通過該文件完成數據的重建。
缺點:
-
對于相同數量的數據集而言,AOF文件通常要大于RDB文件。RDB 在恢復大數據集時的速度比 AOF 的恢復速度要快。
-
根據AOF選擇同步策略的不同,效率也不同,但AOF在運行效率上往往會慢于RDB。
Redis的事務
什么是Redis的事務
Redis的事務是一個單獨的隔離操作,事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷,所以Redis事務是在一個隊列中,一次性、順序性、排他性地執行一系列命令。
Redis 事務的主要作用就是串聯多個命令防止別的命令插隊。
Redis事務的相關命令
Redis事務相關的命令主要有以下幾種:
-
DISCARD:命令取消事務,放棄執行事務隊列內的所有命令,恢復連接為非 (transaction) 模式,如果正在使用 WATCH 命令監視某個(或某些) key,那么取消所有監視,等同于執行命令 UNWATCH 。
-
EXEC:執行事務隊列內的所有命令。
-
MULTI:用于標記一個事務塊的開始。
-
UNWATCH:用于取消 WATCH命令對所有 key 的監視。如果已經執行過了EXEC或DISCARD命令,則無需再執行UNWATCH命令,因為執行EXEC命令時,開始執行事務,WATCH命令也會生效,而 DISCARD命令在取消事務的同時也會取消所有對 key 的監視,所以不需要再執行UNWATCH命令了
-
WATCH:用于標記要監視的key,以便有條件地執行事務,WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之后的事務就不會執行。
Redis事務執行的三個階段
開始事務(MULTI)
命令入列
執行事務(EXEC)
Redis事務的特性
-
Redis事務不保證原子性,單條的Redis命令是原子性的,但事務不能保證原子性。
-
Redis事務是有隔離性的,但沒有隔離級別,事務中的所有命令都會序列化、按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。(順序性、排他性)
-
Redis事務不支持回滾,Redis執行過程中的命令執行失敗,其他命令仍然可以執行。(一次性)
Redis事務為什么不支持回滾?
在Redis的事務中,命令允許失敗,但是Redis會繼續執行其它的命令而不是回滾所有命令,是不支持回滾的。
主要原因有以下兩點:
-
Redis 命令只在兩種情況失敗:
-
語法錯誤的時候才失敗(在命令輸入的時候不檢查語法)。
-
要執行的key數據類型不匹配:這種錯誤實際上是編程錯誤,這應該在開發階段被測試出來,而不是生產上。
-
-
因為不需要回滾,所以Redis內部實現簡單并高效。(在Redis為什么是單線程而不是多線程也用了這個思想,實現簡單并且高效)
Redis的集群、主從、哨兵
Redis集群的實現方案有哪些?
在說Redis集群前,先說下為什么要使用Redis集群,Redis單機版主要有以下幾個缺點:
-
不能保證數據的可靠性,服務部署在一臺服務器上,一旦服務器宕機服務就不可用。
-
性能瓶頸,內存容量有限,處理能力有限
Redis集群就是為了解決Redis單機版的一些問題,Redis集群主要有以下幾種方案
-
Redis 主從模式
-
Redis 哨兵模式
-
Redis 自研
-
Redis Clustert
下面對這幾種方案進行簡單地介紹:
Redis主從模式
Redis單機版通過RDB或AOF持久化機制將數據持久化到硬盤上,但數據都存儲在一臺服務器上,并且讀寫都在同一服務器(讀寫不分離),如果硬盤出現問題,則會導致數據不可用,為了避免這種問題,Redis提供了復制功能,在master數據庫中的數據更新后,自動將更新的數據同步到slave數據庫上,這就是主從模式的Redis集群,如下圖
?
主從模式解決了Redis單機版存在的問題,但其本身也不是完美的,主要優缺點如下:
優點:
-
高可靠性,在master數據庫出現故障后,可以切換到slave數據庫
-
讀寫分離,slave庫可以擴展master庫節點的讀能力,有效應對大并發量的讀操作
缺點:
-
不具備自動容錯和恢復能力,主節點故障,從節點需要手動升為主節點,可用性較低
Redis 哨兵模式
為了解決主從模式的Redis集群不具備自動容錯和恢復能力的問題,Redis從2.6版本開始提供哨兵模式
哨兵模式的核心還是主從復制,不過相比于主從模式,多了一個競選機制(多了一個哨兵集群),從所有從節點中競選出主節點,如下圖
?
從上圖中可以看出,哨兵模式相比于主從模式,主要多了一個哨兵集群,哨兵集群的主要作用如下:
-
監控所有服務器是否正常運行:通過發送命令返回監控服務器的運行狀態,處理監控主服務器、從服務器外,哨兵之間也相互監控。
-
故障切換:當哨兵監測到master宕機,會自動將slave切換成master,然后通過發布訂閱模式通知其他的從服務器,修改配置文件,讓它們切換master。同時那臺有問題的舊主也會變為新主的從,也就是說當舊的主即使恢復時,并不會恢復原來的主身份,而是作為新主的一個從。
哨兵模式的優缺點:
優點:
-
哨兵模式是基于主從模式的,解決可主從模式中master故障不可以自動切換故障的問題。
缺點:
-
浪費資源,集群里所有節點保存的都是全量數據,數據量過大時,主從同步會嚴重影響性能
-
Redis主機宕機后,投票選舉結束之前,誰也不知道主機和從機是誰,此時Redis也會開啟保護機制,禁止寫操作,直到選舉出了新的Redis主機。
-
只有一個master庫執行寫請求,寫操作會單機性能瓶頸影響
Redis 自研
哨兵模式雖然解決了主從模式存在的一些問題,但其本身也存在一些弊端,比如數據在每個Redis實例中都是全量存儲,極大地浪費了資源,為了解決這個問題,Redis提供了Redis Cluster,實現了數據分片存儲,但Redis提供Redis Cluster之前,很多公司為了解決哨兵模式存在的問題,分別自行研發Redis集群方案。
客戶端分片
客戶端分片是把分片的邏輯放在Redis客戶端實現,通過Redis客戶端預先定義好的路由規則(使用哈希算法),把對Key的訪問轉發到不同的Redis實例中,查詢數據時把返回結果匯集。如下圖
?
客戶端分片的優缺點:
優點:Redis實例彼此獨立,相互無關聯,每個Redis實例像單服務器一樣運行,非常容易線性擴展,系統的靈活性很強。
缺點:
-
客戶端sharding不支持動態增刪節點。服務端Redis實例群拓撲結構有變化時,每個客戶端都需要更新調整。
-
運維成本比較高,集群的數據出了任何問題都需要運維人員和開發人員一起合作,減緩了解決問題的速度,增加了跨部門溝通的成本。
-
在不同的客戶端程序中,維護相同的路由分片邏輯成本巨大。比如:java項目、PHP項目里共用一套Redis集群,路由分片邏輯分別需要寫兩套一樣的邏輯,以后維護也是兩套。
代理分片
客戶端分片的最大問題就是服務端Redis實例群拓撲結構有變化時,每個客戶端都需要更新調整。
為了解決這個問題,代理分片出現了,代理分片將客戶端分片模塊單獨分了出來,作為Redis客戶端和服務端的橋梁,如下圖
?
代理模式的優點:解決了服務端Redis實例群拓撲結構有變化時,每個客戶端都需要更新調整的問題。缺點是由于Redis客戶端的每個請求都經過代理才能到達Redis服務器,這個過程中會產生性能損失。
常見的代理分片有Twitter開源的Redis代理Twemproxy和豌豆莢自主研發的Codis
Redis Cluster
前面介紹了為了解決哨兵模式的問題,各大企業提出了一些數據分片存儲的方案,在Redis3.0中,Redis也提供了響應的解決方案,就是Redist Cluster。
Redis Cluster是一種服務端Sharding技術,Redis Cluster并沒有使用一致性hash,而是采用slot(槽)的概念,一共分成16384個槽。將請求發送到任意節點,接收到請求的節點會將查詢請求發送到正確的節點上執行。
什么是Redis哈希槽呢?本來不想詳細介紹這個的,但面試確實經常問,還是簡單說一下,
在介紹slot前,要先介紹下一致性哈希(客戶端分片經常會用的哈希算法),那這個一致性哈希有什么用呢?其實就是用來保證節點負載均衡的,那么多主節點,到底要把數據存到哪個主節點上呢?就可以通過一致性哈希算法要把數據存到哪個節點上。
一致性哈希
下面詳細說下一致性哈希算法,首先就是對key計算出一個hash值,然后對2^32取模,也就是值的范圍在[0, 2^32 -1],一致性哈希將其范圍抽象成了一個圓環,使用CRC16算法計算出來的哈希值會落到圓環上的某個地方。
現在將Redis實例也分布在圓環上,如下圖(圖片來源于網絡)
?
假設A、B、C三個Redis實例按照如圖所示的位置分布在圓環上,通過上述介紹的方法計算出key的hash值,發現其落在了位置E,按照順時針,這個key值應該分配到Redis實例A上。
如果此時Redis實例A掛了,會繼續按照順時針的方向,之前計算出在E位置的key會被分配到RedisB,而其他Redis實例則不受影響。
但一致性哈希也不是完美的,主要存在以下問題:當Redis實例節點較少時,節點變化對整個哈希環中的數據影響較大,容易出現部分節點數據過多,部分節點數據過少的問題,出現數據傾斜的情況,如下圖(圖片來源于網絡),數據落在A節點和B節點的概率遠大于B節點
?
為了解決這種問題,可以對一致性哈希算法引入虛擬節點(A#1,B#1,C#1),如下圖(圖片來源于網絡),
?
那這些虛擬節點有什么用呢?每個虛擬節點都會映射到真實節點,例如,計算出key的hash值后落入到了位置D,按照順時針順序,應該落到節點C#1這個虛擬節點上,因為虛擬節點會映射到真實節點,所以數據最終存儲到節點C。
虛擬槽
在Redis Cluster中并沒有使用一致性哈希,而引進了虛擬槽。虛擬槽的原理和一致性哈希很像,Redis Cluster一共有2^14(16384)個槽,所有的master節點都會有一個槽范圍比如0~1000,槽數是可以遷移的。master節點的slave節點不分配槽,只擁有讀權限,其實虛擬槽也可以看成一致性哈希中的虛擬節點。
虛擬槽和一致性哈希算法的實現上也很像,先通過CRC16算法計算出key的hash值,然后對16384取模,得到對應的槽位,根據槽找到對應的節點,如下圖。
?
使用虛擬槽的好處:
-
更加方便地添加和移除節點,增加節點時,只需要把其他節點的某些哈希槽挪到新節點就可以了,當需要移除節點時,只需要把移除節點上的哈希槽挪到其他節點就行了,不需要停掉Redis任何一個節點的服務(采用一致性哈希需要增加和移除節點需要rehash)
Redis Cluster 結構
上面介紹了Redis Cluster如何將數據分配到合適的節點,下面來介紹下Redis Cluster結構,簡單來說,Redis Cluster可以看成多個主從架構組合在一起,如下圖
?
上圖看起來比較亂,其實很好理解,上圖中一個Redis Cluster有兩組節點組成(官方推薦,一般至少三主三從六個節點,畫多個節點看起來太亂,所以上圖只畫了兩個主節點),每組節點可以看成一個主從模式,并且每組節點負責的slot不同(假設有4個slot,A組節點負責第1個和第二個slot,B組節點負責第3個和第4個,其中master節點負責寫,slave節點負責讀)
上圖中共有三種線
主備復制的線很好理解,就和主從模式一樣,在master庫中的數據更新后,自動將更新的數據同步到slave庫上
對外服務的線也很好理解,就是外部在對Redis進行讀取操作,訪問master進行寫操作,訪問slave進行讀操作
Redis Bus的作用相對復雜些,這里簡單說下,
首先要知道Redis Cluster是一個去中心化的架構,不存在統一的配置中心,Redis Cluster中的每個節點都保存了集群的配置信息,在Redis Cluster中,這個配置信息通過Redis Cluster Bus進行交互,并最后達成一致性。
配置信息的一致性主要依靠PING/PONG,每個節點向其他節點頻繁的周期性的發送PING/PONG消息。對于大規模的集群,如果每次PING/PONG 都攜帶著所有節點的信息,則網絡開銷會很大。此時Redis Cluster 在每次PING/PONG,只包含了隨機的一部分節點信息。由于交互比較頻繁,短時間的幾次交互之后,集群的狀態也會達成一致。
當Cluster 結構不發生變化時,各個節點通過gossip 協議(Redis ?Cluster各個節點之間交換數據、通信所采用的一種協議)在幾輪交互之后,便可以得知Cluster的結構信息,達到一致性的狀態。但是當集群結構發生變化時(故障轉移/分片遷移等),優先得知變更的節點會將自己的最新信息擴散到Cluster,并最終達到一致。
其實,上面說了半天也不太容易理解,簡單來說Redis Bus是用于節點之間的信息交路,交互的信息有以下幾個:
-
數據分片(slot)和節點的對應關系(對應上圖中的slot1和slot2在masterA節點上,就是要知道哪個slot在哪個節點上)
-
集群中每個節點可用狀態(不斷向其他節點發消息看看你掛了沒)
-
集群結構發生變更時,通過一定的協議對配置信息達成一致。數據分片的遷移、主備切換、單點master的發現和其發生主備關系變更等,都會導致集群結構變化。(發現有的節點掛了或者有新的節點加進來了,趕緊和其他節點同步信息)
Redis主從架構中數據丟失嗎
Redis主從架構丟失主要有兩種情況
-
異步復制同步丟失
-
集群產生腦裂數據丟失
下面分別簡單介紹下這兩種情況:
異步復制同步丟失:
Redis主節點和從節點之間的復制是異步的,當主節點的數據未完全復制到從節點時就發生宕機了,master內存中的數據會丟失。
如果主節點開啟持久化是否可以解決這個問題呢?
答案是否定的,在master 發生宕機后,sentinel集群檢測到主節點發生故障,重新選舉新的主節點,如果舊的主節點在故障恢復后重啟,那么此時它需要同步新主節點的數據,此時新的主節點的數據是空的(假設這段時間中沒有數據寫入)。那么舊主機點中的數據就會被刷新掉,此時數據還是會丟失。
集群產生腦裂:
簡單來說,集群腦裂是指一個集群中有多個主節點,像一個人有兩個大腦,到底聽誰的呢?
例如,由于網絡原因,集群出現了分區,master與slave節點之間斷開了聯系,哨兵檢測后認為主節點故障,重新選舉從節點為主節點,但主節點可能并沒有發生故障。此時客戶端依然在舊的主節點上寫數據,而新的主節點中沒有數據,在發現這個問題之后,舊的主節點會被降為slave,并且開始同步新的master數據,那么之前的寫入舊的主節點的數據被刷新掉,大量數據丟失。
如何解決主從架構數據丟失問題?
在Redis的配置文件中,有兩個參數如下:
min-slaves-to-write?1 min-slaves-max-lag?10其中,min-slaves-to-write默認情況下是0,min-slaves-max-lag默認情況下是10。
上述參數表示至少有1個salve的與master的同步復制延遲不能超過10s,一旦所有的slave復制和同步的延遲達到了10s,那么此時master就不會接受任何請求。
通過降低min-slaves-max-lag參數的值,可以避免在發生故障時大量的數據丟失,一旦發現延遲超過了該值就不會往master中寫入數據。
這種解決數據丟失的方法是降低系統的可用性來實現的。
Redis集群的主從復制過程是什么樣的?
主從復制主要有以下幾步:
設置服務器的地址和端口號
建立套接字連接(建立主從服務器之間的連接)
發送PING命令(檢驗套接字是否可用)
身份驗證
同步(從主庫向從庫同步數據,分為全量復制和部分復制)
命令傳播(經過上面同步操作,此時主從的數據庫狀態其實已經一致了,許主服務器馬上就接受到了新的寫命令,執行完該命令后,主從的數據庫狀態又不一致。數據同步階段完成后,主從節點進入命令傳播階段;在這個階段主節點將自己執行的寫命令發送給從節點,從節點接收命令并執行,從而保證主從節點數據的一致性)
簡單解釋下全量復制和部分復制:
在Redis2.8以前,從節點向主節點發送sync命令請求同步數據,此時的同步方式是全量復制;在Redis2.8及以后,從節點可以發送psync命令請求同步數據,此時根據主從節點當前狀態的不同,同步方式可能是全量復制或部分復制。
-
全量復制:用于初次復制或其他無法進行部分復制的情況,將主節點中的所有數據都發送給從節點,是一個非常重型的操作。
-
部分復制:用于網絡中斷等情況后的復制,只將中斷期間主節點執行的寫命令發送給從節點,與全量復制相比更加高效。需要注意的是,如果網絡中斷時間過長,導致主節點沒有能夠完整地保存中斷期間執行的寫命令,則無法進行部分復制,仍使用全量復制。
Redis是如何保證主從服務器一致處于連接狀態以及命令是否丟失?
命令傳播階段,從服務器會利用心跳檢測機制定時的向主服務發送消息。
因為網絡原因在主從復制過程中停止復制會怎么樣?
如果出現網絡問題斷開,會自動重連,并且支持斷點續傳,接著上次復制的地方繼續復制,而不是重新復制一份。
下面說下其中的實現細節,首先需要了解replication buffer和replication backlog
replication buffer:主庫連接的每一個從庫的對應一個 replication buffer,主庫執行完每一個操作命令后,會將命令分別寫入每一個從庫所對應的 replication buffer
replication backlog:replication backlog 是一個環形區域,大小可以通過?repl-backlog-size參數設置,并且和 replication buffer不同的是,一個主庫中只有一個 replication backlog。
?
主庫會通過?master_repl_offset?記錄寫入的位置,從庫會通過?slave_repl_offset?記錄自己讀取的位置,這里的位置也叫做偏移量。
剛開始復制數據的時候,上述兩者的值相同,且都位于初始位置。每當主庫向 replication backlog 寫入一個操作,master_repl_offset?就會增加 1,從庫復制完操作命令后,slave_repl_offset?也會增加 1。
正常情況下,slave_repl_offset?會跟著?master_repl_offset?變化,兩者保持一個小的差距,或者相等。
如果主從庫之間的網絡連接中斷,從庫便無法繼續復制主庫的操作命令,但是主庫依然會向 replication backlog 中寫入操作命令。
當網絡恢復之后,從庫會繼續向主庫請求同步數據,主庫通過slave_repl_offset知道從庫復制操作命令的位置。這個時候,主庫只需要把?master_repl_offset?和?slave_repl_offset?之間的操作命令同步給從庫就可以了。
但是前面提到 replication backlog 是一個環形結構,如果網絡中斷的時間過長,隨著主庫不斷向其中寫入操作命令,master_repl_offset?不斷增大,就會有從庫沒有復制的操作命令被覆蓋。如果出現這種情況,就需要重新進行全量復制了。
為了避免全量復制的情況,可以通過修改?repl-backlog-size?參數的值,為 replication backlog 設置合適的大小。
這個值需要結合實際情況來設置,具體思路是:從命令在主庫中產生到從庫復制完成所需要的時間為 t,每秒鐘產生的命令的數量為 c,命令的大小為 s,這個值不能低于他們的乘積。考慮到突發的網絡壓力以及系統運行過程中可能出現的阻塞等情況,應該再將這個值乘以 2 或者更多。
此處參考博客:https://juejin.cn/post/7017827613544546335
了解Redis哈希槽嗎?
見Redis Cluster處
Redi集群最大的節點個數是多少?為什么?
16384個
Redis作者的回答:
-
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
-
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
上面主要說了兩點,
在redis節點發送心跳包時需要把所有的槽放到這個心跳包里,以便讓節點知道當前集群信息,16384=16k,在發送心跳包時使用char進行bitmap壓縮后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是說使用2k的空間創建了16k的槽數。雖然使用CRC16算法最多可以分配65535(2^16-1)個槽位,65535=65k,壓縮后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是說需要8k的心跳包,作者認為這樣做不太值得
由于其他設計折衷,一般情況下一個redis集群不會有超過1000個master節點
Redis集群是如何選擇數據庫的?
Redis 集群目前無法做數據庫選擇,默認在 0 數據庫。
Redis高可用方案如何實現?
常見的Redis高可用方案有以下幾種:
-
數據持久化
-
主從模式
-
Redis 哨兵模式
-
Redis 集群(自研及Redis Cluster)
Redis的分區
Redis的分區作用是什么?
-
擴展數據庫容量,可以利用多臺機器的內存構建更大的數據庫
-
擴展計算能力,分區可以在多核和多計算機之間彈性擴展計算能力,在多計算機和網絡適配器之間彈性擴展網絡帶寬
Redis分區有哪些實現方案?
在介紹Redis集群的實現方案時已經介紹過了客戶端分區和代理分區,常見的Redis分區方案主要有以下三種:
-
客戶端分區:客戶端決定數據被存到哪個Redis節點或者從哪個節點讀取
-
代理分區:客戶端將請求發送到代理,而不是直接發送到Redis節點,代理根據分區策略將請求發送到Redis節點上
-
查詢路由:客戶端隨機請求任意一個Redis節點,這個Redis節點將請求轉發到正確的Redis節點。Redis Cluster實現了一種混合形式的查詢路由,并不是直接將請求從一個Redis節點轉發到另一個Redis節點,而是在客戶端的幫助下直接重定向到正確的redis節點
Redis分區的缺點?
-
不支持多個鍵的操作,例如不能操作映射在兩個Redis實例上的兩個集合的交叉集。(其實可以做到這一點,但是需要間接的解決)
-
Redis不支持多個鍵的事務
-
Redis是以鍵來分區,因此不能使用單個大鍵對數據集進行分片,例如一個非常大的有序集
-
數據的處理會變得復雜,比如你必須處理多個RDB和AOF文件,在多個實例和主機之間持久化你的數據
-
添加和刪除節點也會變得復雜,例如通過在運行時添加和刪除節點,Redis集群通常支持透明地再均衡數據,但是其他系統像客戶端分區或者代理分區的特性就不支持該特性。不過Pre-sharding(預分片)可以在這方面提供幫助。
Redis的分布式問題
什么是分布式鎖?
相信大家對程序中的鎖并不陌生,無論是在并發編程或者Java虛擬機都有學到過。
鎖在程序中的作用主要是同步,就是保證共享資源在同一時刻只能被同一個線程訪問。
分布式鎖則是為了保證在分布式場景下,共享資源在同一時刻只能被同一個線程訪問,或者說是用來控制分布式系統之間同步訪問共享資源。舉個簡單例子,如下圖
?
從上圖可以看出,變量A在三個服務器中都有涉及到,如果不對其進行控制的話,三個服務器中的變量A很難做到同步,解決這個問題的方法就是分布式鎖。
分布式鎖具有哪些特性?
-
互斥性:在任意時刻,同一條數據只能被一臺機器上的一個線程執行
-
高可用性:當部分節點宕機后,客戶端仍可以正常地獲取鎖和釋放鎖
-
獨占性:加鎖和解鎖必須同一臺服務器執行,不能在一個服務器上加鎖,在另一個服務器上釋放鎖
-
防鎖超時:如果客戶端沒有主動釋放鎖,服務器會在一定時間后自動釋放鎖, 防止客戶端宕機或者網絡異常導致宕機
分布式鎖的實現方法?
基本思路就是要在整個系統中提供一個全局、唯一的獲取鎖的“東西”,然后每個系統在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統拿到的就可以認為是同一把鎖。
常見的分布式鎖實現方案有三種:
基于關系型數據庫:
優點:直接借助數據庫容易理解
缺點:在使用關系型數據庫實現分布式鎖的過程中會出現各種問題,例如數據庫單點問題和可重入問題,并且在解決過程中會使得整個方案越來越復雜
基于Redis:
優點:性能好,實現起來較為方便
缺點:
-
key的過期時間設置難以確定,如何設置的失效時間太短,方法沒等執行完,鎖就自動釋放了,那么就會產生并發問題。如果設置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間。
-
Redis的集群部署雖然能解決單點問題,但是并不是強一致性的,鎖的不夠健壯
基于zookeeper:
優點:有效地解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題,實現起來較為簡單。
缺點:性能上不如使用緩存實現分布式鎖
三種方案的對比
| 基于關系型數據庫 | 低 | 低 | 低 | 低 |
| 基于Redis | 中 | 高 | 中 | 中 |
| 基于zookeeper | 高 | 中 | 高 | 高 |
Redis如何實現分布式鎖?
前面講過了分布式鎖的特性,其實實現分布式鎖就是圍繞著這些展開的
Redis實現分布式鎖的主要命令:SETNX,該命令的作用是當key不存在時設置key的值,當Key存在時,什么都不做。
先來看最簡單的實現方式,如下圖
?
從上圖可以看到主要兩個關鍵步驟,加鎖和解鎖。
但是這個簡陋的分布式鎖存在很多問題,并不能滿足上述介紹的分布式鎖的特性,
比如,當線程1執行到上圖中執行業務這步時,業務代碼突然出現異常了,無法進行刪除鎖這一步,那就完犢子了,死鎖了,其他線程也無法獲取到鎖了(因為SETNX的特性)。
改進方案1
那咋整呢?
一提到異常,有人想起了try-catch-finally了,把刪除鎖的操作放到finally代碼塊中,就算出現異常,也是能正常釋放鎖的,執行業務出現異常這個問題確實解決了。但這玩意并不靠譜,如果Redis在執行業務這步宕機了呢,finally代碼塊也不會執行了。
改進方案2
其實這個問題很好解決,只需給鎖設置一個過期時間就可以了,對key設置過期時間在Redis中是常規操作了。就是這個命令SET key value [EX seconds][PX milliseconds] [NX|XX]
-
EX second: 設置鍵的過期時間為second秒;
-
PX millisecond:設置鍵的過期時間為millisecond毫秒;
-
NX:只在鍵不存在時,才對鍵進行設置操作;
-
XX:只在鍵已經存在時,才對鍵進行設置操作;
-
SET操作完成時,返回OK,否則返回nil。
那先現在這個方案就完美了嗎?顯然沒有
例如,線程1獲取到了鎖,并設置了有效時間10秒,但線程1在執行業務時超過了10秒,鎖到期自動釋放了,在鎖釋放后,線程2又獲取了鎖,在線程2執行業務時,線程1執行完了,隨后執行了刪除鎖這一步,但是線程1的鎖早就到期自動釋放了,他刪除的是線程2的鎖!!!
上面這個例子說的有點亂,突然想到一個現實生活中的例子,把Redis比作一個廁所,張三去上廁所,關上了門(獲取鎖,并設置了10秒),上到一半(10秒到了,門自動開了),這時李四進去了,關上了門(獲取鎖,并設置了10秒),張三上完了廁所,把門打開了走了(執行了刪除鎖操作)
上面這個荒誕的例子說明了方案2有兩處不合理的地方,第一是張三廁所沒上完,李四怎么能進去呢?他們上廁所產生了沖突,第二是張三上完廁所怎么能打開李四的門呢(分布式鎖的特性,加鎖和解鎖必須同一臺服務器執行)?
改進方案3
其實看起來方案2的問題很容易解決,只需要把鎖的過期時間設置的非常長,就可以避免這兩個問題,但是這樣并不可行,因為這樣相當于回到最簡陋的方案(會導致李四一直上不到廁所)。
那如何能讓李四上到廁所,還不會讓自己鎖的門被張三打開門呢?
很簡單,為鎖加一個標識,例如生成一個UUID,作為鎖的標識,每個線程獲取鎖時都會生成一個不同的UUID作為鎖的標識,在進行刪除鎖時會進行判斷,鎖的標識和自己生成UUID相等時才進行刪除操作,這樣就避免線程1釋放了線程2的鎖。(相當于自己上自己的鎖,不要計較為什么張三在李四上廁所時不需要李四的鑰匙就能離開廁所這種事,上廁所和分布式鎖邏輯并不完全相同,只是簡單類比)
那怎么解決李四未等張三上完廁所就進廁所呢?(如何確定鎖的過期時間)
可以在加鎖時,先設置一個預估的過期時間,然后開啟一個守護線程,定時去檢測這個鎖的失效時間,如果鎖快要過期了,操作共享資源還未完成,那么就自動對鎖進行續期,重新設置過期時間。
好了,張三和李四上廁所的解決了。
那方案3就沒有其他問題了嗎?其實還是有的,比如目前的分布式鎖還不具備可重入性(同一線程可以重復獲取鎖,解決線程需要多次進入鎖內執行任務的問題)
改進方案4
參考其他重入鎖的實現,可以通過對鎖進行重入計數,加鎖時加 1,解鎖時減 1,當計數歸 0 時才能釋放鎖。
那現在方案就沒有問題了嗎,其實還有
比如,線程1獲取了鎖,線程2沒有獲取到鎖,那么線程2怎么知道線程1啥時候釋放了鎖,進而再去獲取鎖呢?
改進方案5
方案4中問題的解決方案,一般以下兩種解決方案:
-
可以通過客戶端輪詢的方式,就是線程2過一會就來看看是不是能獲取鎖了。這種方式比較消耗服務器資源,當并發量比較大時,會影響服務器的效率。
-
通過Redis的發布訂閱功能,當獲取鎖失敗時,訂閱鎖釋放消息,獲取鎖成功后釋放時,發送鎖釋放消息。
那現在這個方案完美了嗎?也還沒有
目前討論的都是redis是單節點的情況,如果這個節點掛了,那么所有的客戶端都獲取不到鎖了
改進方案6
為了實現多節點Redis的分布式鎖,Redis的作者提出了RedLock算法。
這是RedLock算法官網的地址,https://redis.io/topics/distlock,英文好的建議直接看官方文檔,畢竟我這四六級飄過的人也可能理解的不準確,下面的內容主要是參考官網內容。
首先介紹保證分布式鎖的有效性和安全性的要求:
-
互斥性:在任何給定時刻,只有一個客戶端可以持有一個鎖
-
釋放死鎖:即使鎖定資源的客戶端崩潰或被分區,也可以釋放鎖
-
容錯性:只要大多數Redis節點都在運行,客戶端就能夠獲取和釋放鎖。
為什么基于故障轉移實現的Redis分布式鎖還不夠用?
官網中舉了一個例子:
客戶端A獲得主服務器上的鎖,然后主服務器向從服務器復制數據的過程中崩了,導致數據沒有復制到從數據庫中,這時會在從服務器中選出來一個升級為主服務器,但新的主服務器中并沒有客戶端A設置的鎖。所以客戶端B也可以獲取到鎖,違背了上面說的互斥性
這就解釋為什么需要RedLock算法
RedLock算法
假設有5個完全獨立的Redis服務器,多節點Redis實現的RedLock算法如下
-
獲取當前時間戳
-
客戶端嘗試在5個實例中按順序獲取鎖,在所有實例中使用相同的鍵名和隨機值。當在每個實例中設置鎖時,需要將鎖的獲取時間設置為比鎖過期短很多。例如,如果鎖自動釋放時間為10秒,則鎖的獲取時間在5-50毫秒。這是為了不要過長時間等待已經關閉的Redis實例,如果一個Redis實例不可用,我們應該盡快嘗試獲取下一個Redis實例的鎖。
-
客戶端通過從當前時間中減去步驟1中獲得的時間戳,計算出獲取鎖所需的時間。當且僅當客戶端能夠在大多數實例(至少3個)中獲得鎖,并且花費在獲取鎖的總時間小于鎖的有效性時間時,該鎖被認為已經獲得。
-
如果獲得了鎖,鎖真正的有效時間為鎖初始設置的有效時間(過期時間)減去第三步的時間,例如,鎖初始有限時間為5s,獲取鎖花了0.5s,則鎖真正的有效時間為4.5s(忽略了時鐘漂移,時間漂移指指兩個電腦間時間流速基本相同的情況下,兩個電腦(或兩個進程間)時間的差值)
-
如果客戶端由于某些原因無法獲得鎖(要么無法鎖定N/2+1個Redis實例,要么有鎖的效性時間為負數),客戶端將嘗試解鎖所有Redis實例(即使是它認為無法鎖定的Redis實例)。
RedLock算法是異步的嗎?
可以看成同步算法,雖然沒有跨進程的同步時鐘,但每個進程(多個電腦)的本地時間仍然大致以相同的速度流動,與鎖的自動釋放時間相比,誤差較小,將其忽略的話,則可以看成同步算法。
RedLock失敗重試
當客戶端無法獲取到鎖時,應該在隨機時間后重試,并且理想的客戶端應該并發地將所有命令用時發給所有Redis實例。對于已經獲取鎖的客戶端要在完成任務后及時釋放鎖,這樣其他客戶端就不需要等鎖自動過期后在獲取。如果在獲取鎖后,在主動釋放鎖前無法連接到Redis實例,就只能等鎖自動失效了。
釋放鎖
釋放鎖很簡單,只要釋放所有實例中的鎖,不需要考慮是否釋放成功(釋放時會判斷這個鎖的value值是不是自己設置的,避免釋放其他客戶端設置的鎖)
RedLock的 Safety arguments
-
假設客戶端可以獲取到大多數Redis實例,并且所有Redis實例具有相同的key和過期時間,但不同的Redis實例的key是不同的時間設置的(獲取鎖的時間不可能完全一致),所以過期時間也不同,假設獲取第一個Redis實例的鎖的時間為T1,最后一個為T2,則客戶端獲得鎖的最小有效時間為key的有效時間-(T2-T1)-時鐘漂移。
-
為什么需要獲取一半以上的Redis實例的鎖才算獲取到鎖成功呢?因為如果獲取不到一半也算成功的話會導致多個客戶端同時獲取到鎖,違背了互斥性
-
一個客戶端鎖定大多數Redis實例所需的時間大于或者接近鎖的過期時間時,會認為鎖無效,并解鎖所有Redis實例
RedLock崩潰的相關解決方法
場景:客戶端A在成功獲取鎖后,如果所有Redis重啟,這時客戶端B就可以再次獲取到鎖,違背了互斥性
解決方法:開啟AOF持久化,可以解決這個問題,但是AOF同步到磁盤上的方式默認是每秒一次,如果1秒內斷電,會導致1秒內的數據丟失,如果客戶端是在這1秒內獲得的鎖,立即重啟可能會導致鎖的互斥性失效,解決方法是每次Redis無論因為什么原因停掉都要等key的過期時間到了在重啟(延遲重啟),這么做的缺點就是在等待重啟這段時間內Redis處于關閉的狀態。
最后,上面的方案6還有其他問題嗎?其實還是有的,不過還有一種更適合Java的強大方案是Redisson,有興趣的小伙伴可以去了解下
Redis并發競爭key問題應該如何解決?
Redis并發競爭key就是多個客戶端操作一個key,可能會導致數據出現問題,主要有以下幾種解決辦法:
-
樂觀鎖,watch?命令可以方便的實現樂觀鎖。watch?命令會監視給定的每一個key,當?exec?時如果監視的任一個key自從調用watch后發生過變化,則整個事務會回滾,不執行任何動作。不能在分片集群中使用
-
分布式鎖,適合分布式場景
-
時間戳,適合有序場景,比如A想把key設置為1,B想把key設置為2,C想把key設置為3,對每個操作加上時間戳,寫入前先比較自己的時間戳是不是早于現有記錄的時間戳,如果早于,就不寫入了
-
消息隊列,串行化處理
什么是RedLock
見上文Redis實現分布式鎖的方案6
Redis的緩存問題
說下什么是緩存雪崩、緩存穿透、緩存擊穿,及它們的解決方案
這是一個非常高頻的面試題,也非常容易掌握,比較麻煩的是總是分不清這三個哪個是哪個
下圖是一個正常的系統架構圖,其中緩存的作用是減輕數據庫的壓力,提升系統的性能,無論是緩存雪崩、緩存擊穿還是緩存穿透都是緩存失效了導致數據庫壓力過大。
緩存雪崩
什么是緩存雪崩?
緩存雪崩是指在某一個時刻出現大規模的緩存失效的情況,大量的請求直接打在數據庫上面,可能會導致數據庫宕機,如果這時重啟數據庫并不能解決根本問題,會再次造成緩存雪崩。
為什么會造成緩存雪崩?
一般來說,造成緩存雪崩主要有兩種可能
-
Redis宕機了
-
很多key采取了相同的過期時間
如何解決緩存雪崩?
-
為避免Redis宕機造成緩存雪崩,可以搭建Redis集群
-
盡量不要設置相同的過期時間,例如可以在原有的過期時間加上隨機數
-
服務降級,當流量到達一定的閾值時,就直接返回“系統繁忙”之類的提示,防止過多的請求打在數據庫上,這樣雖然難用,但至少可以使用,避免直接把數據庫搞掛
緩存擊穿
什么是緩存擊穿?
緩存雪崩是大規模的key失效,而緩存擊穿是一個熱點的Key,有大并發集中對其進行訪問,突然間這個Key失效了,導致大并發全部打在數據庫上,導致數據庫壓力劇增,這種現象就叫做緩存擊穿。
比較經典的例子是商品秒殺時,大量的用戶在搶某個商品時,商品的key突然過期失效了,所有請求都到數據庫上了。
如何解決緩存擊穿
-
慮熱點key不設置過期時間,避免key過期失效
-
加鎖,如果緩存失效的情況,只有拿到鎖才可以查詢數據庫,降低了在同一時刻打在數據庫上的請求,防止數據庫宕機,不過這樣會導致系統的性能變差。
緩存穿透
什么是緩存穿透
緩存穿透是指用戶的請求沒有經過緩存而直接請求到數據庫上了,比如用戶請求的key在Redis中不存在,或者用戶惡意偽造大量不存在的key進行請求,都可以繞過緩存,導致數據庫壓力太大掛掉。
如何解決緩存穿透
-
參數校驗,例如可以對用戶id進行校驗,直接攔截不合法的用戶的請求
-
布隆過濾器,布隆過濾器可以判斷這個key在不在數據庫中,特點是如果判斷這個key不在數據庫中,那么這個key一定不在數據庫中,如果判斷這個key在數據庫中,也不能保證這個key一定在數據庫中。就是會有少數的漏網之魚,造成這種現象的原因是因為布隆過濾器中使用了hash算法,對key進行hash時,不同的key的hash值一定不同,但相同的hash的值不能說明這兩個key相同。下面簡單介紹下布隆過濾器,這個面試也常問。
布隆過濾器底層使用bit數組存儲數據,該數組中的元素默認值是0。
布隆過濾器第一次初始化的時候,會把數據庫中所有已存在的key,經過一系列的hash算法計算,算出每個key的位置,并將該位置的值置為1,為了減少哈希沖突的影響,可以對每個key進行多次hash計算,如下圖
?
現在,用戶所有的請求都要經過布隆過濾器過濾一遍,如果只有用戶請求的key的hash值都是1才可以通過,否則直接攔截,如下圖
?
如何保證緩存與數據庫雙寫時的數據一致性?
這是面試的高頻題,需要好好掌握,這個問題是沒有最優解的,只能數據一致性和性能之間找到一個最適合業務的平衡點
首先先來了解下一致性,在分布式系統中,一致性是指多副本問題中的數據一致性。一致性可以分為強一致性、弱一致性和最終一致性
-
強一致性:當更新操作完成之后,任何多個后續進程或者線程的訪問都會返回最新的更新過的值。強一致性對用戶比較友好,但對系統性能影響比較大。
-
弱一致性:系統并不保證后續進程或者線程的訪問都會返回最新的更新過的值。
-
最終一致性:也是弱一致性的一種特殊形式,系統保證在沒有后續更新的前提下,系統最終返回上一次更新操作的值。
大多數系統都是采用的最終一致性,最終一致性是指系統中所有的副本經過一段時間的異步同步之后,最終能夠達到一個一致性的狀態,也就是說在數據的一致性上存在一個短暫的延遲。
如果想保證緩存和數據庫的數據一致性,最簡單的想法就是同時更新數據庫和緩存,但是這實現起來并不現實,常見的方案主要有以下幾種:
-
先更新數據庫,后更新緩存
-
先更新緩存,后更新數據庫
-
先更新數據庫,后刪除緩存
-
先刪除緩存,后更新數據庫
乍一看,感覺第一種方案就可以實現緩存和數據庫一致性,其實不然,更新緩存是個坑,一般不會有更新緩存的操作。因為很多時候緩存中存的值不是直接從數據庫直接取出來放到緩存中的,而是經過一系列計算得到的緩存值,如果數據庫寫操作頻繁,緩存也會頻繁更改,所以更新緩存代價是比較大的,并且更改后的緩存也不一定會被訪問就又要重新更改了,這樣做無意義的性能消耗太大了。下面介紹刪除緩存的方案
先更新數據庫,后刪除緩存
這種方案也存在一個問題,如果更新數據庫成功了,刪除緩存時沒有成功,那么后面每次讀取緩存時都是錯誤的數據。
解決這個問題的辦法是刪除重試機制,常見的方案有利用消息隊列和數據庫的日志
利用消息隊列實現刪除重試機制,如下圖
?
步驟在圖中寫的已經比較清除了,這里簡單說下為什么使用消息隊列,消息隊列可以保證寫到隊列中的消息在成功消費之前不會消失,并且在第4步中獲取消息時只有消費成功才會刪除消息,否則會繼續投遞消息給應用程序,符合消息重試的要求。
但這個方案也有一些缺點,比如系統復雜度高,對業務代碼入侵嚴重,這時可以采用訂閱數據庫日志的方法刪除緩存。如下圖
?
先刪除緩存,后更新數據庫
這種方案也存在一些問題,比如在并發環境下,有兩個請求A和B,A是更新操作,B是查詢操作
假設A請求先執行,會先刪除緩存中的數據,然后去更新數據庫
B請求查詢緩存發現為空,會去查詢數據庫,并把這個值放到緩存中
在B查詢數據庫時A還沒有完全更新成功,所以B查詢并放到緩存中的是舊的值,并且以后每次查詢緩存中的值都是錯誤的舊值
這種情況的解決方法通常是采用延遲雙刪,就是為保證A操作已經完成,最后再刪除一次緩存
?
邏輯很簡單,刪除緩存后,休眠一會兒再刪除一次緩存,雖然邏輯看起來簡單,但實現起來并不容易,問題就出在延遲時間設置多少合適,延遲時間一般大于B操作讀取數據庫+寫入緩存的時間,這個只能是估算,一般可以考慮讀業務邏輯數據的耗時 + 幾百毫秒。
在實際應用中,還是先更新數據庫后刪除緩存這種方案用的多些。
需要注意的是,無論哪種方案,如果數據庫采取讀寫分離+主從復制延遲的話,即使采用先更新數據庫后刪除緩存也會出現類似先刪除緩存后更新數據庫中出現的問題,舉個例子
A操作更新主庫后,刪除了緩存
B操作查詢緩存沒有查到數據,查詢從庫拿到舊值
主庫將新值同步到從庫
B操作將拿到的舊值寫入緩存
這就造成了緩存中的是舊值,數據庫中的是新值,解決方法還是上面說的延遲雙刪,延遲時間要大于主從復制的時間
Redis其他高頻面試題
一個字符串類型的值能存儲最大容量是多少?
一個字符串類型鍵允許存儲的數據的最大容量是512MB
Redis如何實現大量數據插入?
這個問題在Redis的官方文檔給出了答案,從Redis 2.6開始redis-cli支持一種新的被稱之為pipe mode的新模式用于執行大量數據插入工作。具體可以看官網的詳細解釋,這里就不再復制粘貼了:https://www.redis.com.cn/topics/mass-insert.html
如何通過Redis實現異步隊列?
主要有兩種方式
第一種是使用List作為隊列,通過RPUSH生產消息, LPOP消費消息
存在的問題:如果隊列是空的,客戶端會不停的pop,陷入死循環
解決方法:
-
當lpop沒有消息時,可以使用sleep機制先休眠一段時間,然后再檢查有沒有消息。
-
可以使用blpop命令,在隊列沒有數據的時候,會立即進入休眠狀態,一旦數據到來,則立刻醒過來。這種做法的缺點是只能提供一個消費者消費
第二種方法是pub/sub主題訂閱模式,發送者(pub)發送消息,訂閱者(sub)接收消息
存在的問題:消息的發布是無狀態的,無法保證到達,如果訂閱者在發送者發布消息時掉線,之后上線也無法接收發布者發送的消息
解決方法:使用消息隊列
如何通過Redis實現延時隊列?
先說下延時隊列的使用場景:
-
常見的微信紅包場景,A給B發紅包,B沒有收,1天后錢會退回原賬戶
-
電商的訂單支付場景,訂單在半小時內未支付會自動取消
上述場景可以通過定時任務采用數據庫/非關系型數據庫輪詢方案或延遲隊列,現主要介紹下Redis實現的延遲隊列
可以通過Redis的zset命令實現延遲隊列,ZSET是Redis的有序集合,通過zadd score1 value1命令向內存中生產消息,并利用設置好的時間戳作為score進行排序,然后通過zrangebysocre 查詢符合條件的所有待處理的任務,循環執行,也可以zrangebyscore key min max withscores limit 0 1?查詢最早的一條任務,來進行消費,如下圖(畫的第二種,好畫點)
?
Redis回收使用什么算法?
LRU算法和引用計數法
LRU算法很常見,在學習操作系統時也經常看到,淘汰最長時間沒有被使用的對象,LRU算法在手撕代碼環節也經常出現,要提前背熟
引用計數法在學習JVM中也見過的,對于創建的每一個對象都有一個與之關聯的計數器,這個計數器記錄著該對象被使用的次數,當對象被一個新程序使用時,它的引用計數值會被增1,當對象不再被一個程序使用時,它的引用計數值會被減1,垃圾收集器在進行垃圾回收時,對掃描到的每一個對象判斷一下計數器是否等于0,若等于0,就會釋放該對象占用的內存空間,簡單來說就是淘汰使用次數最少的對象(LFU算法)
Redis 里面有1億個 key,其中有 10 個 key 是包含 java,如何將它們全部找出來?
可以使用Redis的KEYS命令,用于查找所有匹配給定模式 pattern 的 key ,雖然時間復雜度為O(n),但常量時間相當小。
注意: 生產環境使用 KEYS命令需要非常小心,在大的數據庫上執行命令會影響性能,KEYS指令會導致線程阻塞一段時間,線上服務會停頓,直到指令執行完畢,服務才能恢復。這個命令適合用來調試和特殊操作,像改變鍵空間布局。
不要在你的代碼中使用 KEYS 。如果你需要一個尋找鍵空間中的key子集,考慮使用 SCAN 或 sets。
生產環境中的Redis是如何部署的
這個按自己得情況說就行了,但得提前想好怎么說,避免忘了
如果本文對你有幫助,別忘記給我個3連 ,點贊,轉發,評論,
咱們下期見!答案獲取方式:已贊 已評 已關~
學習更多知識與技巧,關注與私信博主(03)
?
總結
以上是生活随笔為你收集整理的Redis高频面试题完整版的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 醉翁亭记--欧阳修
- 下一篇: centos 7 下 硬盘GPT格式转换