Redis核心技術基礎(一)

一、數據結構

1,數據結構

  Redis表現突出的原因: 1、在內存中進行操作 2、高效的數據結構(降低複雜度)

  Redis的存儲接口主要有:String、List、Hash、Set和Sorted Set(Redis6.0之前)。底層結構一共有6種:簡單動態字符串、雙向鏈表、壓縮列表、哈希表、跳錶和整數數組。

   

 2,K-V的存儲

  Redis使用哈希表來保存所有的鍵值對。一個哈希表其實就是一個數組,數組的每個元素稱爲一個哈希桶。一個哈希表是由多個哈希桶組成的,每個哈希桶中保存了鍵值對數據。

  如圖,哈希桶中的 entry 元素中保存了*key和*value指針,分別指向了實際的鍵和值,這樣一來,即使值是一個集合,也可以通過*value指針被查找到。

  

   因爲哈希表保存了所有的鍵值對,所以也稱之爲全局哈希表。哈希表可以用O(1)的時間複雜度快速查找到鍵值對——只需要計算建的哈希值即可定位到哈希桶的位置,然後訪問相應的entry元素。

3,哈希表操作變慢

a>哈希衝突

  當往哈希表中寫入很多數據時,哈希衝突是不可避免的。解決方式:Redis採用鏈式哈希,同一個哈希桶中的多個元素用一個鏈表來保存,它們之間依次用指針連接

  

  這裏依然存在一個問題,哈希衝突鏈上的元素只能通過指針逐一查找再操作。如果哈希表裏寫入的數據越來越多,哈希衝突可能也會越來越多,這就會導致某些哈希衝突鏈過長,進而導致這個鏈上的元素查找耗時長,效率降低。

 b>漸進式rehash

  Redis對哈希表做rehash操作就是增加現有的哈希桶數量,讓逐漸增多的entry元素能在更多的桶之間分散保存,減少單桶的衝突數量,提升查詢效率。

  Redis默認使用兩個全局哈希表(哈希表1和哈希表2)處理rehash,具體步驟:a.給哈希表 2 分配更大的空間,例如是當前哈希表 1 大小的兩倍;b.把哈希表 1 中的數據重新映射並拷貝到哈希表 2 中;c.釋放哈希表 1 的空間。

  漸進式rehash:但是第二步涉及大量的數據拷貝,如果一次性把哈希表 1 中的數據都遷移完,會造成 Redis 線程阻塞,所以爲了處理這個問題就採用了漸進式rehash方式。

  簡單來說就是在第二步拷貝數據時,Redis 仍然正常處理客戶端請求,每處理一個請求時,從哈希表 1 中的第一個索引位置開始,順帶着將這個索引位置上的所有 entries 拷貝到哈希表 2 中;等處理下一個請求時,再順帶拷貝哈希表 1 中的下一個索引位置的 entries。如下圖所示:

  

 4,集合數據操作效率

a>底層數據結構

  集合類型的底層數據結構主要有5種:整數數組、雙向鏈表、哈希表、壓縮列表和跳錶。

壓縮列表

  壓縮列表實際上類似於一個數組,數組中的每一個元素都對應保存一個數據。和數組不同的是,壓縮列表在表頭有三個字段zlbytes、zltail和zllen,分別表示:列表長度、列尾的偏移量和entry個數;在表尾還有zlend表示列表結束

  

跳錶

  有序鏈表只能逐一查找元素,導致操作起來非常緩慢,於是就出現了跳錶。具體來說,跳錶在鏈表的基礎上,增加了多級索引,通過索引位置的幾個跳轉,實現數據的快速定位,如下圖所示:

  

數據結構時間複雜度

  

 b>操作複雜度

  1. 單元素操作,是指每一種集合類型對單個數據實現的增刪改查操作。例如,Hash 類型的 HGET、HSET 和 HDEL,Set 類型的 SADD、SREM、SRANDMEMBER 等。這些操作的複雜度由集合採用的數據結構決定,例如,HGET、HSET 和 HDEL 是對哈希表做操作,所以它們的複雜度都是 O(1);Set 類型用哈希表作爲底層數據結構時,它的 SADD、SREM、SRANDMEMBER 複雜度也是 O(1)。
  2. 範圍操作,是指集合類型中的遍歷操作,可以返回集合中的所有數據,比如 Hash 類型的 HGETALL 和 Set 類型的 SMEMBERS,或者返回一個範圍內的部分數據,比如 List 類型的 LRANGE 和 ZSet 類型的 ZRANGE。這類操作的複雜度一般是 O(N),比較耗時,我們應該儘量避免。
  3. 統計操作,是指集合類型對集合中所有元素個數的記錄,例如 LLEN 和 SCARD。這類操作複雜度只有 O(1),這是因爲當集合類型採用壓縮列表、雙向鏈表、整數數組這些數據結構時,這些結構中專門記錄了元素的個數統計,因此可以高效地完成相關操作。
  4. 例外情況,是指某些數據結構的特殊記錄,例如壓縮列表和雙向鏈表都會記錄表頭和表尾的偏移量。這樣一來,對於 List 類型的 LPOP、RPOP、LPUSH、RPUSH 這四個操作來說,它們是在列表的頭尾增刪元素,這就可以通過偏移量直接定位,所以它們的複雜度也只有 O(1),可以實現快速操作

