Redis 的客户端缓存(Client-Side Caching)是一项重要特性,允许客户端在本地缓存 Redis 数据,从而减少与 Redis 服务器的通信频率,提高应用的响应速度和可扩展性。Redis 客户端缓存的实现主要依赖于以下几个核心组件和机制:
- 订阅机制:客户端通过订阅特定的键空间事件,获取键的变更通知。
- 通知机制:Redis 服务器在键发生变更时,通过发布/订阅(Pub/Sub)机制将变更通知推送给客户端。
- 缓存一致性:确保客户端缓存与 Redis 服务器的数据一致性。
核心概念和数据结构
1. 客户端缓存模式
Redis 提供了两种客户端缓存模式:
- 主动推模式(Tracking Mode):服务器主动向客户端推送键的变更通知。
- 被动拉模式(Polling Mode):客户端定期从服务器拉取键的变更信息。
2. 客户端状态
每个客户端的状态通过 client 结构体维护,其中包含缓存相关的信息:
1typedef struct client { 2 // ... 3 uint64_t client_tracking_redirection; /* Redirected ID for tracking invalidation messages */ 4 uint64_t client_tracking_locks; /* Number of tracking locks */ 5 uint8_t client_tracking_prefixes; /* Number of tracking prefixes */ 6 // ... 7} client; 8
启用客户端缓存
客户端缓存可以通过 CLIENT CACHING 命令启用和配置。
1redis> CLIENT CACHING yes 2 3OK 4
订阅键空间事件
通过订阅键空间事件,客户端能够接收到指定键的变更通知。订阅通过 PSUBSCRIBE 命令实现:
1redis> PSUBSCRIBE "__keyspace@0__:mykey" 2
通知机制
Redis 服务器通过发布/订阅机制将变更通知推送给客户端。这通过 trackingInvalidateKey 和相关函数实现。
1. 启用键空间通知
首先,需要在 Redis 配置文件中启用键空间通知:
1notify-keyspace-events Ex 2
2. 处理键变更事件
当键发生变更时,Redis 会调用 trackingInvalidateKey 函数来处理变更事件。
1void trackingInvalidateKey(client *c, robj *keyobj) { 2 if (!keyobj) return; 3 4 sds sdskey = keyobj->ptr; 5 size_t keylen = sdslen(sdskey); 6 7 // 查找所有订阅该键的客户端并发送无效化消息 8 listNode *ln; 9 listIter li; 10 listRewind(server.tracking_clients, &li); 11 while ((ln = listNext(&li)) != NULL) { 12 client *target = listNodeValue(ln); 13 if (target == c) continue; 14 15 // 发送无效化消息 16 addReplyArrayLen(target, 2); 17 addReplyBulkCString(target, "invalidate"); 18 addReplyBulk(target, keyobj); 19 } 20} 21
缓存一致性管理
为了确保客户端缓存的一致性,Redis 通过以下机制管理缓存:
1. 追踪表
Redis 维护一个追踪表,用于记录每个客户端订阅的键。
1typedef struct trackingTableEntry { 2 sds key; 3 client *c; 4} trackingTableEntry; 5 6dict *tracking_table; 7
2. 键变更通知
当键发生变更时,Redis 会遍历追踪表,查找订阅该键的客户端,并发送变更通知。
1void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) { 2 // 构建订阅频道名称 3 sds channel = sdsnewlen("__keyspace@", 11); 4 channel = sdscatprintf(channel, "%d__", dbid); 5 channel = sdscatsds(channel, key); 6 7 // 发送通知 8 list *clients = dictFetchValue(tracking_table, channel); 9 if (clients != NULL) { 10 listIter li; 11 listNode *ln; 12 listRewind(clients, &li); 13 while ((ln = listNext(&li)) != NULL) { 14 client *c = listNodeValue(ln); 15 if (c->flags & CLIENT_CLOSE_AFTER_REPLY) continue; 16 addReplyArrayLen(c, 3); 17 addReplyBulkCString(c, "message"); 18 addReplyBulk(c, channel); 19 addReplyBulkCString(c, event); 20 } 21 } 22 sdsfree(channel); 23} 24
代码示例
以下是一个简单的代码示例,展示了如何在客户端启用缓存并处理键变更通知:
1#include <stdio.h> 2#include <stdlib.h> 3#include <hiredis/hiredis.h> 4 5void onMessage(redisAsyncContext *c, void *reply, void *privdata) { 6 redisReply *r = (redisReply *)reply; 7 if (r == NULL) return; 8 printf("Received message: %s\n", r->element[2]->str); 9} 10 11int main(int argc, char **argv) { 12 redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379); 13 if (c->err) { 14 printf("Error: %s\n", c->errstr); 15 return 1; 16 } 17 18 // 启用客户端缓存 19 redisAsyncCommand(c, NULL, NULL, "CLIENT CACHING yes"); 20 21 // 订阅键空间事件 22 redisAsyncCommand(c, onMessage, NULL, "PSUBSCRIBE __keyspace@0__:mykey"); 23 24 // 事件循环 25 redisAsyncSetConnectCallback(c, NULL); 26 redisAsyncSetDisconnectCallback(c, NULL); 27 redisAsyncCommand(c, NULL, NULL, "PING"); 28 29 redisAsyncContext *sub = redisAsyncConnect("127.0.0.1", 6379); 30 redisAsyncCommand(sub, NULL, NULL, "SUBSCRIBE __keyspace@0__:mykey"); 31 32 redisEventLoop(sub); 33 34 redisAsyncFree(c); 35 return 0; 36} 37
总结
Redis 的客户端缓存通过订阅机制和通知机制实现,确保客户端能够及时获取键的变更信息,从而保持缓存的一致性。通过启用客户端缓存和订阅键空间事件,客户端可以有效地减少与 Redis 服务器的通信频率,并显著提升应用的响应速度。上述代码示例展示了如何启用客户端缓存并处理键变更通知,以及 Redis 在后台如何管理和发送变更通知的实现细节。
《Redis(136)Redis的客户端缓存是如何实现的?》 是转载文章,点击查看原文。