Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis数据存储原理和结构

Redis数据存储原理和结构解读

作者:浅慕Antonio

这篇文章详细介绍了Redis的数据结构和过期机制,包括键值对存储、哈希表、字典、跳表等,并探讨了大Key问题和解决方法

1.1 KV结构

Redis 本质上是一个 Key-Value(键值对,KV)数据库,在它丰富多样的数据结构底层,都基于一种统一的键值对存储结构来进行数据的管理和操作

Redis 使用一个全局的哈希表来管理所有的键值对,这个哈希表就像是一个大字典,通过键(Key)能快速定位到对应的值(Value)。

从用户视角看,用户使用各种命令操作不同类型的数据结构(如 String、Hash、List 等),但从底层实现角度,它们都是以键值对形式存储在这个哈希表中。

Redis的存储分类的目的是,在数据量小的时候,追求存储效率高;在数据量大的时候,追求运行速度快。

数据类型(Value 所属类型)编码方式触发条件
stringint字符串长度 ≤ 20 且可转换为整数(如 "123")
embstr字符串长度 ≤ 44
raw字符串长度 > 44
cpu 缓存行优化(补充逻辑,基于缓存行 64 字节设计,辅助提升访问效率,非独立编码类型)
listquicklistRedis 3.2+ 默认实现,是 ziplist + 双向链表的混合结构(动态适配数据量)
ziplist数据量小时被 quicklist 间接使用(紧凑存储小列表)
hashziplist节点数量 ≤ 512(hash-max-ziplist-entries)且字符串长度 ≤ 64(hash-max-ziplist-value)
dict节点数量 > 512 或字符串长度 > 64
setintset元素全为整数且数量 ≤ 512(set-max-intset-entries)
dict元素含非整数或数量 > 512
zsetziplist子节点数量 ≤ 128(zset-max-ziplist-entries)且字符串长度 ≤ 64(zset-max-ziplist-value)
skiplist子节点数量 > 128 或字符串长度 > 64

1.2 字典(dict)

Redis 中的字典(dict) 是一种高效的键值对存储结构,广泛用于实现 Redis 核心的 KV 存储(全局键值对)以及 Hash 类型在数据量较大时的底层存储(当 Hash 节点数>512 或字符串长度>64 时)。

其设计借鉴了哈希表的思想,通过链地址法处理冲突,并支持动态扩容(重哈希)以保证性能

字典数据结构组成

字典由三个核心结构体组成,层层嵌套实现哈希表的功能:

1.dictEntry哈希表节点

存储单个键值对(key-value),next 指针用于连接哈希冲突的节点(形成链表)

typedef struct dictEntry {
    void *key;                  // 键(字符串类型)
    union {                     // 值(支持多种类型)
        void *val;              // 指针类型(如复杂数据结构)
        uint64_t u64;           // 无符号整数
        int64_t s64;            // 有符号整数
        double d;               // 浮点数
    } v;
    struct dictEntry *next;     // 下一个节点指针(处理哈希冲突的链表)
} dictEntry;

2.dictht哈希表

管理哈希表数组,table 是存放 dictEntry 指针的数组,sizemask 用于快速计算键的索引(替代取余运算)。

typedef struct dictht {
    dictEntry **table;          // 哈希表数组(存储dictEntry指针)
    unsigned long size;         // 数组长度(必须是2的幂,如4、8、16...)
    unsigned long sizemask;     // 掩码,值为 size-1(用于计算数组索引)
    unsigned long used;         // 已存储的节点数量(key-value总数)
} dictht;

3.dict字典顶层结构

通过 ht[2] 支持渐进式重哈希(避免一次性扩容的性能波动),rehashidx 跟踪重哈希进度。

typedef struct dict {
    dictType *type;             // 字典类型(包含哈希函数、键值复制/释放函数等)
    void *privdata;             // 私有数据(供type中的函数使用)
    dictht ht[2];               // 两个哈希表(ht[0]:当前使用;ht[1]:重哈希时使用)
    long rehashidx;             // 重哈希索引(-1表示未进行重哈希;≥0表示当前迁移进度)
    int16_t pauserehash;        // 重哈希暂停标记(>0表示暂停,避免遍历期间干扰)
} dict;

哈希运算与索引映射

字典通过哈希函数将 key 映射到哈希表数组的索引,步骤如下:

计算哈希值:对 key(字符串)执行哈希函数(如 siphash),得到一个 64 位整数哈希值。

映射到数组索引:利用 sizemasksize-1)对哈希值进行位运算(哈希值 & sizemask),得到数组索引。

哈希冲突的处理

