当前位置:首页 > 技术 > 正文内容

Redis核心架构与底层实现深度解析

访客 技术 2026年10月21日 1

集群路由与基础指令

在Redis集群模式下,客户端发起针对特定键的指令时,系统首先会计算该键所属的哈希槽,随后定位到负责该槽的节点,最终由该节点执行指令并返回结果。

可以通过OBJECT ENCODING指令探查数据库键的底层值编码方式,例如执行 OBJECT ENCODING user_key 来查看 user_key 的内部编码。使用TYPE指令则可获取值的数据类型,例如 TYPE user_key。

对象编码查看

其他常用运维指令包括:

  • OBJECT REFCOUNT: 获取值对象的引用计数。
  • OBJECT IDLETIME: 获取键的空转时长。
  • SELECT: 切换逻辑数据库。
  • FLUSHDB: 清空当前库。
  • DBSIZE: 获取键总数。
  • INFO: 查看服务器详情(如 INFO server)。
  • TTL / PTTL: 查看剩余存活时间(秒/毫秒)。
  • PERSIST: 移除键的过期时间。
  • SLAVEOF: 将当前节点设为指定主节点的从节点。
  • CLUSTER KEYSLOT: 查看键对应的集群哈希槽。
  • SUBSCRIBE: 订阅频道,例如 SUBSCRIBE "event.order"。
  • PSUBSCRIBE: 订阅模式,例如 PSUBSCRIBE "event.*" 订阅所有以 event. 开头的频道。
  • PUBLISH: 发布消息,例如 PUBLISH "event.order" "created"。

核心数据结构

简单动态字符串 (SDS)

Redis并未直接采用C语言的字符串,而是自定义了SDS。C字符串在拼接时缺乏长度校验易导致溢出,而SDS在执行拼接前会自动校验并按需扩容。此外,C字符串获取长度需遍历计数,时间复杂度为O(N),SDS则直接读取 len 属性,复杂度为O(1)。

SDS结构

SDS利用 free 属性记录未使用的字节数。空间分配策略如下:扩容时不仅分配所需空间,还会额外预留一定空间(如修改后长度为5,可能实际分配12字节以预分配冗余);缩容时暂不释放内存,而是记录在 free 中,留给未来使用。C字符串依靠空字符 \0 判定结尾,无法存储二进制数据,而SDS以二进制安全方式处理缓冲区数据,兼容二进制存储,仅为了兼容部分C函数而在末尾保留 \0。

SDS与C字符串对比 SDS内存布局

双端链表

Redis链表具备以下特性:双端(节点含 prev 和 next 指针,前后访问复杂度O(1))、无环(头节点 prev 和尾节点 next 指向NULL)、带首尾指针(通过 head 和 tail 指针快速定位首尾)、带长度计数器(len 属性记录节点数)、多态(使用 void* 保存值,并配备 dup, free, match 函数实现类型定制)。

链表结构
typedef struct List {
    ListNode *headRef;
    ListNode *tailRef;
    unsigned long length;
    void (*copyNode)(void *ptr);
    void (*releaseNode)(void *ptr);
    int (*matchNode)(void *ptr1, void *ptr2);
} List;

字典 (Hash)

底层由两个哈希表组成,常规使用一个,另一个用于Rehash。负载因子计算公式为:used / size(已用节点数 / 哈希表大小)。

扩容与缩容条件:

  • 当服务器未执行BGSAVE或BGREWRITEAOF且负载因子≥1时扩容;若正在执行上述命令且负载因子≥5时也会强制扩容。
  • 当负载因子<0.1时缩容。

为避免子进程使用写时复制导致内存消耗倍增,在持久化期间尽量规避Hash扩容。扩容大小为当前容量的2的N次方(例如当前使用5,扩容至16)。缩容大小为大于当前使用量的最小2的N次方。Rehash过程是渐进式的,分多次逐步完成。

跳跃表

一种有序数据结构,通过多层指针实现快速访问,平均查找复杂度O(logN),最坏O(N)。Redis在有序集合元素较多或成员较长时采用跳跃表,此外在集群节点内部也使用该结构。

跳跃表结构

zskiplist 结构包含:头尾指针、最大层数、节点数量。zskiplistNode 结构包含:层(前进指针与跨度)、后退指针、分值(score,从小到大排列)、成员对象。

