高併發存儲番外篇:Redis 套路,一網打盡

本文內容提要

  1. Redis 爲什麼這麼快
    1.1. 數據結構 SDS 的妙用
    1.2. 性能優良的事件模型驅動
    1.3. 基於內存的操作

  2. Redis 爲什麼這麼靠譜
    2.1. AOF 持久化
    2.2. RDB 持久化
    2.3. Sentinel 高可用

  3. Redis6.x 多線程一覽

  4. Redis 最佳實踐

Part1Redis 爲什麼這麼快

1.1 數據結構 SDS 的妙用

我們知道 redis 的底層是用 c 語言來編寫的,但是,數據結構確沒有直接套用 C 的結構,而是根據 redis 的定位自建了一套數據結構。

C 語言中的字符串結構:

SDS 定義下的字符串結構:

可以看到,相比於 C 語言來說,也就多了幾個字段,分別用來標識空閒空間和當前數據長度,但簡直是神來之筆:

上面的內容以字符串來說明 SDS 和 C 語言數據結構的差異和優勢。順便來看看鏈表、hash 表、跳錶分別被 Redis 設計成了什麼樣的數據結構:

可以看到,Redis 在設計數據結構的時候出發點是一致的。總結起來就是一句話:空間換時間。

用犧牲存儲空間和微小的計算代價,來換取數據的快速操作

1.2 性能優良的事件驅動模式

redis6.x 之前,一直在說單線程如何如之何的好。

那麼,具體單線程體現在哪裏,又是怎麼完成數據讀寫工作的呢?

$ 單線程

關於新版本的多線程模型在後面小節單獨說,這裏先說單線程。

所謂單線程是指對數據的所有操作都是由一個線程按順序挨個執行的,使用單線程可以:

然而,使用了單線程的處理方式,就意味着到達服務端的請求不可能被立即處理。

那麼怎麼來保證單線程的資源利用率和處理效率呢?

$ IO 多路複用和事件驅動

Redis 服務端,從整體上來看,其實是一個事件驅動的程序,所有的操作都以事件的方式來進行。

如圖所示,Redis 的事件驅動架構由套接字、I/O 多路複用、文件事件分派器、事件處理器四個部分組成:

套接字 (Socket),是對網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象。

I/O 多路複用,通過監視多個描述符,當描述符就緒,則通知程序進行相應的操作,來幫助單個線程高效的處理多個連接請求。

Redis 爲每個 IO 多路複用函數都實現了相同的 API,因此,底層實現是可以互換的。

Reids 默認的 IO 多路複用機制是 epoll,和 select/poll 等其他多路複用機制相比,epoll 具有諸多優點:

MOeJjq

事件驅動,Redis 設計的事件分爲兩種,文件事件和時間事件,文件事件是對套接字操作的抽象,而時間事件則是對一些定時操作的抽象。

文件事件:

時間事件: 分爲定時事件和週期性時間;redis 的所有時間事件都存放在一個無序鏈表中,當時間事件執行器運行時,需要遍歷鏈表以確保已經到達時間的事件被全部處理。

可以看到,Redis 整個執行方案是通過高效的 I/O 多路複用件驅動方式加上單線程內存操作來達到優秀的處理效率和極高的吞吐量。

1.3 基於內存的操作

上面的小節也提到了,redis 之所以可以使用單線程來處理,其中的一個原因是,內存操作對資源損耗較小,保證了處理的高效性。

如此寶貴的內存資源,Redis 是怎麼維護和管理的呢?

$ 除了增刪改查還有哪些維護性操作 [1]

命中率統計,在讀取一個鍵之後,服務器會根據鍵是否存在來更新服務器的鍵空間命中次數或鍵空間不命中次數。

LRU 時間更新,在讀取一個鍵之後,服務器會更新鍵的 LRU 時間,這個值可以用於計算鍵的閒置時間。

惰性刪除,如果服務器在讀取一個鍵時發現該鍵已經過期,那麼服務器會先刪除這個過期鍵,然後才執行餘下的其他操作。

鍵的 dirty 標識,如果有客戶端使用 WATCH 命令監視了該鍵,服務器會將這個鍵標記爲 dirty,讓事務程序注意到這個鍵已經被修改過。每次修改都會對 dirty 加一,用於_觸發持久化和複製_。

數據庫通知,“如果服務器開啓了數據庫通知功能,那麼在對鍵進行修改之後,服務器將按配置發送相應的數據庫通知”

$ Redis 何如管理內存

過期鍵刪除,內存和 CPU 資源都是寶貴的,Redis 通過定期刪除設定合理的執行時長和執行頻率,配合惰性刪除兜底的方式,來達到 CPU 時間佔用和內存浪費之間的平衡。

數據淘汰,如果 key 生產的太快,定期刪除操作跟不上新生產的速率,而這些 key 又很少被訪問無法觸發惰性刪除,是否會把內存撐爆?回答是不會,因爲 redis 有數據淘汰策略:

值得一提的是,這裏的 lru 和平常我們所熟知的 lru 還不完全一樣,redis 使用的是採樣概率的思想,省略了雙向鏈表的內存消耗。