二、高性能IO模型

1,Redis爲什麼是單線程?

  對於一個多線程的系統來說,在有合理的資源分配情況下,可以增加系統中處理請求操作的資源實體,進而提升系統能夠同時處理的請求數,即吞吐率。但是在實際使用中,剛開始增加線程數時,系統吞吐率會增加,但是,再進一步增加線程時,系統吞吐率就增長遲緩了,有時甚至還會出現下降的情況。

  

 

 

   原因在於系統中通常會存在被多線程同時訪問的共享資源,比如一個共享的數據結構。當有多個線程要修改這個共享資源時,爲了保證共享資源的正確性,就需要額外的機制進行保證,這樣就會帶來額外的開銷。以Redis爲例,Redis中的List數據類型,有出隊(LPOP)和入隊(LPUSH)操作,假設採用多線程設計,現在有兩個線程 A 和 B,線程 A 對一個 List 做 LPUSH 操作,並對隊列長度加 1。同時,線程 B 對該 List 執行 LPOP 操作,並對隊列長度減 1。爲了保證隊列長度的正確性,Redis 需要讓線程 A 和 B 的 LPUSH 和 LPOP 串行執行,這樣一來,Redis 可以無誤地記錄它們對 List 長度的修改。這就是多線程編程模式面臨的共享資源的併發訪問控制問題

2,單線程Redis爲什麼那麼快?

  Redis單線程模型能達到每秒數十萬的處理能力取決於:1.Redis的大部分操作在內存上完成,並採用了高效的數據結構;2.Redis採用了多路複用機制,使其在網絡IO操作中能併發處理大量的客戶端請求,實現高吞吐率。

a>基本IO模型與阻塞點

  以Get爲例,網絡全流程如下圖,其中bind/listen、accept、recv、parse和send屬於網絡IO處理,而get屬於鍵值數據操作。

  

 

   有潛在的阻塞點,分別是 accept() 和 recv()。當 Redis 監聽到一個客戶端有連接請求,但一直未能成功建立起連接時,會阻塞在 accept() 函數這裏,導致其他客戶端無法和 Redis 建立連接。類似的,當 Redis 通過 recv() 從一個客戶端讀取數據時,如果數據一直沒有到達,Redis 也會一直阻塞在 recv()。由於socket 網絡模型本身支持非阻塞模式,可以有效規避這個問題。

b>非阻塞模式

  Socket 網絡模型的非阻塞模式設置,主要體現在三個關鍵的函數調用上。socket()方法會返回主動套接字,然後調用listen()方法,將主動套接字轉化爲監聽套接字,最後調用accept()方法接收到達的客戶端連接,並返回已連接套接字。

   

 

   針對監聽套接字,我們可以設置非阻塞模式:當 Redis 調用 accept() 但一直未有連接請求到達時,Redis 線程可以返回處理其他操作,而不用一直等待。雖然 Redis 線程可以不用繼續等待,但是總得有機制繼續在監聽套接字上等待後續連接請求,並在有請求時通知 Redis。類似的,我們也可以針對已連接套接字設置非阻塞模式:Redis 調用 recv() 後,如果已連接套接字上一直沒有數據到達,Redis 線程同樣可以返回處理其他操作。