整数集合

当集合元素全为整数且数量较少时使用。编码方式根据元素类型决定,若新插入元素类型长度大于现有所有元素,需先进行升级:扩展数组空间 -> 转换现有元素类型并移位 -> 插入新元素。整数集合不支持降级。

压缩列表

为节约内存而设计的顺序型数据结构,用于列表和哈希的底层实现(元素少且短时)。由连续内存块组成,每个节点包含 previous_entry_length、encoding、content 三部分。

压缩列表节点

previous_entry_length 可能为1或5字节,若前驱节点长度变化导致该属性从1字节扩展至5字节,可能引发后续所有节点级联扩容,此现象称为连锁更新。

对象系统

Redis基于上述数据结构构建了对象系统,包含字符串、列表、哈希、集合、有序集合五类,并实现了引用计数内存回收与对象共享机制,同时记录访问时间以支持空转时长计算。

redisObject结构

字符串对象

编码可为 int(保存整数)、embstr(保存≤32字节短字符串,一次内存分配,RedisObject与SDS连续)、raw(保存>32字节长字符串,两次内存分配)。浮点数以字符串形式保存。embstr 为只读,修改时会被转化为 raw。

列表对象

编码可为 ziplist 或 linkedlist(3.2版本后使用 quicklist)。当元素长度<64字节且数量<512时使用 ziplist。

哈希对象

编码可为 ziplist(键值对节点紧挨存放)或 hashtable。当键值长度均<64字节且数量<512时使用 ziplist。

ziplist哈希 hashtable哈希

集合对象

编码可为 intset 或 hashtable(值为NULL)。当元素全为整数且数量≤512时使用 intset。

intset集合 hashtable集合

有序集合对象

编码可为 ziplist(元素与分值紧挨存放并按分值排序)或 skiplist。skiplist 底层使用 zset 结构,同时包含跳跃表与字典:跳跃表支持范围查询,字典支持O(1)复杂度分值查询。两者共享元素指针,不产生冗余内存。当元素数量<128且长度<64字节时使用 ziplist。

ziplist有序集合 zset结构

内存回收与共享

引用计数机制:创建时初始化为1,被使用加1,不再使用减1,归零时释放内存。Redis初始化时预创建0~9999的共享字符串对象,避免重复创建。由于校验字符串是否相同需消耗CPU,故Redis不共享包含复杂结构的字符串对象。

引用计数

数据库实现

服务器状态 redisServer 的 db 数组保存所有数据库,默认16个。客户端状态 redisClient 的 db 指针指向当前使用的数据库。

服务器数据库结构 客户端数据库指针

数据库键空间由 redisDb 的 dict 字典表示。读写键空间时会触发维护操作:更新命中率与LRU时间、处理过期键、标记脏键以通知持久化与复制、触发键空间通知等。

键空间结构

过期键管理

expires 字典保存所有键的过期时间。判定过期:当前时间戳是否大于过期字典记录的时间。

过期字典

删除策略:

  • 定时删除:到期立即删除,对内存友好但消耗CPU。
  • 惰性删除:访问时检查删除,对CPU友好但可能泄漏内存。
  • 定期删除:周期性抽查删除,折中方案。

Redis综合使用惰性删除(expireIfNeeded 函数在命令执行前检查)与定期删除(activeExpireCycle 函数在serverCron中周期执行)。

持久化与复制中的过期键处理:

  • RDB:生成时忽略过期键;主服务器载入时忽略过期键,从服务器载入时全部加载(后续同步会覆盖)。
  • AOF:过期键被删除时追加DEL命令;重写时忽略过期键。
  • 复制:从服务器不主动删除过期键,需等待主服务器发送DEL命令,保证主从一致性。

RDB持久化

将内存状态保存为磁盘二进制文件。

SAVE 阻塞服务器执行;BGSAVE 派生子进程执行,父进程继续处理请求。BGSAVE期间拒绝SAVE与BGSAVE,延迟BGREWRITEAOF。服务器启动时若AOF关闭则自动载入RDB文件,载入期间阻塞。

自动保存条件由 saveparams 数组配置,配合 dirty 计数器(修改次数)和 lastsave 时间戳,serverCron 周期检查是否满足条件触发BGSAVE。

saveparams结构 saveparam结构 dirty与lastsave

RDB文件结构

