Redis(136)Redis的客户端缓存是如何实现的?

Redis 的客户端缓存(Client-Side Caching)是一项重要特性,允许客户端在本地缓存 Redis 数据,从而减少与 Redis 服务器的通信频率,提高应用的响应速度和可扩展性。Redis 客户端缓存的实现主要依赖于以下几个核心组件和机制:

  1. 订阅机制:客户端通过订阅特定的键空间事件,获取键的变更通知。
  2. 通知机制:Redis 服务器在键发生变更时,通过发布/订阅(Pub/Sub)机制将变更通知推送给客户端。
  3. 缓存一致性:确保客户端缓存与 Redis 服务器的数据一致性。

核心概念和数据结构

1. 客户端缓存模式

Redis 提供了两种客户端缓存模式:

  • 主动推模式(Tracking Mode):服务器主动向客户端推送键的变更通知。
  • 被动拉模式(Polling Mode):客户端定期从服务器拉取键的变更信息。

2. 客户端状态

每个客户端的状态通过 client 结构体维护,其中包含缓存相关的信息:

c 复制代码
typedef struct client {
    // ...
    uint64_t client_tracking_redirection; /* Redirected ID for tracking invalidation messages */
    uint64_t client_tracking_locks; /* Number of tracking locks */
    uint8_t client_tracking_prefixes; /* Number of tracking prefixes */
    // ...
} client;

启用客户端缓存

客户端缓存可以通过 CLIENT CACHING 命令启用和配置。

c 复制代码
redis> CLIENT CACHING yes

OK

订阅键空间事件

通过订阅键空间事件,客户端能够接收到指定键的变更通知。订阅通过 PSUBSCRIBE 命令实现:

c 复制代码
redis> PSUBSCRIBE "__keyspace@0__:mykey"

通知机制

Redis 服务器通过发布/订阅机制将变更通知推送给客户端。这通过 trackingInvalidateKey 和相关函数实现。

1. 启用键空间通知

首先,需要在 Redis 配置文件中启用键空间通知:

conf 复制代码
notify-keyspace-events Ex

2. 处理键变更事件

当键发生变更时,Redis 会调用 trackingInvalidateKey 函数来处理变更事件。

c 复制代码
void trackingInvalidateKey(client *c, robj *keyobj) {
    if (!keyobj) return;

    sds sdskey = keyobj->ptr;
    size_t keylen = sdslen(sdskey);

    // 查找所有订阅该键的客户端并发送无效化消息
    listNode *ln;
    listIter li;
    listRewind(server.tracking_clients, &li);
    while ((ln = listNext(&li)) != NULL) {
        client *target = listNodeValue(ln);
        if (target == c) continue;

        // 发送无效化消息
        addReplyArrayLen(target, 2);
        addReplyBulkCString(target, "invalidate");
        addReplyBulk(target, keyobj);
    }
}

缓存一致性管理

为了确保客户端缓存的一致性,Redis 通过以下机制管理缓存:

1. 追踪表

Redis 维护一个追踪表,用于记录每个客户端订阅的键。

c 复制代码
typedef struct trackingTableEntry {
    sds key;
    client *c;
} trackingTableEntry;

dict *tracking_table;

2. 键变更通知

当键发生变更时,Redis 会遍历追踪表,查找订阅该键的客户端,并发送变更通知。

c 复制代码
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
    // 构建订阅频道名称
    sds channel = sdsnewlen("__keyspace@", 11);
    channel = sdscatprintf(channel, "%d__", dbid);
    channel = sdscatsds(channel, key);

    // 发送通知
    list *clients = dictFetchValue(tracking_table, channel);
    if (clients != NULL) {
        listIter li;
        listNode *ln;
        listRewind(clients, &li);
        while ((ln = listNext(&li)) != NULL) {
            client *c = listNodeValue(ln);
            if (c->flags & CLIENT_CLOSE_AFTER_REPLY) continue;
            addReplyArrayLen(c, 3);
            addReplyBulkCString(c, "message");
            addReplyBulk(c, channel);
            addReplyBulkCString(c, event);
        }
    }
    sdsfree(channel);
}

代码示例

以下是一个简单的代码示例,展示了如何在客户端启用缓存并处理键变更通知:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <hiredis/hiredis.h>

void onMessage(redisAsyncContext *c, void *reply, void *privdata) {
    redisReply *r = (redisReply *)reply;
    if (r == NULL) return;
    printf("Received message: %s\n", r->element[2]->str);
}

int main(int argc, char **argv) {
    redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
    if (c->err) {
        printf("Error: %s\n", c->errstr);
        return 1;
    }

    // 启用客户端缓存
    redisAsyncCommand(c, NULL, NULL, "CLIENT CACHING yes");

    // 订阅键空间事件
    redisAsyncCommand(c, onMessage, NULL, "PSUBSCRIBE __keyspace@0__:mykey");

    // 事件循环
    redisAsyncSetConnectCallback(c, NULL);
    redisAsyncSetDisconnectCallback(c, NULL);
    redisAsyncCommand(c, NULL, NULL, "PING");

    redisAsyncContext *sub = redisAsyncConnect("127.0.0.1", 6379);
    redisAsyncCommand(sub, NULL, NULL, "SUBSCRIBE __keyspace@0__:mykey");

    redisEventLoop(sub);

    redisAsyncFree(c);
    return 0;
}

总结

Redis 的客户端缓存通过订阅机制和通知机制实现,确保客户端能够及时获取键的变更信息,从而保持缓存的一致性。通过启用客户端缓存和订阅键空间事件,客户端可以有效地减少与 Redis 服务器的通信频率,并显著提升应用的响应速度。上述代码示例展示了如何启用客户端缓存并处理键变更通知,以及 Redis 在后台如何管理和发送变更通知的实现细节。

相关推荐
不知更鸟6 小时前
Django 项目设置流程
后端·python·django
黄昏恋慕黎明7 小时前
spring MVC了解
java·后端·spring·mvc
G探险者9 小时前
为什么 VARCHAR(1000) 存不了 1000 个汉字? —— 详解主流数据库“字段长度”的底层差异
数据库·后端·mysql
百锦再9 小时前
第18章 高级特征
android·java·开发语言·后端·python·rust·django
Tony Bai10 小时前
Go 在 Web3 的统治力:2025 年架构与生态综述
开发语言·后端·架构·golang·web3
程序猿202310 小时前
项目结构深度解析:理解Spring Boot项目的标准布局和约定
java·spring boot·后端
RainbowSea10 小时前
内网穿透配置和使用
java·后端
掘金码甲哥11 小时前
网关上的限流器
后端
q***062911 小时前
搭建Golang gRPC环境:protoc、protoc-gen-go 和 protoc-gen-go-grpc 工具安装教程
开发语言·后端·golang