---
# System prepended metadata

title: 快取在系統中的應用 & Redis 應對高並發量與高可用實現

---

Redis 簡介&緩存機制
===
為何要使用 Redis
---
主要為了緩解資料庫大量的 GET 請求，讓每次 GET 不需要一直進行效能較差的 I/O 操作，藉此提高網站並發量，也可以阻擋客戶端請求不在資料庫中的資料，避免資料庫進行多次無效 I/O。
另外在分佈式系統中，也因為 Redis 單線程的特性，所以可以做到分佈式鎖，這將在未來的文章中詳細說明。

Redis 的主要運作方法
---
Redis 基於 RAM 工作，在用戶每次發送 GET 請求時，都先查閱 Redis 中有沒有緩存這個要查詢的數據，如果沒有緩存再將請求轉發到資料庫中，並且這份資料會被 Redis 緩存，當下次查詢時就不需要再轉發請求至資料庫中了。

使用緩存引發的問題
---
Redis 將所有的資料都存在記憶體中，這樣雖然可以使讀取速度加快許多，但仍然會遇到一些問題：

1. 內存不足
2. 資料揮發性，若系統不穩則緩存數據都會消失

針對問題一，我們提出解決方案：
給緩存的資料設定一個時效，過時的緩存資料將被刪除。

定時刪除、惰性刪除、記憶體置換
---
然而要遍歷所有資料並找到過時資料是一個相當耗時間的操作，這會影響 Redis 的效能，反而導致 Redis 無法處理用戶發送的請求，因此 Redis 使用隨機算法，只隨機找到過期的資料刪除。這個方法稱為**定時刪除**。

如果只使用隨機算法來刪除過時資料，總會有部分資料始終逃過隨機算法導致內存堆積不需要的資料而引發內存不足，因此另外衍生了一個機制，如果今天請求的資料存在在 Redis 中，但資料已經過期，則 Redis會直接將這筆資料刪除。也因為這樣的刪除資料方法是被動觸發的，因此被稱為**惰性刪除**。

經過以上兩個方法幾乎可以解決大部分的問題，但仍然有可能遇到狀況，例如在資料都尚未過期時就已經內存不足，或者逃過隨機算法後，又沒有被查詢導致一直存在在內存中，因此 Redis 最終還會引入 LRU 置換法則，將最舊最少用到的緩存刪除。

緩存穿透、緩存擊穿、緩存雪崩
---
經過以上方法基本解決內存不足的問題，但關於緩存仍然存在一些問題：

1. 緩存穿透
2. 緩存擊穿
3. 緩存雪崩

### 緩存穿透
關於緩存穿透，用戶不斷發送一個查找不在資料庫內的資料的請求，此時因為資料本身就不在資料庫中，因此也不會被 Redis 緩存，這導致資料庫不斷執行無效的 I/O 操作使系統崩潰。

解決緩存穿透的方法，Redis 利用 Bloom Filter 來處理這個問題，Bloom Filter 如果告知資料不在資料庫則必定不在，如果告知資料在資料庫時，實際上資料也可能不在資料庫中，這個解決方法雖然不完美，但已經是比較好且快速的方法。
:::info
根據查到的資料，誤判率大約小於 1%，所以其實還是很好用
:::

### 緩存擊穿
再來說到緩存擊穿，假設資料庫閒置了一段時間，然後突然收到大量同一筆資料的請求，因為閒置了一段時間的關係，這筆資料已經被 Redis 刪除，因此資料庫就會忙著處理這些相同的 I/O 請求拖慢系統運行。這樣的情況就被稱為**緩存擊穿**。

### 緩存雪崩
最後是緩存雪崩，緩存雪崩建立在緩存擊穿之上，當同時收到大量不同資料的重複請求時會發生**緩存雪崩**，這些資料可能同時被 Redis 視為過期資料而刪除，使資料庫又同時處理大量的重複 I/O。

解決緩存雪崩的辦法是讓應用程式設置過期時間時盡量分散，使不要有太多的資料同時過期。

Redis 持久化存儲
===
前面解決了內存不足，和使用內存造成的一些問題，這裡將要討論下一個問題，也就是資料的揮發性問題。

持久化存儲策略 RDB
---
如果系統突然斷電，當回復供電後所有的緩存資料將會全數消失，如果要可以恢復緩存資料，那麼勢必要備份所有的資料到硬碟中，然而大量的數據直接備份到硬碟顯然效率低下，因此只能設定每幾分鐘後做一次備份，且這個備份行為必須要 fork 出一個子進程來操作，這就是所謂 Redis 中的 **RDB**。

前面也提到備份是一個對於電腦來說漫長的過程，在子進程備份的過程中又有許多請求發送到 Redis 中，等到備份完成時，這個備份檔案早就已經和現在緩存中的檔案不一樣了

持久化存儲策略 AOF
---
由於上述問題，加上備份並不適合高頻執行，因此衍生應對問題的辦法為學習資料庫的記錄方法，將每一筆更動資料的過程記錄下來，並且再添加一個額外的 aof_buf 來記錄備份期間新增的指令。這就是 Redis 的 **AOF 檔案**

這樣雖然解決了直接將數據備份的效能問題，但備份的資料仍然過大，如果想要壓縮那麼就需要分析寫入資料的指令是否可以合併，這個動作稱為 **AOF 重寫**

:::danger
這裡的指令是否可以合併並不是分析冗餘指令並刪除他們，這樣分析會使重寫時間過長。而是如果有連續更改同一個資料，那麼就只記錄最後一筆的狀態即可
:::