RDB文件结构

由REDIS常量、版本号、数据库数据、EOF常量、校验和组成。数据库部分由SELECTDB常量、数据库编号、键值对组成。键值对包含TYPE、key、value,若带过期时间则含EXPIRETIME_MS与毫秒时间戳。

多数据库RDB示例 数据库部分结构 无过期时间键值对 带过期时间键值对

字符串对象若为INT编码则直接保存;若为RAW编码且长度≤20字节原样保存,>20字节启用LZF压缩保存。

INT编码 无压缩字符串 压缩后字符串

列表、集合、哈希、有序集合等对象按各自结构依次保存元素数量与内容。INTSET与ZIPLIST编码对象在RDB中会先转换为字符串对象再保存。

列表保存 集合保存 集合保存示例 哈希保存 哈希保存示例 有序集合保存 有序集合保存示例

AOF持久化

通过记录所有写命令来还原数据库状态。分为命令追加(写入 aof_buf 缓冲区)、文件写入、文件同步三步。同步策略由 appendfsync 决定(always / everysec / no),默认everysec。

AOF缓冲区 appendfsync策略

AOF载入过程:创建伪客户端 -> 循环读取并执行写命令。AOF重写通过读取当前数据库状态直接生成精简命令,在子进程中执行。重写期间主进程收到的写命令会同时记录到AOF重写缓冲区,子进程完成后由主进程将缓冲区追加至新AOF文件并覆盖旧文件。

事件机制

Redis是事件驱动程序,处理文件事件(网络套接字操作)与时间事件(定时任务)。

文件事件处理器基于Reactor模式,使用I/O多路复用监听套接字,分派给对应的事件处理器(连接应答、命令读取、命令回复等)。

文件事件处理器

时间事件存放在无序链表中,定时器遍历链表查找并执行到期事件。serverCron 是最重要的周期性时间事件。

客户端属性

客户端状态 redisClient 保存了套接字描述符、名字、标志、数据库指针、输入缓冲区(querybuf)、命令参数(argv, argc)、输出缓冲区(固定 buf 与可变 reply 链表)、身份验证状态等。伪客户端(fd=-1)用于AOF载入与Lua脚本执行。

argv与argc

输出缓冲区存在硬性限制与软性限制,超限将导致客户端被关闭。

服务器运作

命令执行过程:客户端发送协议 -> 服务器读取分析存入 argv -> 查表定位命令实现函数 -> 执行预备操作 -> 执行命令 -> 返回回复。

serverCron 每100ms执行,负责更新时间缓存、LRU时钟、管理内存与持久化等。服务器初始化经历状态创建、配置载入、数据结构初始化、数据还原、开启事件循环五步。

主从复制

旧版SYNC命令全量同步开销极大。2.8版后引入PSYNC命令,支持完整重同步与部分重同步。部分重同步依赖复制偏移量、复制积压缓冲区(1MB FIFO队列)与运行ID。

SYNC通信 部分重同步

复制实现流程:设置主节点 -> 建立套接字 -> 发送PING -> 身份验证 -> 发送端口信息 -> 同步 -> 命令传播。同步后主从互为客户端。

从节点默认每秒发送 REPLCONF ACK 心跳,用于检测连接、辅助min-slaves配置及检测命令丢失。

Sentinel哨兵

高可用方案,监控主从节点,在主节点下线时执行故障转移。启动过程:初始化服务器 -> 替换为Sentinel专用代码 -> 初始化状态 -> 监听主服务器。

哨兵系统
typedef struct SentinelInstance {
    int stateFlags;
    char *identifier;
    char *runtimeId;
    uint64_t epoch;
    sentinelAddr *address;
    mstime_t downTimeout;
    int quorumVotes;
    int syncCount;
    mstime_t failoverDeadline;
} SentinelInstance;

Sentinel创建命令与订阅两条连接连向主服务器,通过INFO命令自动发现从服务器与其他Sentinel。节点间通过 __sentinel__:hello 频道互相通信。主观下线(SDOWN)后向其他Sentinel询问,获得足够票数后标记客观下线(ODOWN)。

领头Sentinel选举采用Raft算法,先到先得且需半数以上支持。故障转移选择新主节点规则:排除下线/断线节点 -> 排除近期未通信节点 -> 优先级最高 -> 复制偏移量最大 -> 运行ID最小。