Redis 會在每一次處理命令的時候判斷是否達到了最大限制,如果達到則使用對應的算法去刪除涉及到的 Key,這時,我們前面所維護過鍵的 LRU 值就會派上用場了。

Part2Redis 爲什麼這麼靠譜

天有不測風雲,服務器也有趴窩的時候,Redis 這個基於內存的存儲遇到服務器宕機該怎麼應對呢?

2.1RDB 持久化

持久化是一種常見的解決方案,那麼,我們首先能想到的最簡單的持久化方案,就是每隔一段時間把內存裏的數據保存一次,來避免絕大部分數據的丟失。這也是 Redis 的 RDB 持久化得思路。

RDB 有兩種方式,save 和 bgsave

save,會阻塞服務器的其他操作,直到 save 執行完成,所以,這個期間的所有命令請求都會被拒絕。對客戶端影響較大。

BGSave,由子進程進行數據保存,期間 redis 仍然可以繼續處理客戶端請求。爲了防止競爭和衝突,bgsave 被設計成和 save/bgrewriteaof 操作互斥。

Redis 服務器默認每 100 毫秒執行一次,如果數據庫修改次數 (dirty 計數器) 大於設置的閾值,並且距離上次執行保存的時間 (lastsave 屬性) 大於設置的閾值,則執行保存操作。

因爲是統一批量的保存操作,rdb 文件有二進制存儲、結構緊湊、空間消耗少、恢復速度快等特點,在持久化方案上不可或缺。

2.2AOF 持久化

然而,因爲 bgsave 的週期間隔和保存觸發條件等原因,在服務器宕機時,不可避免的會丟失一部分最新的數據。這就需要一些輔助手段來做持久化補充。

RDB 保存的是鍵值對,而 AOF 則用來保存寫命令。

爲什麼 AOF 保存的是命令,而不是鍵值對呢?

Coder 的技術之路認爲,一是因爲 aof 刷盤,是在文件事件處理過程當中的,具體位置是在結束一個事件循環之前,調用追加函數進行,所以,使用請求命令來存儲更方便;二是如果遇到追加過程中命令被破壞,也可以通過 redis-check-aof 來恢復 (命令恢復起來比較方便)。

AOF 刷盤策略,由於 aof 追加動作是和客戶端請求處理串行執行的,所以每次都刷盤對性能影響較大,因此都是先追加到 aof_buf 緩存區裏,而是否同步到 AOF 文件中則依賴 always、everysec(默認)、no 的刷盤配置。想比 everysec ,always 對性能影響較大,而 no 則容易丟失數據。

AOF 文件重寫壓縮,AOF 因爲保存了請求命令,自然要比 RDB 更大,並且隨着程序的運行,會越來越大,然而,文件中有很多冗餘的命令數據是可以壓縮的,因爲對於某個鍵值對,某一時刻只會有一個狀態。

那麼,在重寫過程中新產生的操作該怎麼辦呢?

2.3Sentinel 高可用解決方案

上面兩個小節,主要是在闡述單機服務器的數據穩定性保障,那麼,如果是多機、多進程該怎麼來保障呢?

哨兵的作用:監視服務節點的健康

當主節點宕機時,由哨兵感知,並在從節點中重新選舉主節點:

同時,sentinel 還會監視宕機的 master 節點,恢復之後會將其設置爲從節點加入集羣。

除了主從切換的 sentinel 方案,還有 Cluster 集羣模式來保障 redis 的高可用,用來解決主從複製的存儲浪費問題。

Part3Redis6.x 的多線程

之前已經闡述過了單線程模型的整體流程,這裏不太贅述。

Redis 的多線程模型,不是傳統意義上的多線程併發,而是把 socket 解析回寫的這部分操作並行化,以解決 IO 上的時間消耗帶來的系統瓶頸。

對客戶端的任何請求,其實還是主線程在執行,避免了操作相同數據時線程間的競爭,把 io 部分並行化,降低了 io 對資源的損耗,從而提升了系統的吞吐量。仔細想來,感覺和 rpc 中的異步調用差不多意思,都是綁定來源,等待處理完成後給給各來源返回對應結果。

Part4Redis 最佳實踐

Redis 被當做分佈式緩存的應用場景非常普遍,有關緩存穿透、緩存擊穿、緩存雪崩、數據漂移、緩存踩踏、緩存污染、熱點 key 等常見問題,在上一篇文章 諸多策略,緩存爲王中已經有了詳細闡述,這裏不再重複。

這裏主要給出一些日常開發中的關注點:

其實沒有什麼最佳實踐,業務各有各的不同,都需要在實踐中研究嘗試,如果大家有非常好的實際案例,也歡迎補充,歡迎留言交流~

**鑑於之前 5000 字及以上長文的閱讀完成率,很少有讀者朋友可以看到這裏。如果兄弟竟然真的看到了這段話,那說明。。。 **

要是覺得文章還湊活,其實可以點個贊鼓勵一下大家的每個鼓勵都是我堅持的動力!

參考資料

[1] Redis 設計與實現: 黃健宏. 著

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cJwxQdudc99-YzGA3Fzaqg