基於Redis構建微服務的反應式架構 - bitsrc

banq發表於2021-11-05

如何使用 Redis 的特性來生成反應式資料流?

Redis 是我遇到的最強大、最通用的技術之一。可悲的是,大多數人只知道它是因為它是一個很好的快取解決方案。

我們需要解決這個問題。

特別是,我想向您展示您可以建立一個以 Redis 作為主要元件的反應式架構。這是一個巨大的優勢,特別是如果您由於其他要求(即好的 ol'cache)已經將它作為基礎架構的一部分。

您使用 Redis 與我將在此處描述的功能進行互動的方式取決於您,老實說,此時任何選項都與下一個選項一樣有效。我傾向於使用 Node.js,但這就是我,你可以自由使用最適合你的東西。

 

構建反應式架構

這裡首先要了解什麼是反應式架構,為什麼我們要構建一個而不是採用更傳統的方法?

簡而言之,反應式架構是在滿足所有前提條件的那一刻開始執行每一位邏輯的架構——我想我應該在“簡單”這個詞周圍加上引號。

讓我換一種說法:當您需要在特定事件發生後觸發邏輯時,您有兩種選擇:

  • 定期檢查某種標誌,直到它開啟,這意味著事件發生了。
  • 坐等,直到其他東西通知您的服務該事件已觸發。

第二部分是物件導向程式設計中觀察者模式的關鍵。觀察到的物件讓對其內部狀態感興趣的每個人都知道,它實際上已更新。

我們在這裡嘗試做的是將相同的 OOP 模式外推到架構級設計中。因此,與其在我們的程式中加入一些邏輯,我說的是一旦正確的事件發生,就會觸發服務的功能。

這是分發和擴充套件平臺的最有效方式,因為:

  • 您不必浪費時間和網路流量輪詢特定標誌的資料來源(或您認為應該輪詢的任何內容)。此外,如果您使用的是按使用付費的基礎架構,則不需要的輪詢可能會導致額外費用,在目標服務上進行不必要的工作,並且如果在您的程式碼等待輪詢期間發生多個事件,您可能最終不得不聚合事件.
  • 您可以通過新增新服務、並行工作和儘可能快地捕獲事件來擴充套件服務的處理能力。
  • 平臺更穩定。通過被動工作,您可以確保您的服務以最佳速度執行,而不必擔心由於來自客戶端的資料過載而崩潰。

反應式平臺本質上是非同步的,因此任何嘗試使用它們的客戶端應用程式也需要適應相同的範例。外部 API 可能是通過 HTTP 實現的 REST,但這並不意味著您將獲得作為響應的答案,相反,您將獲得一個200 OK響應,這意味著您的請求已被接收。為了讓您的應用程式獲得實際結果,它必須訂閱將包含此類響應的特定事件。

請記住這一點,否則,您將花費很長時間來除錯為什麼沒有得到您想要的響應。

 

那我們需要什麼?

撇開這一點,我們需要什麼才能使我們的平臺/架構成為反應式平臺/架構?它不是 ReactJS,這是肯定的。我們需要一個訊息代理,它可以集中多個服務之間的訊息分發。

有了可以充當代理的東西,我們需要確保我們的程式碼的編寫方式可以通過讓代理知道它在哪裡以及它需要的事件型別來訂閱某些事件。

之後,將向我們的服務傳送通知,並觸發我們的邏輯。

聽起來很簡單吧?那是因為它就是!

 

那麼 Redis 是如何考慮的呢?

Redis 不僅僅是一個鍵值記憶體儲存,事實上,它有 3 個我喜歡它的特性,允許我根據不同的預期行為建立反應式架構。

這3個特點是:

  • 釋出/訂閱。Redis 內部有一個訊息佇列,它允許我們傳送訊息並將它們分發給每個訂閱的程式。這是一種即發即棄的合約,這意味著如果沒有監聽器處於活動狀態,那麼訊息就會丟失。所以在使用這個頻道時要考慮到這一點。
  • 鍵空間(KeySpace)通知。可能是我最喜歡的 Redis 功能。這些壞男孩是由 Redis 本身建立的事件,並分發給決定訂閱它們的每個程式。它們與鍵空間的變化有關,這意味著您儲存在其中的資料發生的任何事情。例如,當您刪除或更新金鑰時,或者當其 TTL 計數器達到 0 時自動刪除。這允許您生成定時事件。在“某事”發生 3 天后,您是否必須觸發一些邏輯?這是如何。
  • Redis 流。這是 Redis 資料型別的混合體,混合了鍵空間通知和釋出/訂閱,所有這些組合在一起並且執行良好。Streams 嘗試模擬tail -f命令在您的終端上的行為。如果您從未見過該命令,那麼這是一個 *nix 命令,它顯示檔案的最後一行,並持續監聽檔案的更改,以便在您新增新行時,它會立即列出。流也會發生同樣的情況。考慮到正確的用例,它們非常強大且非常有用。您可以在此處閱讀有關它們的更多資訊

所有這些功能都允許您以一種或另一種方式與您的流程進行通訊,並且根據您所追求的行為型別,您可能想要解決其中一個或全部。

讓我們快速瀏覽一些示例,讓您瞭解使用什麼以及何時使用。

經典的基於事件的訊息傳遞

最簡單的例子是每個微服務都在等待某事發生。要觸發的事件,該事件可能來自外部,即系統的使用者或客戶端。

基於Redis構建微服務的反應式架構 - bitsrc

看上圖,把中央的紅管看成是Redis的Pub/Sub或者Blocking list,這是一個更可靠的Pub/Sub的自定義實現。

流程從 #1 開始,由“客戶端應用程式”提交請求,並在 9 點結束,“客戶端應用程式”收到有關響應的通知。其餘的部分?我不在乎,客戶端應用程式也不應該在乎。

這是這種正規化的優點之一,該架構成為客戶端的黑匣子。單個請求可以觸發數百個事件或僅觸發一個,行為將是相同的:一旦響應準備就緒,它將被傳遞給客戶端。而不是客戶端知道需要多長時間或多久需要檢查它是否準備好。這些都不重要。

請記住以下注意事項:

  • 訊息由其訂閱者釋出到“通道”中。如果您想釋出不同型別的主題,建議您擁有不同的頻道。此外,如果您需要額外的粒度來區分哪個消費者必須負責處理一條特定的訊息,則詳細資訊將需要成為訊息的一部分。這是因為一個頻道的所有訂閱者都將獲得相同的訊息,因此如果您有多個程式偵聽並獲得相同的訊息,您最終可能會重新執行相同的操作。可以在 Redis 中使用訊息的 ID 實現一個標誌(例如),以確保建立它的第一個程式是負責處理事件的程式,而其餘程式可以忽略它。這是一種可靠的方法,因為在 Redis 中設定金鑰是一個原子過程,
  • 如果沒有訂閱者收聽某個特定頻道,則釋出的訊息將丟失。如果您使用 Pub/Sub 模式就是這種情況,因為它在“即發即忘”機制下工作。如果您想確保您的郵件在處理之前一直保留在那裡,您可以使用“阻止列表”方法。該解決方案包括直接在 Redis 的鍵空間上建立一個列表(即一個普通的值列表),並讓程式訂閱以獲取有關該鍵的鍵空間通知。這樣他們就可以決定如何處理插入的資料(即如果他們想忽略它,處理它並刪除它等)。
  • 如果您要傳送複雜的訊息,例如 JSON,則需要對其進行序列化。這是因為對於阻止列表和 Pub/Sub,您唯一可以傳送的是字串。話雖如此,如果您需要在沒有序列化的情況下通過線路傳送複雜型別,您可以考慮使用 Redis Streams,這是它​​們允許的。當然,這裡的限制是唯一允許的型別是 Redis,而不是您用來編寫解決方案的語言的型別。

現在讓我們看看如果您的事件觸發取決於某些時間會發生什麼。

 

基於時間的觸發

反應式架構的另一個常見行為是能夠在預定義的時間過去後觸發某些事件。例如:在發現資料問題 10 分鐘後觸發警報。或者等待 30 分鐘,然後觸發 IoT 裝置已停止傳送資料的警報。

這些通常是與現實世界的限制相關的行為,需要一些時間來解決,或者甚至可以通過“稍等片刻”並重新開始倒數計時來解決自己的問題(例如連線不可靠的物聯網裝置) )。

