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 在后台如何管理和发送变更通知的实现细节。

相关推荐
ZePingPingZe3 分钟前
Spring Boot常见注解
java·spring boot·后端
SimonKing5 分钟前
镜像拉不下来怎么办?境内Docker镜像状态在线监控来了
java·后端·程序员
a程序小傲6 分钟前
华为Java面试被问:SQL执行顺序
java·后端·sql·华为·面试
码上成长14 分钟前
长耗时接口异步改造总结
前端·git·后端
diudiu962823 分钟前
Logback使用指南
java·开发语言·spring boot·后端·spring·logback
Lisonseekpan39 分钟前
Elasticsearch 入门指南
大数据·分布式·后端·elasticsearch·搜索引擎
zhangyifang_00941 分钟前
Spring中的BeanDefinition
java·后端·spring
楠枬1 小时前
负载均衡 -LoadBalance
后端·spring·spring cloud·负载均衡
milanyangbo1 小时前
深入解析 Disruptor:从RingBuffer到缓存行填充的底层魔法
java·数据库·后端·架构
计算机学姐1 小时前
基于Python的智能点餐系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask