Redis 6.0 客戶端快取的伺服器端實現

中介軟體小哥發表於2020-09-29

原文:https://redis.io/topics/client-side-caching

翻譯:Wen Hui

轉載:中介軟體小哥

客戶端快取是用於提供高效能服務的一項技術。它使用應用伺服器節點(通常情況下和資料庫伺服器使用不同的物理機)的可用記憶體,用來在應用端直接儲存一部分資料庫資訊。

正常情況下當客戶端請求應用伺服器一些資料時,應用伺服器會請求資料庫這些資訊,如下圖所示:

當使用客戶端快取時,應用伺服器端會儲存經常訪問的資料請求,以便在下次客戶端請求過程中重用之前的資料庫查詢回覆,而無需再向資料庫進行查詢。

儘管用於本地快取的應用程式記憶體可能不是很大,但是與請求諸如資料庫之類的網路服務相比,訪問本地計算機記憶體所需的時間要小几個數量級。由於在通常情況下,少量比例資料會經常頻繁的被訪問,因此該模式可以極大地減少應用程式獲取資料的延遲,並同時減少資料庫端的負載。

此外,在許多資料集中,資訊很少進行更改。例如,社交網路中的大多數使用者帖子要麼是不變的,要麼很少被使用者編輯。再加上通常只有一小部分帖子非常受歡迎的事實,要麼是因為一小群使用者擁有大量關注者,或者因為最近的帖子具有更高的曝光度,由此可見為什麼這種模式在實際情況下會非常有用。

通常來說,客戶端快取的兩個主要優點是:

1. 可用的資料延遲非常短。

2. 資料庫系統接收的查詢較少,從而可以使用更少的節點來提供相同的資料服務。

 

在電腦科學中只有兩大問題

上述模式的問題是在資料被修改或過期時,如何使應用程式儲存的資訊無效,以避免向使用者顯示陳舊資料。例如,在上面的應用程式本地快取了user:1234資訊之後,Alice可以將其使用者名稱更新為Flora。但是應用程式可能會繼續為使用者1234提供舊的使用者名稱。

有時,取決於我們要建模的應用程式,這個問題並不重要,因此客戶端將只使用固定的最大“生存時間”來快取資訊。一旦過了給定的時間,該資訊將不再被視為有效。使用Redis時,更復雜的模式會利用釋出/訂閱系統,以便向偵聽的客戶端傳送無效訊息。從使用的頻寬的角度來看,這是可行的,但卻是棘手且昂貴的,因為這種模式通常涉及嚮應用程式中的每個客戶端傳送無效訊息,即使某些客戶端可能沒有無效資料的任何副本。 此外,每個更改資料的應用程式查詢都需要使用PUBLISH命令,從而使資料庫花費更多的CPU時間來處理此命令。

無論使用哪種模式,都有一個簡單的事實:許多非常大的應用程式都實現某種形式的客戶端快取,因為這是擁有快速儲存或快速快取伺服器的下一個邏輯步驟。因此,Redis 6實現了對客戶端快取的直接支援,以使該模式更易於實現,更易於訪問,可靠且高效。

 

客戶端快取的Redis實現

Redis客戶端快取支援稱為跟蹤(tracking),並具有兩種模式:

在預設模式下,伺服器會記住給定客戶端訪問了哪些鍵,並在別的客戶端修改相同的鍵時向客戶端傳送無效訊息。這將花費伺服器端的記憶體,但僅對在記憶體中擁有修改的鍵的客戶端傳送無效訊息。

相反,在廣播模式下,伺服器不會嘗試記住給定客戶端訪問了哪些鍵,因此該模式在伺服器端根本不會花費任何記憶體。相反,客戶端會訂閱鍵字首(例如object:或user :),並且每次其他客戶端修改與該字首匹配的鍵值時都會收到通知訊息。

回顧一下,現在讓我們暫時忘記廣播模式,重點關注第一種模式。我們將在後面詳細介紹廣播模式。

客戶可以根據需要啟用跟蹤。連線開始時未啟用跟蹤。

啟用跟蹤後,伺服器會記住每個客戶端在連線生存期內請求過的鍵(通過傳送有關此鍵的讀取命令)。

當某個客戶端修改了某個鍵,或者由於鍵具有關聯的到期時間而將其逐出,或者由於最大記憶體策略而將其逐出時,所有啟用了跟蹤且可能已快取鍵的客戶端都會收到無效訊息通知。

當客戶端收到無效訊息時,要求它們刪除相應的鍵值資訊,以避擴音供過時的資料。

這是協議的示例:

從表面上看,這看起來很棒,但是如果你想到有10萬個已連線的客戶端在每個持久連線中都請求數百萬個鍵,則伺服器將會因為儲存太多資訊而崩潰。因此,Redis使用兩個關鍵思想來限制伺服器端使用的記憶體量以及處理實現該功能的資料結構的CPU成本:

伺服器會在一個全域性列表中記住可能已將給定鍵值快取過的客戶端列表。該表稱為無效表。這個無效表可以設定最大數量的記錄,如果插入了新鍵值,則伺服器可以通過假裝已修改(即使沒有修改)並將其傳送到客戶端來驅逐舊條目。這樣做,它可以使伺服器端回收用於此鍵值的記憶體,即使這樣會迫使鍵值在本地客戶端快取被逐出。

在無效表內部,我們實際上不需要儲存指向客戶端結構的指標,這將在客戶端斷開連線時在無效表中需要強制執行垃圾回收過程:相反,我們要做的只是儲存客戶端ID(每個Redis客戶端都有唯一的數字ID)。如果客戶端斷開連線,則隨著快取槽無效,將逐步收集垃圾資訊。

在這裡只有一個鍵空間,不以資料庫編號做劃分。因此,如果客戶端在資料庫2中快取鍵foo,而其他一些客戶端在資料庫3中更改了鍵foo的值,則仍然會傳送無效訊息。這樣,我們可以忽略資料庫編號,從而減少了記憶體使用量和實現的複雜度。

 

兩種連線方式

使用Redis 6支援的新版本的Redis協議RESP3,可以在同一連線中執行資料查詢並接收無效訊息。但是,許多客戶端實現可能更喜歡使用兩個獨立的連線來實現客戶端快取:一個用於資料,另一個用於無效訊息。因此,當客戶端啟用跟蹤時,它可以通過指定不同連線的“客戶端ID”來指定將無效訊息重定向到另一個連線。許多資料連線可以將無效訊息重定向到同一連線,這對於實現連線池的客戶端很有用。這兩個連線模型是RESP2唯一支援的模型(缺乏在同一連線中複用不同型別資訊的能力)。

這次我們將通過在舊的RRESP2模式下使用實際的Redis協議顯示一個示例,如何完成一個完整的會話,包括以下步驟:啟用跟蹤重定向到另一個連線,請求鍵的值資訊以及在鍵的內容被其他客戶端修改的情況下,獲取伺服器傳送的鍵失效資訊。

首先,客戶端開啟第一個用於失效的連線,請求返回連線的ID,並通過Pub / Sub訂閱專用通道,該通道在RESP2模式下用於獲得失效訊息(請記住,RESP2是通常的Redis協議,而不是你可以使用的更高階的協議):

客戶端可以決定在本地記憶體中快取“ foo” =>“ bar”。

現在,另一個客戶端將修改“ foo”鍵的值:

因此,失效連線將收到一條失效資訊,該資訊使指定的鍵值失效。

客戶端將檢查在此快取槽中是否有快取的鍵值資訊,並將逐出不再有效的資訊。

請注意,Pub / Sub訊息的第三個元素不是單個鍵,而是隻有一個元素的Redis陣列。因為我們傳送一個陣列,所以如果有成組的鍵無效,我們可以在一條訊息中做到這一點。

關於理解使用RESP2的客戶端快取以及為了讀取無效訊息而進行的Pub / Sub連線非常重要的一點是,使用Pub / Sub完全是一個為了重用舊的客戶端實現的技巧,但實際上並未真正傳送並被所有訂閱該頻道的客戶所接收。只有我們在CLIENT命令的REDIRECT引數中指定的連線才會實際收到Pub / Sub訊息,從而使此功能具有更大的可伸縮性。

如果改用RESP3,則將無效訊息作為推送訊息傳送(在同一連線中,或者在使用重定向時在輔助連線中)(請參閱RESP3規範以獲取更多資訊)。

 

追蹤用來追蹤什麼

如上面例子可以看到,預設情況下,客戶端不需要告訴伺服器它們正在快取哪些鍵。伺服器會追蹤只讀命令上下文中提到的每個鍵,因為它可能會被快取。

這具有明顯的優點,即不需要客戶端告訴伺服器它正在快取什麼。此外,在許多客戶端實現中,這就是你想要的,因為一個好的解決方案可能是使用先進先出的方法僅快取尚未快取的所有內容:我們可能希望快取固定數量的物件,每個物件我們檢索到新資料後,就可以對其進行快取,丟棄最早的快取物件。更高階的實現可能會刪除最不常用的物件或類似物件。

請注意,無論如何,如果伺服器上有寫流量,則快取槽將在一段時間內失效。通常,當伺服器假設我們得到的東西也快取時,我們就要進行權衡:

1. 當客戶端傾向於使用歡迎新物件的策略來快取更多內容時,這樣做會更有效率。

2. 伺服器將被迫保留有關客戶端鍵的更多資料。

3. 客戶端將收到有關其未快取的物件的無效訊息。

因此,下一節將介紹另一種方法

 

OPT-IN 模式

客戶端實現可能只希望快取選定的鍵,並明確地與伺服器通訊它們將快取的內容和不快取的內容:快取新物件時,這將需要更多的頻寬,但同時會減少伺服器需要記住的資料量,以及客戶端收到的無效訊息數量。