對於這種情況,架構保持不變,唯一的區別是中央通訊集線器肯定使用來自 Redis鍵空間通知

您會看到,您需要了解有關 Redis 的兩個主要功能才能實現這一目標:

  1. 設定鍵值對時,您可以選擇以秒為單位定義 TTL(生存時間)。這就變成了倒數計時,一旦達到 0,金鑰將自動銷燬。
  2. 當您訂閱一個鍵空間時(這也適用於 pub/sub,但我們在這裡沒有使用它),您可以使用模式進行訂閱。換句話說,您可以訂閱“last_connection_time_of_device*”,而不是訂閱鍵“last_connection_time_of_device100002”的事件。然後,一旦發生某些事情,建立的每個與該模式匹配的金鑰都會通知您。

考慮到這兩點,您可以建立訂閱這些特定鍵的服務,並在它們被刪除後(即事件被觸發時)做出反應。同時,您讓生產者不斷更新金鑰,這也會重置 TTL 計時器。因此,如果您正在跟蹤裝置上次傳送其心跳的時間,則可以像我上面顯示的那樣為每個裝置設定一個金鑰,並在每次獲得新的心跳時更新該金鑰。一旦 TTL 結束,這意味著您在配置的時間內沒有收到新的心跳。您訂閱的程式將只接收金鑰名稱,因此如果您只需要裝置的 ID,您可以像我展示的那樣構造您的金鑰,並解析名稱以捕獲所需的資訊。

 

