1. 程式人生 > 其它 >redis_10 _ 第1~9講課後思考題答案及常見問題答疑

redis_10 _ 第1~9講課後思考題答案及常見問題答疑

咱們的課程已經更新9講了,這段時間,我收到了很多留言。很多同學都認真地回答了課後思考題,有些回答甚至可以說是標準答案。另外,還有很多同學針對Redis的基本原理和關鍵機制,提出了非常好的問題,值得好好討論一下。

今天,我就和你聊一聊課後題答案,並且挑選一些典型問題,集中進行一次講解,希望可以解決你的困惑。

課後思考題答案

第1講

問題:和跟Redis相比,SimpleKV還缺少什麼?

@曾軾麟、@Kaito 同學給出的答案都非常棒。他們從資料結構到功能擴充套件,從記憶體效率到事務性,從高可用叢集再到高可擴充套件叢集,對SimpleKV和Redis進行了詳細的對比。而且,他們還從運維使用的角度進行了分析。我先分享一下兩位同學的答案。

@曾軾麟同學:

  1. 資料結構:缺乏廣泛的資料結構支援,比如支援範圍查詢的SkipList和Stream等資料結構。
  2. 高可用:缺乏哨兵或者master-slave模式的高可用設計;
  3. 橫向擴充套件:缺乏叢集和分片功能;
  4. 記憶體安全性:缺乏記憶體過載時的key淘汰演算法的支援;
  5. 記憶體利用率:沒有充分對資料結構進行優化,提高記憶體利用率,例如使用壓縮性的資料結構;
  6. 功能擴充套件:需要具備後續功能的拓展;
  7. 不具備事務性:無法保證多個操作的原子性。

@Kaito同學:

SimpleKV所缺少的有:豐富的資料型別、支援資料壓縮、過期機制、資料淘汰策略、主從複製、叢集化、高可用叢集等,另外,還可以增加統計模組、通知模組、除錯模組、元資料查詢等輔助功能。

我也給個答案總結。還記得我在開篇詞講過的“兩大維度”“三大主線”嗎?這裡我們也可以藉助這個框架進行分析,如下表所示。此外,在表格最後,我還從鍵值資料庫開發和運維的輔助工具上,對SimpleKV和Redis做了對比。

第2講

問題:整數陣列和壓縮列表作為底層資料結構的優勢是什麼?

整數陣列和壓縮列表的設計,充分體現了Redis“又快又省”特點中的“省”,也就是節省記憶體空間。整數陣列和壓縮列表都是在記憶體中分配一塊地址連續的空間,然後把集合中的元素一個接一個地放在這塊空間內,非常緊湊。因為元素是挨個連續放置的,我們不用再通過額外的指標把元素串接起來,這就避免了額外指標帶來的空間開銷。

我畫一張圖,展示下這兩個結構的記憶體佈局。整數陣列和壓縮列表中的entry都是實際的集合元素,它們一個挨一個儲存,非常節省記憶體空間。

Redis之所以採用不同的資料結構,其實是在效能和記憶體使用效率之間進行的平衡。

第3講

問題:Redis基本IO模型中還有哪些潛在的效能瓶頸?

這個問題是希望你能進一步理解阻塞操作對Redis單執行緒效能的影響。在Redis基本IO模型中,主要是主執行緒在執行操作,任何耗時的操作,例如bigkey、全量返回等操作,都是潛在的效能瓶頸。

第4講

問題1:AOF重寫過程中有沒有其他潛在的阻塞風險?

這裡有兩個風險。

風險一:Redis主執行緒fork建立bgrewriteaof子程序時,核心需要建立用於管理子程序的相關資料結構,這些資料結構在作業系統中通常叫作程序控制塊(Process Control Block,簡稱為PCB)。核心要把主執行緒的PCB內容拷貝給子程序。這個建立和拷貝過程由核心執行,是會阻塞主執行緒的。而且,在拷貝過程中,子程序要拷貝父程序的頁表,這個過程的耗時和Redis例項的記憶體大小有關。如果Redis例項記憶體大,頁表就會大,fork執行時間就會長,這就會給主執行緒帶來阻塞風險。

風險二:bgrewriteaof子程序會和主執行緒共享記憶體。當主執行緒收到新寫或修改的操作時,主執行緒會申請新的記憶體空間,用來儲存新寫或修改的資料,如果操作的是bigkey,也就是資料量大的集合型別資料,那麼,主執行緒會因為申請大空間而面臨阻塞風險。因為作業系統在分配記憶體空間時,有查詢和鎖的開銷,這就會導致阻塞。

問題2:AOF 重寫為什麼不共享使用 AOF 本身的日誌?