為此,必須使用OPTIN選項啟用跟蹤:

在這種模式下,預設情況下,不應快取讀取查詢中的鍵,而是當客戶端要快取某些內容時,它必須在實際命令檢索資料之前立即傳送一個特殊命令CACHING:

為了使協議更有效率,可以使用NOREPLY選項傳送CACHING命令:在這種情況下,CACHING命令中客戶端不會收到伺服器返回的資訊:

CACHING命令會影響隨後執行的命令,但是,如果下一個命令是MULTI,則將跟蹤事務中的所有命令。同樣,對於Lua指令碼,將跟蹤該指令碼執行的所有命令。

 

廣播模式

到目前為止,我們描述了Redis實現的第一個客戶端快取模型。還有一個稱為廣播模式,它從另一個折衷的角度來看問題,它不消耗伺服器端的任何記憶體,而是向客戶端傳送更多的無效訊息。在這種模式下,我們有以下主要行為:

客戶端使用BCAST選項啟用客戶端快取,並使用PREFIX選項指定一個或多個字首。例如:CLIENT TRACKING on REDIRECT 10 BCAST PREFIX object:PREFIX user:。如果根本沒有指定任何字首,則假定該字首為空字串,因此客戶端將會接收每個被修改的鍵的無效訊息。相反,如果使用一個或多個字首,則僅在失效訊息中傳送與指定字首之一匹配的鍵。

伺服器未在失效表中儲存任何內容。相反,它僅使用不同的字首表,其中每個字首都與客戶端列表相關聯。

每次修改與任何字首匹配的鍵時,所有訂閱該字首的客戶端都將收到無效訊息。

伺服器的CPU消耗與註冊字首數量成正比。如果只有幾個,幾乎看不出任何區別。使用大量字首,CPU成本可能變得非常高。

在這種模式下,伺服器可以優化為訂閱給定字首的所有客戶端建立單個回覆的過程,並將相同的回覆傳送給所有客戶端。這有助於降低CPU使用率。

 

避免競爭條件

在實施客戶端快取以將無效訊息重定向到其他連線時,你應該意識到存在競爭狀況。請參見以下示例互動,在此我們將資料連線稱為“ D”,並將失效連線稱為“ I”

如上所示,由於對GET的回覆返回給客戶端會有較長延時,因此在已經不再有效的實際資料之前,我們收到了無效訊息。因此,我們將繼續提供舊版本的foo鍵的值資訊。為避免此問題,當我們使用佔位符傳送命令時,填充快取是一個好主意:

當對資料和無效訊息使用單個連線時,這種競爭條件是不可能的,因為在這種情況下訊息的順序始終是已知的。

 

與伺服器斷開連線時該怎麼辦

同樣,如果丟失與套接字的連線以獲取無效訊息,則可能會以客戶端收到陳舊資料結束。為了避免這個問題,我們需要做以下事情:

1. 確保如果連線丟失,則重新整理本地快取。

2. 在將RESP2與Pub / Sub一起使用時,或者在RESP3上,都定期對無效訂閱連線進行ping操作(即使連線處於Pub / Sub模式下,也可以傳送PING命令!)。如果連線看起來斷開並且我們無法接收ping回覆,請在設定的最長時間後關閉連線並重新整理快取。

 

什麼需要快取

客戶可能希望執行有關給定快取的鍵在實際請求中被呼叫的次數的內部統計資訊,以瞭解以後對哪些鍵使用客戶端快取。一般來說:

· 我們不想快取很多不斷變化的鍵。

· 我們不想快取很多很少被請求的鍵。

· 我們希望快取經常請求的鍵並以合理的速率進行更改。有關鍵沒有以合理的速度更改的示例,請考慮一個不斷增加的全域性計數器。

但是,更簡單的客戶端可能只是使用一些隨機取樣來逐出資料,只是記住最後一次被查詢的特定鍵值,從而試圖逐出最近未被查詢的鍵。

 

有關客戶端庫實現的建議

· 處理TTL:如果要支援帶TTL的快取鍵,請確保查詢鍵的TTL值並在本地快取中設定TTL。

· 即使沒有TTL,在每個鍵中都放置一個最大TTL是一個比較好的做法。這是個很好的保護措施,可避免可能導致客戶端在本地副本中包含舊資料的錯誤或連線問題。

· 絕對需要限制客戶端使用的記憶體量。新增新鍵時,必須有一種方法可以將舊鍵逐出。

 

限制Redis使用的記憶體量

只需確保為Redis記住的最大鍵數配置一個合適的值,或者使用BCAST模式,該模式在Redis端根本不佔用任何記憶體。請注意,當不使用BCAST時,Redis消耗的記憶體與跟蹤的鍵數量以及請求此類鍵的客戶端數量成正比。

相關文章