影鍵技術

另一方面,如果您在該鍵中儲存了一個複雜的結構並且需要它,則必須稍微更改此方法。這是因為當 TTL 到期時,金鑰將被刪除,從而刪除其中的資料,因此您無法真正檢索它。這時,您可以使用一種稱為“陰影鍵”的技術。

影子鍵本質上是用於觸發事件的鍵,但這實際上是在隱藏包含您需要的資料的實際鍵。回到我們的例子,假設生產者每次收到心跳都會更新 2 個鍵:

  • “last_connection_time_of_device100002”帶有最後從裝置接收到的有效負載的unix時間戳。
  • “device_data_id100002”帶有來自裝置的額外資訊。

在這兩個鍵中,只有第一個也有 TTL,第二個沒有。因此,當您收到過期通知時,您將從過期金鑰 (last_connection_time_of_device100002) 中獲取 ID 並使用它來讀取第二個金鑰的內容。然後,如果需要,您也可以繼續刪除其他金鑰,或者將其保留在那裡,無論對您的用例是否有效。

這裡需要考慮的唯一警告是,如果您在叢集模式下配置了 Redis,則金鑰空間通知不會在整個叢集中傳播。這意味著您必須確保您的消費者連線到每個節點。其中一些通知會丟失,否則沒有人會收到它們。這是這種技術的唯一缺點,但在您花幾天時間除錯非同步邏輯之前瞭解它很重要(去過那裡,做過)。

如您所見,兩種情況下的複雜性都降低到只需確保您訂閱正確的事件或分發渠道。例如,如果您要嘗試使用普通 SQL 資料庫執行此操作,則必須圍繞程式碼建立大量邏輯,以有效確定何時輸入新資料或何時刪除一條資訊。

相反,這裡的所有複雜性都由 Redis 抽象出來,您需要擔心的只是編寫業務邏輯。那對我來說就是黃金。

 

您是否將 Redis 用於快取之外的任何其他場景?在評論中與他人分享您的經驗,我很想知道你們都是如何使用我最喜歡的技術之一!

相關文章