如果都用AOF日誌的話,主執行緒要寫,bgrewriteaof子程序也要寫,這兩者會競爭檔案系統的鎖,這就會對Redis主執行緒的效能造成影響。

第5講

問題:使用一個 2 核 CPU、4GB 記憶體、500GB 磁碟的雲主機執行 Redis,Redis 資料庫的資料量大小差不多是 2GB。當時 Redis主要以修改操作為主,寫讀比例差不多在 8:2 左右,也就是說,如果有 100 個請求,80 個請求執行的是修改操作。在這個場景下,用 RDB 做持久化有什麼風險嗎?

@Kaito同學的回答從記憶體資源和CPU資源兩方面分析了風險,非常棒。我稍微做了些完善和精簡,你可以參考一下。

記憶體不足的風險:Redis fork一個bgsave子程序進行RDB寫入,如果主執行緒再接收到寫操作,就會採用寫時複製。寫時複製需要給寫操作的資料分配新的記憶體空間。本問題中寫的比例為80%,那麼,在持久化過程中,為了儲存80%寫操作涉及的資料,寫時複製機制會在例項記憶體中,為這些資料再分配新記憶體空間,分配的記憶體量相當於整個例項資料量的80%,大約是1.6GB,這樣一來,整個系統記憶體的使用量就接近飽和了。此時,如果例項還有大量的新key寫入或key修改,雲主機記憶體很快就會被吃光。如果雲主機開啟了Swap機制,就會有一部分資料被換到磁碟上,當訪問磁碟上的這部分資料時,效能會急劇下降。如果雲主機沒有開啟Swap,會直接觸發OOM,整個Redis例項會面臨被系統kill掉的風險。

主執行緒和子程序競爭使用CPU的風險:生成RDB的子程序需要CPU核執行,主執行緒本身也需要CPU核執行,而且,如果Redis還啟用了後臺執行緒,此時,主執行緒、子程序和後臺執行緒都會競爭CPU資源。由於雲主機只有2核CPU,這就會影響到主執行緒處理請求的速度。

第6講

問題:為什麼主從庫間的複製不使用 AOF?

答案:有兩個原因。

  1. RDB檔案是二進位制檔案,無論是要把RDB寫入磁碟,還是要通過網路傳輸RDB,IO效率都比記錄和傳輸AOF的高。
  2. 在從庫端進行恢復時,用RDB的恢復效率要高於用AOF。

第7講

問題1:在主從切換過程中,客戶端能否正常地進行請求操作呢?

主從叢集一般是採用讀寫分離模式,當主庫故障後,客戶端仍然可以把讀請求傳送給從庫,讓從庫服務。但是,對於寫請求操作,客戶端就無法執行了。

問題2:如果想要應用程式不感知服務的中斷,還需要哨兵或客戶端再做些什麼嗎?

一方面,客戶端需要能快取應用傳送的寫請求。只要不是同步寫操作(Redis應用場景一般也沒有同步寫),寫請求通常不會在應用程式的關鍵路徑上,所以,客戶端快取寫請求後,給應用程式返回一個確認就行。

另一方面,主從切換完成後,客戶端要能和新主庫重新建立連線,哨兵需要提供訂閱頻道,讓客戶端能夠訂閱到新主庫的資訊。同時,客戶端也需要能主動和哨兵通訊,詢問新主庫的資訊。

第8講

問題1:5個哨兵例項的叢集,quorum值設為2。在執行過程中,如果有3個哨兵例項都發生故障了,此時,Redis主庫如果有故障,還能正確地判斷主庫“客觀下線”嗎?如果可以的話,還能進行主從庫自動切換嗎?

因為判定主庫“客觀下線”的依據是,認為主庫“主觀下線”的哨兵個數要大於等於quorum值,現在還剩2個哨兵例項,個數正好等於quorum值,所以還能正常判斷主庫是否處於“客觀下線”狀態。如果一個哨兵想要執行主從切換,就要獲到半數以上的哨兵投票贊成,也就是至少需要3個哨兵投票贊成。但是,現在只有2個哨兵了,所以就無法進行主從切換了。

問題2:哨兵例項是不是越多越好呢?如果同時調大down-after-milliseconds值,對減少誤判是不是也有好處?

哨兵例項越多,誤判率會越低,但是在判定主庫下線和選舉Leader時,例項需要拿到的贊成票數也越多,等待所有哨兵投完票的時間可能也會相應增加,主從庫切換的時間也會變長,客戶端容易堆積較多的請求操作,可能會導致客戶端請求溢位,從而造成請求丟失。如果業務層對Redis的操作有響應時間要求,就可能會因為新主庫一直沒有選定,新操作無法執行而發生超時報警。

調大down-after-milliseconds後,可能會導致這樣的情況:主庫實際已經發生故障了,但是哨兵過了很長時間才判斷出來,這就會影響到Redis對業務的可用性。

第9講

問題:為什麼Redis不直接用一個表,把鍵值對和例項的對應關係記錄下來?

如果使用表記錄鍵值對和例項的對應關係,一旦鍵值對和例項的對應關係發生了變化(例如例項有增減或者資料重新分佈),就要修改表。如果是單執行緒操作表,那麼所有操作都要序列執行,效能慢;如果是多執行緒操作表,就涉及到加鎖開銷。此外,如果資料量非常大,使用表記錄鍵值對和例項的對應關係,需要的額外儲存空間也會增加。

基於雜湊槽計算時,雖然也要記錄雜湊槽和例項的對應關係,但是雜湊槽的個數要比鍵值對的個數少很多,無論是修改雜湊槽和例項的對應關係,還是使用額外空間儲存雜湊槽和例項的對應關係,都比直接記錄鍵值對和例項的關係的開銷小得多。

好了,這些問題你都回答上來了嗎?如果你還有其他想法,也歡迎多多留言,跟我和其他同學進行交流討論。

典型問題講解

接下來,我再講一些代表性問題,包括Redis rehash的時機和執行機制,主執行緒、子程序和後臺執行緒的聯絡和區別,寫時複製的底層實現原理,以及replication buffer和repl_backlog_buffer的區別。

問題1:rehash的觸發時機和漸進式執行機制

我發現,很多同學對Redis的雜湊表資料結構都很感興趣,尤其是雜湊表的rehash操作,所以,我再集中回答兩個問題。

1.Redis什麼時候做rehash?

Redis會使用裝載因子(load factor)來判斷是否需要做rehash。裝載因子的計算方式是,雜湊表中所有entry的個數除以雜湊表的雜湊桶個數。Redis會根據裝載因子的兩種情況,來觸發rehash操作:

  • 裝載因子≥1,同時,雜湊表被允許進行rehash;
  • 裝載因子≥5。

在第一種情況下,如果裝載因子等於1,同時我們假設,所有鍵值對是平均分佈在雜湊表的各個桶中的,那麼,此時,雜湊表可以不用鏈式雜湊,因為一個雜湊桶正好儲存了一個鍵值對。

但是,如果此時再有新的資料寫入,雜湊表就要使用鏈式雜湊了,這會對查詢效能產生影響。在進行RDB生成和AOF重寫時,雜湊表的rehash是被禁止的,這是為了避免對RDB和AOF重寫造成影響。如果此時,Redis沒有在生成RDB和重寫AOF,那麼,就可以進行rehash。否則的話,再有資料寫入時,雜湊表就要開始使用查詢較慢的鏈式雜湊了。

在第二種情況下,也就是裝載因子大於等於5時,就表明當前儲存的資料量已經遠遠大於雜湊桶的個數,雜湊桶裡會有大量的鏈式雜湊存在,效能會受到嚴重影響,此時,就立馬開始做rehash。

剛剛說的是觸發rehash的情況,如果裝載因子小於1,或者裝載因子大於1但是小於5,同時雜湊表暫時不被允許進行rehash(例如,例項正在生成RDB或者重寫AOF),此時,雜湊表是不會進行rehash操作的。

2.採用漸進式hash時,如果例項暫時沒有收到新請求,是不是就不做rehash了?

其實不是的。Redis會執行定時任務,定時任務中就包含了rehash操作。所謂的定時任務,就是按照一定頻率(例如每100ms/次)執行的任務。

在rehash被觸發後,即使沒有收到新請求,Redis也會定時執行一次rehash操作,而且,每次執行時長不會超過1ms,以免對其他任務造成影響。

問題2:主執行緒、子程序和後臺執行緒的聯絡與區別

我在課程中提到了主執行緒、主程序、子程序、子執行緒和後臺執行緒這幾個詞,有些同學可能會有疑惑,我再幫你總結下它們的區別。

首先,我來解釋一下程序和執行緒的區別。

從作業系統的角度來看,程序一般是指資源分配單元,例如一個程序擁有自己的堆、棧、虛存空間(頁表)、檔案描述符等;而執行緒一般是指CPU進行排程和執行的實體。

瞭解了程序和執行緒的區別後,我們再來看下什麼是主程序和主執行緒。

如果一個程序啟動後,沒有再建立額外的執行緒,那麼,這樣的程序一般稱為主程序或主執行緒。

舉個例子,下面是我寫的一個C程式片段,main函式會直接呼叫一個worker函式,函式worker就是執行一個for迴圈計算。下面這個程式執行後,它自己就是一個主程序,同時也是個主執行緒。

int counter = 0;
void *worker() {
for (int i=0;i<10;i++) {
counter++;
}
return NULL;
}

int main(int argc, char *argv[]) {
worker();
}

和這段程式碼類似,Redis啟動以後,本身就是一個程序,它會接收客戶端傳送的請求,並處理讀寫操作請求。而且,接收請求和處理請求操作是Redis的主要工作,Redis沒有再依賴於其他執行緒,所以,我一般把完成這個主要工作的Redis程序,稱為主程序或主執行緒。

在主執行緒中,我們還可以使用fork建立子程序,或是使用pthread_create建立執行緒。下面我先介紹下Redis中用fork建立的子程序有哪些。

  • 建立RDB的後臺子程序,同時由它負責在主從同步時傳輸RDB給從庫;
  • 通過無盤複製方式傳輸RDB的子程序;
  • bgrewriteaof子程序。

然後,我們再看下Redis使用的執行緒。從4.0版本開始,Redis也開始使用pthread_create建立執行緒,這些執行緒在建立後,一般會自行執行一些任務,例如執行非同步刪除任務。相對於完成主要工作的主執行緒來說,我們一般可以稱這些執行緒為後臺執行緒。關於Redis後臺執行緒的具體執行機制,我會在第16講具體介紹。

為了幫助你更好地理解,我畫了一張圖,展示了它們的區別。

問題3:寫時複製的底層實現機制

Redis在使用RDB方式進行持久化時,會用到寫時複製機制。我在第5節課講寫時複製的時候,著重介紹了寫時複製的效果:bgsave子程序相當於複製了原始資料,而主執行緒仍然可以修改原來的資料。

今天,我再具體講一講寫時複製的底層實現機制。

對Redis來說,主執行緒fork出bgsave子程序後,bgsave子程序實際是複製了主執行緒的頁表。這些頁表中,就儲存了在執行bgsave命令時,主執行緒的所有資料塊在記憶體中的實體地址。這樣一來,bgsave子程序生成RDB時,就可以根據頁表讀取這些資料,再寫入磁碟中。如果此時,主執行緒接收到了新寫或修改操作,那麼,主執行緒會使用寫時複製機制。具體來說,寫時複製就是指,主執行緒在有寫操作時,才會把這個新寫或修改後的資料寫入到一個新的實體地址中,並修改自己的頁表對映。

我來藉助下圖中的例子,具體展示一下寫時複製的底層機制。

bgsave子程序複製主執行緒的頁表以後,假如主執行緒需要修改虛頁7裡的資料,那麼,主執行緒就需要新分配一個物理頁(假設是物理頁53),然後把修改後的虛頁7裡的資料寫到物理頁53上,而虛頁7裡原來的資料仍然儲存在物理頁33上。這個時候,虛頁7到物理頁33的對映關係,仍然保留在bgsave子程序中。所以,bgsave子程序可以無誤地把虛頁7的原始資料寫入RDB檔案。

問題4:replication buffer和repl_backlog_buffer的區別

在進行主從複製時,Redis會使用replication buffer和repl_backlog_buffer,有些同學可能不太清楚它們的區別,我再解釋下。

總的來說,replication buffer是主從庫在進行全量複製時,主庫上用於和從庫連線的客戶端的buffer,而repl_backlog_buffer是為了支援從庫增量複製,主庫上用於持續儲存寫操作的一塊專用buffer。

Redis主從庫在進行復制時,當主庫要把全量複製期間的寫操作命令發給從庫時,主庫會先建立一個客戶端,用來連線從庫,然後通過這個客戶端,把寫操作命令發給從庫。在記憶體中,主庫上的客戶端就會對應一個buffer,這個buffer就被稱為replication buffer。Redis通過client_buffer配置項來控制這個buffer的大小。主庫會給每個從庫建立一個客戶端,所以replication buffer不是共享的,而是每個從庫都有一個對應的客戶端。

repl_backlog_buffer是一塊專用buffer,在Redis伺服器啟動後,開始一直接收寫操作命令,這是所有從庫共享的。主庫和從庫會各自記錄自己的複製進度,所以,不同的從庫在進行恢復時,會把自己的複製進度(slave_repl_offset)發給主庫,主庫就可以和它獨立同步。

好了,這節課就到這裡。非常感謝你的仔細思考和提問,每個問題都很精彩,在看留言的過程中,我自己也受益匪淺。另外,我希望我們可以組建起一個Redis學習團,在接下來的課程中,歡迎你繼續在留言區暢所欲言,我們一起進步,希望每個人都能成為Redis達人!