當然在合併指令時也是需要消耗一點時間的，那麼也必然需要 fork 出一個子進程來做這件事，那麼就會遇到和備份指令時一樣的問題，也就是在合併期間會有新的寫入指令，因此需要再準備一個 aof_rewrite_buf 來保存 aof 重寫期間的新指令，等重寫完成後再將這個 buf 中的指令寫進重寫好的 aof 檔案，經過以上的步驟，就是全部的 Redis 持久化存儲的方法。

Redis 高可用實現
===
由於 Redis 本身只有一個服務器，很難實現高可用，因此實現高可用就誕生了基礎版本的
“**主-從模式**”，由主節點負責寫入數據和數據同步，從節點負責讀取數據，並且如果主節點斷線或發生問題，則會由從節點補位。

主從節點資料同步方法
---
舉個例子，主節點負責將資料寫成一個 RDB 並將這個 RDB 發送給從節點，同時，在同步的過程中主節點也會記錄同步過程中新增的指令並將這個記錄發送給從節點，這就是 Redis 的資料同步。

現在假設斷線的是從節點，持續了一小段時間後重新連回來，這時候資料已經有缺失，此時如果將整個 RDB 重新傳送給這個從節點也是可行，但斷線時間只有一小段時間，整個重傳消耗過多的系統資源，因此衍生出下面的解決方案：

每次主節點發送寫入資料給從節點時，也將這個資料寫入 buffer 中，並且設定一個**偏移量**，這個偏移量可以讓主節點知道從節點同步到哪裡，進而將缺失的資料從 buffer 中發送給從節點。舉個例子，假設從節點在接收資料到 3004 這個偏移量時斷線，主節點繼續更新資料，當從節點重新連回時主節點資料的偏移量已經到 3012，那麼此時只需要將 3004 到 3012 之間的資料傳送給這個從節點即可。

自動更新主節點與哨兵（Sentinel）機制
---
由於現在如果主節點壞掉還是需要工程師手動選擇一個從節點頂替為主節點，因此衍生了哨兵機制。

作為哨兵的節點並不做讀寫操作，而是每隔一小段時間就發送 ping 命令給其他節點確認是否在線，還要負責每過一段時間發送 info 命令問候主節點確認主節點是否在線，如果一個哨兵無法與主節點連線，則會被判定為**主觀下線**，所有的哨兵會互相溝通，如果大部分的哨兵都無法與該節點連線才會被判定為**客觀下線**，具體要多少哨兵的主觀下線可以自己設定。

當確定被判斷為客觀下線時啟動替換主節點的機制，具體執行三個步驟：
1. 選擇一個從節點頂替當前主節點
2. 將所有從節點的資料與新主節點的資料同步
3. 將原主節點設為從節點

從這裡可以看到，我們還需要一個選擇從節點頂替主節點的規則，這裡有兩個規則：
1. 硬體資源越好的，優先級愈高
2. 複製偏移量越大的資料愈新，所以優先級愈高

哨兵會選擇兩者兼具者將從節點頂替為主節點。

最後你的整體結構可能會如下方圖片一樣：

![redis.drawio](https://hackmd.io/_uploads/Hk_Dq4y20.png)



Redis 集群架構
===
從以上的做法可以發現，我們為了實現高可用用了許多 Redis 伺服器，但這些服務器幾乎都儲存相同的資料或者沒有儲存資料（哨兵），當資料量越來越大時 Redis 沒辦法儲存大量的數據，因此我們需要集群架構來擴大 Redis 的儲存空間。

集群架構就是利用多個 Redis 服務器，將資料分佈儲存在各個伺服器中，以此來擴大容量，那麼就會遇到兩個問題：
1. 擴充問題，當有新的 Redis 伺服器加入時如何處理？
2. 資料分配問題，該如何進行資料分配，使各個 Redis 伺服器有合理的資源分配？

Redis 版本的 “三向交握”
---
先來解決第一個問題，當有新的 Redis 節點要加入時需要一個介紹人，這個介紹人可以是原來集群中的任意一個 Redis 伺服器，當獲取到欲加入伺服器的 ip 位址及 port 號後，介紹人發送一個 Meet 請求，新伺服器則需要回應一個 pong 請求，當介紹人收到 pong 請求後再發送 ping 請求給新伺服器，然後告知其他節點這個新伺服器加入的訊息，至此完成 Redis 伺服器的擴充。

Redis 資源分配、slot
---
Redis 學習了哈希表的方法，將資料分劃分為 16384 個槽位（slot），作為工程師的我們，根據每個 Redis 伺服器性能的不同分配槽位數量，如記憶體空間愈大分配愈多槽位，且每個槽位都需要有人負責，否則這個集群將被判定為 “下線”。

有了分配原則，系統還需要知道每個槽位由誰負責，Redis 採用空間換取時間，利用大型陣列（或者說類似 map 映射的做法）記錄每一個槽位由誰負責。

有了以上的做法，現在查詢數據時需要先檢查查詢的資料 slot 是不是由自己負責，如果不是就會回傳一個 moved 並告知這個資料 slot 由誰負責，系統再根據回傳的 ip 及 port 找到需要的資料。

集群架構的高可用
---
從剛剛的說明可以知道，每一個 slot 都需要有人負責，如果有一個槽位沒人負責就會造成整個集群下線，因此為了解決伺服器可能會斷線的問題，集群的高可用繼續沿用了 “主-從架構”，每一個主伺服器都是集群中的一個個體，從伺服器則會與主伺服器的資料同步，當主伺服器斷線時從伺服器會馬上替補，詳細做法可見上面 “Redis 高可用實現”。