近日 Redis 6.0.0 GA 版本發佈,這是 Redis 歷史上最大的一次版本更新,包括了客戶端緩存 (Client side caching)、ACL、Threaded I/O 和 Redis Cluster Proxy 等諸多更新。html
咱們今天就依次聊一下客戶端緩存的必要性、具體使用、原理分析和實現。redis
咱們都知道,使用 Redis 進行數據的緩存的主要目的是減小對 MySQL 等數據庫的訪問,提供更快的訪問速度,畢竟 《Redis in Action》中提到的, Redis 的性能大體是普通關係型數據庫的 10 ~ 100 倍。數據庫
因此,以下圖所示,Redis 用來存儲熱點數據,Redis 未命中,再去訪問數據庫,這樣能夠應付大多數狀況下的性能要求。centos
可是,Redis 也有其性能上限,而且訪問 Redis 必然有必定的網絡 I/O 以及序列化反序列化損耗。因此,每每會引入進程緩存,將最熱的數據存儲在本地,進一步加快訪問速度。緩存
如上圖所示,Guava Cache 等進程緩存做爲一級緩存,Redis 做爲二級緩存:bash
只使用 Redis 分佈式緩存時,遇到數據更新時,應用程序更新完 MySQL 中的數據,能夠直接將 Redis 中對應緩存失效掉,保持數據的一致性。服務器
而進程內緩存的數據一致性比分佈式的緩存面臨更大的挑戰。數據更新的時候,如何通知其餘進程也更新本身的緩存呢?網絡
若是按照分佈式緩存的思路,咱們能夠設置極短的緩存失效時間,這樣沒必要實現複雜的通知機制。分佈式
可是不一樣進程內的數據依然會面臨不一致的問題,而且不一樣進程緩存失效時間不統一,同一個請求到了不一樣的進程,可能出現反覆幻讀的狀況。
ide
Ben 在 RedisConf18 給出了一個方案(視頻和 PPT 連接在文末),經過 Redis 的 Pub/Sub,能夠通知其餘進程緩存對此緩存進行刪除。若是 Redis 掛了或者訂閱機制不靠譜,依靠超時設定,依然能夠作兜底處理。
Antirez(Redis 的做者) 也正是聽取 Ben 這個方案後,才決定在 Redis Server 支持客戶端緩存的,由於在有服務端參與的狀況下能夠更好的處理上述這些問題。
下面使用 Docker 安裝 Redis 6.0.1,而後使用 telnet 來簡單演示一下 Redis 6.0 的客戶端緩存功能。全部相關的功能以下圖所示,分別是使用RESP3 協議版本的普通模式和廣播模式,以及使用 RESP2 協議版本的轉發模式。咱們先來看普通模式。
先使用 redis-cli 設置緩存值 test=111,使用 telnet 鏈接上 Redis,而後發送 hello 3 開啓 RESP3 協議。
[root@VM_0_3_centos ~]# telnet 127.0.0.1 6379 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello 3 // telnet 輸出結果格式化標準化後以下,不然換行太多而且是 RESP3 格式,不須要了解格式。 > HELLO 3 1# "server" => "redis" 2# "version" => "6.0.1" 3# "proto" => (integer) 3 4# "id" => (integer) 10 5# "mode" => "standalone" 6# "role" => "master" 7# "modules" => (empty array)
這裏須要注意,Redis 服務端只會 track 客戶端在一個鏈接生命週期內的獲取的只讀命令的 key值。Redis 客戶端默認不開啓 track 模式,須要使用命令開啓,而後必需要先獲取一次 test 的值,這樣 Redis 服務器纔會記錄它。
client tracking on +OK get test $3 111
當鍵被修改,或者由於失效時間(expire time)和內存上限 maxmemory 策略被驅除時,Redis 服務端會通知這些客戶端。咱們這裏簡單地更新 test 的值,telnet 則會收到以下通知
>2 // RESP3 中的 PUSH 類型,標誌爲 > 符號 $10 invalidate *1 $4 test
若是你再一次更新 test 值,此次 telnet 就不會再收到失效(invalidate)消息。除非 telnet 再進行一次 get 操做,從新 tracking 對應的鍵值。
也就是說 Redis 服務端記錄的客戶端 track 信息只生效一次,發送過失效消息後就會刪除,只有下次客戶端再次執行只讀命令被 track,纔會進行下一次消息通知 。
取消 tracking 的命令以下所示。
client tracking off +OK
Redis 還提供了一種廣播模式(BCAST),它是另一種客戶端緩存的實現方式。這種方式下 Redis 服務端再也不消耗過多內存存儲信息,而是發送更多的失效消息給客戶端。
這是服務端存儲過多數據,消耗內存和客戶端收到過多消息,消耗網絡帶寬之間的權衡(tradeoff)。
// 已經 hello 3 開啓 RESP3 協議,否則沒法收到失效消息,下同 client tracking on bcast +OK // 此時設置 key 爲 a 的鍵值,收到以下消息。 >2 $10 invalidate *1 $1 a
若是你不想全部的鍵值的失效消息都收到,則能夠限制 key 的前綴,以下命令則表示只關注前綴爲 test 的鍵值的消息。通常來講,業務的緩存 key 都是根據業務擁有統一的前綴,因此這一特性十分方便。
client tracking on bcast prefix test
與普通模式必須獲取一次鍵的規則不一樣,廣播模式下,只要鍵被修改或刪除,符合規則的客戶端都會收到失效消息,並且是能夠屢次獲取的
與普通模式相比,雖然少存儲了一些數據,可是因爲須要對前綴規則進行匹配,會消耗必定的 CPU 資源,因此注意別使用過長的前綴。
上述操做時客戶端都須要先開啓 RESP3,Redis 爲了兼容 RESP2 協議提供了轉發(Redirect)模式,再也不使用 RESP3 原生支持 PUSH 消息,而是將消息經過 Pub/Sub 通知給另一個客戶端,具體流程以下圖所示。
這裏須要兩個 telnet,其中一個 telnet 須要訂閱 _redis_:invalidate
信道。而後另外一個 telnet 開啓 Redirect 模式,並制定將失效消息經過訂閱信道發送給第一個 telnet。
# telent B client id :368 subscribe _redis_:invalidate # telnet A,開啓 track 並指定轉發給 B client tracking on bcast redirect 368 # telent B 此時有鍵值被修改,收到 __redis__:invalidate 信道的消息 message $20 __redis__:invalidate *1 $1 a
你會發現,轉發模式和文章開始提到的多級緩存中的更新機制很相似了,只不過那個方案中是業務系統修改完 key 後發送消息通知,而這裏是 Redis 服務端代替業務系統發送消息通知。
使用 OPTIN 能夠選擇性的開啓 tracking。只有你發送 client caching yes (Redis 文檔中是 CACHING 命令,可是實驗時發現無效)以後的下一條的只讀命令的 key 纔會 tracking,不然其餘的只讀命令的 key 不會被 tracking。
client tracking on optin client caching yes get a get b // 此時修改 a 和 b 的值,發現只收到 a 的失效消息 >2 $10 invalidate *1 $1 a
而 OPTOUT 參數與之相反,你能夠有選擇的退出 tracking。發送 client caching off 以後的下一條只讀命令的 key 不會被 tracking,其餘只讀命令都會被 tracking。
OPTIN 和 OPTOUT 是針對非 BCAST 模式,也就是隻有發送了某個 key 的只讀命令後,纔會追蹤相應的 key。而 BCAST 模式是不管你是否發送某個 key 的只讀命令,只有 Redis 修改了 key,都會發送相應的 key 的失效消息(前綴匹配的)。
默認狀況下,失效消息會發送給全部須要的 Redis 客戶端,可是有些狀況下觸發失效消息也就是更新 key 的客戶端不須要收到該消息。
設置 NOLOOP,能夠避免這種狀況,更新 Key 的客戶端將再也不收到消息,該選項在普通模式和廣播模式下都適用。
由上文能夠知道,普通模式下須要存儲大量的被 tracking 的 key 和客戶端信息(具體存儲的數據下文中會講解),因此當 10k 客戶端使用該模式處理百萬個鍵時,會消耗大量的內存空間,因此 Redis 引入了 tracking_table_max_keys 配置,默認爲無,不限制。
當有一個新的鍵被 tracking 時,若是當前 tracking 的 key 的數量大於 tracking_table_max_keys,則會隨機刪除以前 tracking 的 key,而且向對應的客戶端發送失效消息。
咱們也先講解普通模式的原理,Redis 服務端使用 TrackingTable 存儲普通模式的客戶端數據,它的數據類型是基數樹(radix tree)。
基數樹是針對稀疏的長整型數據查找的多叉搜索樹,能快速且節省空間的完映射,通常用於解決 Hash衝突和 Hash表大小的設計問題,Linux 的內存管理就使用了它。
Redis 用它存儲鍵的指針和客戶端 ID 的映射關係。由於鍵對象的指針就是內存地址,也就是長整型數據。客戶端緩存的相關操做就是對該數據的增刪改查:
enableTracking
方法使用基數樹記錄下該 key 和 clientId 的映射關係。trackingInvalidateKey
方法根據 key 從 TrackingTable 中查找全部對應的客戶端ID,而後調用 sendTrackingMessage
方法發送失效消息給這些客戶端(會檢查 CLIENT_TRACKING 相關標誌位是否開啓和是否開啓了 NOLOOP)。廣播模式與普通模式相似,Redis 一樣使用 PrefixTable
存儲廣播模式下的客戶端數據,它存儲前綴字符串指針和(須要通知的key和客戶端ID)的映射關係。它和廣播模式最大的區別就是真正發送失效消息的時機不一樣:
PrefixTable
的前綴對應的客戶端列表中加入該客戶端ID。trackingInvalidateKey
方法,trackingInvalidateKey
方法中若是發現 PrefixTable
不爲空,則調用 trackingRememberKeyToBroadcast
依次遍歷全部前綴,若是key 符合前綴規則,則記錄到 PrefixTable
對應的位置。trackingBroadcastInvalidationMessages
函數來真正發送消息。Redis 會在每次執行過命令後(processCommand方法)調用 trackingLimitUsedSlots
來判斷是否須要進行清理:
關於源碼,在 tracking.c 文件下,咱們這裏只看一下最爲關鍵的 trackingInvalidateKey
函數和 sendTrackingMessage
函數,理解了這兩個函數,廣播模式和處理最大 tracking 上限等相關函數都與之相似。
void trackingInvalidateKey(client *c, robj *keyobj) { if (TrackingTable == NULL) return; sds sdskey = keyobj->ptr; // 省略,若是廣播模式的記錄基數樹不爲空,則先處理廣播模式 // 1 根據鍵的指針去 TrackingTable 查找 rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey)); if (ids == raxNotFound) return; // 2 使用迭代器遍歷 raxIterator ri;raxStart(&ri,ids);raxSeek(&ri,"^",NULL,0); while(raxNext(&ri)) { // 3 根據 clientId 查找 client 實例 client *target = lookupClientByID(id); // 4 若是未開啓 track 或者是廣播模式則跳過。 if (target == NULL || !(target->flags & CLIENT_TRACKING)|| target->flags & CLIENT_TRACKING_BCAST) { continue; } // 5 若是開啓了 NOLOOP 而且是致使key發生變化的client則跳過。 if (target->flags & CLIENT_TRACKING_NOLOOP && target == c) { continue; } // 6 發送失效消息 sendTrackingMessage(target,sdskey,sdslen(sdskey),0); } // 7 減小數據統計,根據sdskey刪除對應的記錄 TrackingTableTotalItems -= raxSize(ids); raxFree(ids); raxRemove(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey),NULL); }
源碼如上所示,trackingInvalidateKey 方法主要作了 7 件事情:
下面來看真正發送消息的 sendTrackingMessage
函數,它主要作了6件事:
_redis_:invalidate
信道中發送失效消息的頭部信息;void sendTrackingMessage(client *c, char *keyname, size_t keylen, int proto) { int using_redirection = 0; // 1 若是 client_tracking_redirection 不爲空,則開啓了轉發模式 if (c->client_tracking_redirection) { // 2 找到轉發的客戶端實例 client *redir = lookupClientByID(c->client_tracking_redirection); if (!redir) { // 3 若是轉發客戶端關閉了,則必須通知原客戶端 .... return; } c = redir; using_redirection = 1; } if (c->resp > 2) { // 4 若是是 RESP3 則發PUSH addReplyPushLen(c,2); addReplyBulkCBuffer(c,"invalidate",10); } else if (using_redirection && c->flags & CLIENT_PUBSUB) { // 5 轉發模式,往 TrackingChannelName 信道中發送消息 addReplyPubsubMessage(c,TrackingChannelName,NULL); } else { return; } // 6 發送鍵等信息,和上邊4,5操做連在一塊兒的。 addReplyProto(c,keyname,keylen); }
相信看到這裏的小夥伴們都已經有點疲憊了吧,可是還請你們多多點贊多多評論。後續還會學習其餘 Redis 6.0.0 的其餘亮點功能,請你們繼續關注。