Redis核心架构与底层实现深度解析
集群路由与基础指令
在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利用 free 属性记录未使用的字节数。空间分配策略如下:扩容时不仅分配所需空间,还会额外预留一定空间(如修改后长度为5,可能实际分配12字节以预分配冗余);缩容时暂不释放内存,而是记录在 free 中,留给未来使用。C字符串依靠空字符 \0 判定结尾,无法存储二进制数据,而SDS以二进制安全方式处理缓冲区数据,兼容二进制存储,仅为了兼容部分C函数而在末尾保留 \0。
双端链表
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基于上述数据结构构建了对象系统,包含字符串、列表、哈希、集合、有序集合五类,并实现了引用计数内存回收与对象共享机制,同时记录访问时间以支持空转时长计算。
字符串对象
编码可为 int(保存整数)、embstr(保存≤32字节短字符串,一次内存分配,RedisObject与SDS连续)、raw(保存>32字节长字符串,两次内存分配)。浮点数以字符串形式保存。embstr 为只读,修改时会被转化为 raw。
列表对象
编码可为 ziplist 或 linkedlist(3.2版本后使用 quicklist)。当元素长度<64字节且数量<512时使用 ziplist。
哈希对象
编码可为 ziplist(键值对节点紧挨存放)或 hashtable。当键值长度均<64字节且数量<512时使用 ziplist。
集合对象
编码可为 intset 或 hashtable(值为NULL)。当元素全为整数且数量≤512时使用 intset。
有序集合对象
编码可为 ziplist(元素与分值紧挨存放并按分值排序)或 skiplist。skiplist 底层使用 zset 结构,同时包含跳跃表与字典:跳跃表支持范围查询,字典支持O(1)复杂度分值查询。两者共享元素指针,不产生冗余内存。当元素数量<128且长度<64字节时使用 ziplist。
内存回收与共享
引用计数机制:创建时初始化为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。
RDB文件结构
由REDIS常量、版本号、数据库数据、EOF常量、校验和组成。数据库部分由SELECTDB常量、数据库编号、键值对组成。键值对包含TYPE、key、value,若带过期时间则含EXPIRETIME_MS与毫秒时间戳。
字符串对象若为INT编码则直接保存;若为RAW编码且长度≤20字节原样保存,>20字节启用LZF压缩保存。
列表、集合、哈希、有序集合等对象按各自结构依次保存元素数量与内容。INTSET与ZIPLIST编码对象在RDB中会先转换为字符串对象再保存。
AOF持久化
通过记录所有写命令来还原数据库状态。分为命令追加(写入 aof_buf 缓冲区)、文件写入、文件同步三步。同步策略由 appendfsync 决定(always / everysec / no),默认everysec。
AOF载入过程:创建伪客户端 -> 循环读取并执行写命令。AOF重写通过读取当前数据库状态直接生成精简命令,在子进程中执行。重写期间主进程收到的写命令会同时记录到AOF重写缓冲区,子进程完成后由主进程将缓冲区追加至新AOF文件并覆盖旧文件。
事件机制
Redis是事件驱动程序,处理文件事件(网络套接字操作)与时间事件(定时任务)。
文件事件处理器基于Reactor模式,使用I/O多路复用监听套接字,分派给对应的事件处理器(连接应答、命令读取、命令回复等)。
时间事件存放在无序链表中,定时器遍历链表查找并执行到期事件。serverCron 是最重要的周期性时间事件。
客户端属性
客户端状态 redisClient 保存了套接字描述符、名字、标志、数据库指针、输入缓冲区(querybuf)、命令参数(argv, argc)、输出缓冲区(固定 buf 与可变 reply 链表)、身份验证状态等。伪客户端(fd=-1)用于AOF载入与Lua脚本执行。
输出缓冲区存在硬性限制与软性限制,超限将导致客户端被关闭。
服务器运作
命令执行过程:客户端发送协议 -> 服务器读取分析存入 argv -> 查表定位命令实现函数 -> 执行预备操作 -> 执行命令 -> 返回回复。
serverCron 每100ms执行,负责更新时间缓存、LRU时钟、管理内存与持久化等。服务器初始化经历状态创建、配置载入、数据结构初始化、数据还原、开启事件循环五步。
主从复制
旧版SYNC命令全量同步开销极大。2.8版后引入PSYNC命令,支持完整重同步与部分重同步。部分重同步依赖复制偏移量、复制积压缓冲区(1MB FIFO队列)与运行ID。
复制实现流程:设置主节点 -> 建立套接字 -> 发送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命令开启,服务器处理命令前将信息发送给所有监视器客户端。