由于哈希值范围(64 位)远大于哈希表数组长度(size),根据抽屉原理(n+1 个苹果放入 n 个抽屉,至少一个抽屉有 2 个),必然存在不同 key 映射到同一索引的情况(哈希冲突)。

字典采用链地址法解决冲突:

渐进式重哈希(扩容 / 缩容)

当哈希表的负载因子used / size)过高(扩容)或过低(缩容)时,需要调整数组大小以优化性能:

为避免一次性迁移所有节点导致的性能阻塞,字典采用渐进式重哈希

初始化:创建 ht[1] 并设置新 size(如扩容为 ht[0].size * 2),rehashidx 设为 0(开始重哈希)。

渐进式迁移

完成迁移:当 ht[0].used 变为 0 时,释放 ht[0],将 ht[1] 赋值给 ht[0]ht[1] 重置为空,rehashidx 设为 - 1(重哈希结束)。

scan

Redis 的 scan 命令是用于渐进式遍历集合类数据(如键空间、哈希表、集合等)的命令,主要解决了 keys 命令因一次性全量遍历导致服务器阻塞的问题

实现目标

遍历机制

1.3 Expire 机制

Redis 的 expire 机制用于管理设置了过期时间的键(key),通过两种核心策略结合,在 “内存占用” 与 “性能消耗” 之间平衡,确保过期键能被及时清理

1. 惰性删除(Lazy Expiration)

触发时机

执行逻辑

特点

相关命令

2. 定时删除(Active Expiration)

为弥补惰性删除的内存泄漏问题,Redis 同时采用定时删除策略,主动清理部分过期 key。

核心配置与逻辑

​
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /*
Keys for each DB loop. */
/*The default effort is 1, and the maximum
configurable effort
     * is 10. */
config_keys_per_loop =
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                          
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
int activeExpireCycleTryExpire(redisDb *db,
dictEntry *de, long long now);

​

特点

惰性删除保证 “访问时过期键必被清理”,避免无效数据干扰;定时删除主动清理 “长期未访问的过期键”,防止内存溢出。两者结合,既保证了性能,又控制了内存占用

1.4 大Key

“大 Key” 指 Redis 中存储的体积过大的对象(如包含数万字段的 Hash、百万元素的 ZSet 等),这类 key 会导致明显的性能问题

大 Key 的危害

大Key 的检测

通过 Redis 自带的 redis-cli --bigkeys 命令检测大 Key,该命令基于 SCAN 遍历所有键,统计不同类型中体积最大的键。

redis-cli -h 127.0.0.1 --bigkeys -i 0.1

解决方案

1.5 跳表

Redis 中的跳表(Skiplist)是一种多层级有序链表结构,主要用于实现有序集合(ZSet),支持高效的范围查询(如 ZRANGEZREVRANGE)和排序操作。其设计兼顾了查询性能与实现复杂度,通过 “空间换时间” 的思路,在保证平均时间复杂度接近 平衡树的同时,避免了复杂的节点分 裂 / 重构操作

为什么使用跳表?

跳表的诞生是为了解决 “有序集合的高效查询与修改” 问题:

跳表时间复杂度分析

理想跳表的逻辑

理想情况下,跳表每间隔一个节点生成一个 “高层级节点”,模拟二叉树结构(如第 1 层每 2 个节点有 1 个高层节点,第 2 层每 4 个节点有 1 个高层节点),此时查询时间复杂度为 O(logN)。但这种结构在插入 / 删除后难以维护(重构成本极高)。

概率化跳表(实际实现)

为避免重构,跳表采用概率化层级生成

跳表的实现

Redis 跳表通过 zskiplistNode(节点)和 zskiplist(跳表顶层结构)实现,配合 zset 结构体与字典(dict)协同工作

// 跳表节点
typedef struct zskiplistNode {
    sds ele;               // 元素值(字符串)
    double score;          // 排序分数(仅支持浮点数)
    struct zskiplistNode *backward; // 后退指针(用于反向遍历)
    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针(当前层级的下一个节点)
        unsigned long span;            // 跨度(当前节点到下一个节点的元素数量,用于计算排名 ZRANK)
    } level[];             // 动态层级数组(长度为节点实际层级)
} zskiplistNode;

// 跳表顶层结构
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 头节点、尾节点
    unsigned long length;                // 元素总数(对应 ZCARD 命令)
    int level;                           // 当前跳表的最高层级
} zskiplist;

// ZSet 结构体(结合跳表与字典)
typedef struct zset {
    dict *dict;            // 字典:key=元素值,value=分数(快速查询元素分数)
    zskiplist *zsl;        // 跳表:按分数排序,支持范围查询
} zset;

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文