集群

通过 CLUSTER MEET 握手加入集群,集群分片16384个哈希槽。

typedef struct ClusterNodeDef {
    mstime_t createTime;
    char nodeId[CLUSTER_NAME_SIZE];
    int nodeFlags;
    uint64_t configVersion;
    char nodeIp[IP_STR_LEN];
    int nodePort;
    ClusterConn *connHandle;
    unsigned char assignedSlots[CLUSTER_SLOTS / 8];
    int slotCount;
    struct ClusterNodeDef *masterRef;
    int replicaCount;
    struct ClusterNodeDef **replicaList;
    list *failureReports;
} ClusterNodeDef;
typedef struct ClusterTopology {
    ClusterNodeDef *selfNode;
    uint64_t globalEpoch;
    int clusterStatus;
    int activeNodeCount;
    dict *nodeDictionary;
    ClusterNodeDef *slotAssignments[CLUSTER_SLOTS];
    ClusterNodeDef *importSources[CLUSTER_SLOTS];
    ClusterNodeDef *exportTargets[CLUSTER_SLOTS];
    zskiplist *slotKeyIndex;
} ClusterTopology;
集群状态

客户端请求若键所在槽不归当前节点处理,返回MOVED重定向。重新分片由redis-trib执行,迁移期间若键不在源节点且正在迁移,返回ASK重定向。ASK为临时指引,需先发送ASKING命令。

迁移键过程

集群故障转移由从节点发起,投票选举新主节点。节点间通过Gossip协议(MEET, PING, PONG)交换状态,FAIL消息广播下线事件,PUBLISH消息广播发布订阅事件。

发布与订阅

频道订阅由 pubsub_channels 字典维护,模式订阅由 pubsub_patterns 链表维护。PUBLISH指令同时向频道与匹配模式的订阅者发送消息。PUBSUB命令可查看订阅状态。

typedef struct PatternSubscription {
    redisClient *subscriber;
    robj *patternRule;
} PatternSubscription;

事务

MULTI开启事务,命令入队,EXEC执行。WATCH实现乐观锁,若被监视键在EXEC前被修改,事务拒绝执行。Redis事务具备原子性、一致性、隔离性,但无回滚机制。耐久性取决于持久化配置(appendfsync为always时具备)。

typedef struct TransactionQueue {
    TransactionCmd *cmdArray;
    int cmdCount;
} TransactionQueue;

Lua脚本

通过EVAL与EVALSHA执行。Redis内嵌Lua环境,提供全局 redis 表调用命令。

-- 检查库存并扣减
if redis.call("EXISTS", KEYS[1]) == 1 then
    local stock = tonumber(redis.call("GET", KEYS[1]))
    if stock >= tonumber(ARGV[1]) then
        redis.call("DECRBY", KEYS[1], ARGV[1])
        return 1
    end
end
return 0

SCRIPT系列命令管理脚本缓存与超时。主节点复制EVALSHA命令时,若从节点未缓存该脚本,会自动转换为EVAL命令传播。

SORT命令

底层创建 redisSortObject 数组,按权重排序。支持BY(外部键权重)、LIMIT(分页)、GET(获取外部键)、STORE(保存结果)、ALPHA(字母排序)等选项。执行顺序固定:排序 -> LIMIT -> GET -> STORE -> 返回。

二进制位数组

底层采用SDS逆序存储(便于SETBIT扩展)。GETBIT与SETBIT通过偏移量计算字节索引与位索引。BITCOUNT统计汉明重量,结合查表法与variable-precision SWAR算法实现高效计算。

uint32_t calculateHammingWeight(uint32_t val) {
    val = (val & 0x55555555) + ((val >> 1) & 0x55555555);
    val = (val & 0x33333333) + ((val >> 2) & 0x33333333);
    val = (val & 0x0F0F0F0F) + ((val >> 4) & 0x0F0F0F0F);
    val = (val * 0x01010101) >> 24;
    return val;
}

BITOP基于C内置位运算实现逻辑与、或、异或、非。

慢查询与监视器

慢查询日志由 slowlog-log-slower-than 与 slowlog-max-len 配置,超出上限删除最旧记录。监视器通过MONITOR命令开启,服务器处理命令前将信息发送给所有监视器客户端。

标签: Redis

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。