c>基於多路複用的高性能I/O模型

  Linux 中的 IO 多路複用機制是指一個線程處理多個 IO 流,就是我們經常聽到的 select/epoll 機制。簡單來說,在 Redis 只運行單線程的情況下,該機制允許內核中,同時存在多個監聽套接字和已連接套接字。內核會一直監聽這些套接字上的連接請求或數據請求。一旦有請求到達,就會交給 Redis 線程處理,這就實現了一個 Redis 線程處理多個 IO 流的效果。  

  

 

   select/epoll 提供了基於事件的回調機制,即針對不同事件的發生,調用相應的處理函數。select/epoll 一旦監測到 FD 上有請求到達時,就會觸發相應的事件。這些事件會被放進一個事件隊列,Redis 單線程對該事件隊列不斷進行處理。這樣一來,Redis 無需一直輪詢是否有請求實際發生,這就可以避免造成 CPU 資源浪費。同時,Redis 在對事件隊列中的事件進行處理時,會調用相應的處理函數,這就實現了基於事件的回調。因爲 Redis 一直在對事件隊列進行處理,所以能及時響應客戶端請求,提升 Redis 的響應性能。  

四、AOF日誌

1,AOF日誌是如何實現的

  Redis採用寫後日志:1,避免記錄錯誤命令;2,在命令執行後記錄日誌,不會阻塞當前的寫操作

  爲何不做寫前日誌(Write Ahead Log,WAL)?爲了避免額外的檢查開銷,Redis在向AOF裏面記錄日誌的時候,並不會先去對這些命令進行語法檢查。所以,如果先記日誌再執行命令的話,日誌中就有可能記錄了錯誤的命令,Redis在使用日誌恢復數據時,就可能出錯。而寫後日志是先讓系統執行命令,只有命令能執行成功,纔會記錄到日誌中,否則,系統就會直接向客戶端報錯。

  潛在風險:1.剛執行完一個命令,還沒來得及記日誌就宕機了,會有命令和數據的丟失;2.AOF雖然避免了當前命令的阻塞,但可能會給下個操作帶來風險,AOF日誌是在主線程中執行的,如果寫入磁盤時,磁盤壓力大,會導致寫盤很慢,進而導致後續操作阻塞。

2,寫回策略

  • Always,同步寫回:每個寫命令執行完,立馬同步地將日誌寫回磁盤
  • Everysec,每秒寫回:每個寫命令執行完,只是先把日誌寫到AOF文件的內存緩衝區,每隔一秒把緩衝區中的內容寫入磁盤
  • No,操作系統控制的寫回:每個寫命令執行完,只是先把日誌寫到AOF文件的內存緩衝區,由操作系統決定何時將緩衝區內容寫回

  

  AOF文件越來越大,會出現“性能問題”:1.文件系統本身對文件大小有限制,無法保存過大的文件;2.如果文件太大,之後再往裏面追加命令記錄的話,效率會變低;3.如果發生宕機,AOF中記錄的命令要一個個重新執行,過程非常緩慢,會影響Redis的正常使用。

3,AOF重寫機制

  AOF重寫機制就是在重寫時,Redis根據數據庫的現狀創建一個新的AOF文件。重寫機制具有“多變一”功能,也就是舊日誌文件中的多條命令,在重寫後的新日誌中變成一條命令

  AOF重寫會阻塞嗎?與AOF日誌由主線程寫回不同,重寫過程是由後臺子進程bgrewriteaof來完成的,這也是爲了避免阻塞主線程,導致數據庫性能下降。“一個拷貝,兩處日誌”。

  “一個拷貝”:每次執行重寫,主線程fork出後臺的bgrewriteaof子進程,將主線程的內存拷貝一份給bgrewriteaof子進程,子進程在不影響主線程的情況下,逐一把拷貝的數據寫成操作並記入重寫日誌。

  “兩處日誌”:第一處日誌指正在使用的AOF日誌,Redis會把這個操作寫到它的緩衝區。第二處日誌是指新的AOF重寫日誌,這個操作也會被寫到重寫日誌的緩衝區,等到拷貝數據的所有操作記錄重寫完成後,重寫日誌記錄這些最新操作也會寫入新的AOF文件,以保證數據庫最新狀態的記錄,此時則可以用新的AOF文件替代舊文件。

  

  每次 AOF 重寫時,Redis 會先執行一個內存拷貝,用於重寫;然後,使用兩個日誌保證在重寫過程中,新寫入的數據不會丟失。而且,因爲 Redis 採用額外的線程進行數據重寫,所以,這個過程並不會阻塞主線程。

4,何時觸發AOF重寫?

  有兩個配置項在控制AOF重寫的觸發時機:

  • auto-aof-rewrite-min-size: 表示運行AOF重寫時文件的最小大小,默認爲64MB 
  • auto-aof-rewrite-percentage: 這個值的計算方法是:當前AOF文件大小和上一次重寫後AOF文件大小的差值,再除以上一次重寫後AOF文件大小。也就是當前AOF文件比上一次重寫後AOF文件的增量大小,和上一次重寫後AOF文件大小的比值。

   AOF文件大小同時超出上面這兩個配置項時,會觸發AOF重寫。

五、內存快照

1,定義

  所謂內存快照,就是指內存中的數據在某一時刻的狀態記錄

  對於Redis來說,它實現類似照片記錄效果的方式,就是把某一時刻的狀態以文件的形式寫到磁盤上,也就是快照。這樣即使宕機,快照文件也不會丟失,數據的可靠性也就得到了保證。這種快照文件就稱爲RDB文件,Redis DataBase。

  問題:1.快照如何取景(給哪些數據做快照);2.在按快門時,不能亂動(快照是數據如何修改)

2,給哪些內存數據做快照?

  Redis的數據都在內存中,爲了提供所有數據的可靠性保證,它執行的是全量快照,也就是說,把內存中的所有數據都記錄到磁盤中。

  Redis提供了兩個命令來生成RDB文件,分別是save和bgsave

  • save:在主線程中執行,會導致阻塞
  • bgsave:創建一個子線程,專門用於寫入RDB文件,避免了主線程的阻塞,這也是Redis RDB文件生成的默認配置

3,快照時數據能否修改?

  bgsave能避免阻塞。但是避免阻塞和正常處理寫操作並不是一回事。執行bgsave時,主線程的確不會阻塞,可以正常接收請求,但是爲了保證快照的完整性,它只能處理讀操作,因爲不能修改正在執行快照的數據。

  Redis藉助操作系統提供的寫時複製技術(Copy-On-Write,COW),保證在執行快照的同時處理寫操作。

  

   如圖,主線程fork出bgsave的子線程後,此時希望修改鍵值對C爲鍵值對C',那麼這塊數據會被複制一份,生成該數據的副本(鍵值對C')。然後主線程在數據副本上進行修改。同時,bgsave子進程可以繼續把原來的數據(鍵值對C)寫入RDB文件。

4,快照的時間間隔

  如下圖,我們可以在T0時刻做一次快照,然後又在T0+t時刻做一次快照,在這期間,數據塊5和9被修改了。如果在t這段時間內,機器宕機了,那麼只能按照T0時刻的快照進行恢復。數據塊5和9的修改由於沒有快照記錄無法恢復

  

   所以要想盡可能恢復數據,t值就要儘可能小。但是如果t值特別小比如爲每秒一次快照又會出現什麼問題呢?

  雖然bgsave執行時不阻塞主線程,但是如果頻繁地執行全量快照,也會帶來兩方面的開銷:

  • 頻繁將全量數據寫入磁盤,會給磁盤帶來很大的壓力,多個快照競爭有限的磁盤帶寬,前一個快照還沒有做完,後一個又開始做了,容易造成惡性循環
  • bgsave子進程需要通過fork操作從主線程創建出來。雖然,子進程在創建後不會阻塞主線程,但是fork這個創建過程本身會阻塞主線程,而且主線程的內存越大,阻塞時間越長。如果頻繁fork出bgsave子進程,這就會頻繁阻塞主線程了(所以在Redis中如果有一個bgsave在運行,就不會再啓動第二個bgsave子進程)。

5,增量快照

  在第一次做完全量快照後,T1和T2時刻如果再做快照,我們只需要將被修改的數據寫入快照文件即可。但是記住那些數據被修改,需要使用額外的元數據信息來記錄,這會帶來額外的空間開銷問題。

  

   如果有1w個被修改的鍵值對,我們就需要有1w條額外的記錄。引入的額外空間開銷比較大,這對於內存資源寶貴的Redis來說,有些得不償失。

  Redis 4.0提出了混合使用AOF日誌和內存快照的方法:內存快照以一定的頻率執行,在兩次快照之間使用AOF日誌記錄這期間的所有命令。這樣一來,快照不用很頻繁地執行,這就避免了頻繁 fork 對主線程的影響。而且,AOF 日誌也只用記錄兩次快照間的操作,也就是說,不需要記錄所有操作了,因此,就不會出現文件過大的情況了,也可以避免重寫開銷。

  如下圖,T1和T2時刻的修改,用AOF日誌記錄,等到第二次做全量快照時,就可以清空AOF日誌,因爲此時的修改都已經記錄到快照中了,恢復時就不在用日誌了

  

 6,AOF和RDB的選擇問題

  • 數據不能丟失時,內存快照和 AOF 的混合使用是一個很好的選擇;
  • 如果允許分鐘級別的數據丟失,可以只使用 RDB;
  • 如果只用 AOF,優先使用 everysec 的配置選項,因爲它在可靠性和性能之間取了一個平衡。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章