Redis的数据结构

所有的结构

  • Redis 里每个值都是一个 redisObject(简称 robj),它是所有类型的公共外壳:
c 复制代码
typedef struct redisObject {
    unsigned type:4;       // 类型:STRING / LIST / HASH / SET / ZSET
    unsigned encoding:4;   // 编码 ← OBJECT ENCODING 读的就是这个字段
    unsigned lru:24;       // LRU/LFU 时间戳(内存淘汰用)
    int refcount;          // 引用计数(共享对象、写时复制用)
    void *ptr;             // 指针 → 真正的底层数据结构
} robj;
  • 五种 type,八种 encoding
type encoding(OBJECT ENCODING 打印值) 真正的 C 数据结构 触发条件
String int robj->ptr 直接存 long 值 值是 64 位整数
String embstr robj + sdshdr8 一次分配 字符串 ≤ 44 字节
String raw robj 与 sdshdr 分开分配 字符串 > 44 字节
List quicklist 双向链表,节点内装 listpack 恒定(3.2+)
Hash listpack(旧版叫 ziplist) 连续内存紧凑表 字段 ≤128 且 值 ≤64 字节
Hash hashtable dict(哈希表) 超过阈值
Set intset 有序整数数组 全整数 且 元素 ≤512
Set hashtable dict(value 全为 NULL) 超过阈值
ZSet listpack(旧版 ziplist) 连续内存紧凑表 元素 ≤128 且 member ≤64 字节
ZSet skiplist skiplist + dict 联合体 超过阈值

String

int编码

c 复制代码
SET count 12345
↓
robj { type=STRING, encoding=INT, ptr=(void*)12345 }
  • 就是存数字的,比较好INCR等操作

embstr和raw编码

  • 这两底层都是SDS,差别在内存分配次数

    embstr(≤44 字节):
    ┌──────────────────────────────────┐
    │ robj(16B) │ sdshdr8(3B) │ 字符串 │ ← 一次 malloc
    └──────────────────────────────────┘

    raw(>44 字节):
    ┌───────────┐ ┌──────────────────────┐
    │ robj(16B) │ ─ptr→ │ sdshdr16/32/64 │ 字符串 │ ← 两次 malloc
    └───────────┘ └──────────────────────┘

  • embstr 只读一次缓存行就拿到所有元数据

  • 切换点:embstr 是只读优化,一旦修改(APPEND/SETRANGE)就转成 raw,不再转回来

SDS
  • SDS(Simple Dynamic String)是 Redis 自己实现的动态字符串结构,用来替代 C 语言原生的 char* 字符串
c 复制代码
struct sdshdr8 {
    uint8_t  len;      // 已使用字节数(不含 \0)
    uint8_t  alloc;    // 实际分配的字节数(不含 \0 和 header)
    unsigned char flags;  // 低 3 位标记是 sdshdr8/16/32/64
    char buf[];        // 真正存字符串的地方,末尾永远有 \0
};
  • 解决了
    • 多一个字段len来记录长度
    • 用len 字段记录长度,不用遍历到/0就可以知道长度,buf 里可以随便存 \0
    • 拼接字符串的时候会检查 alloc 够不够,不够就自动扩容
    • 有一套空间预分配策略,不需要频繁realloc

List

只有quicklist编码

复制代码
┌─────────────────────────────────────────────────────────┐
│ robj(类型壳)                                           │
│   type = REDIS_LIST          ← 标识这是一个 List         │
│   encoding = OBJ_ENCODING_QUICKLIST ← 当前唯一编码       │
│   ptr ──────────────────┐                               │
└─────────────────────────┼───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│ quicklist(链表层)                                      │
│                                                         │
│  quicklistNode ──→ quicklistNode ──→ quicklistNode       │
│    │                  │                  │               │
│    ▼                  ▼                  ▼               │
│  ┌──────────┐      ┌──────────┐      ┌──────────┐       │
│  │ listpack │      │ listpack │      │ listpack │       │
│  │  8KB max │      │  8KB max │      │  8KB max │       │
│  └──────────┘      └──────────┘      └──────────┘       │
│    ↑ 每个节点里的 listpack 是一段连续内存                 │
└─────────────────────────────────────────────────────────┘
  • List 的真实编码就是 quicklist,listpack 只是 quicklist 内部每个节点存数据用的格式

  • listpack 是 Redis 7.0 引入的紧凑连续内存结构,用来替代旧的 ziplist(解决了 ziplist 的连锁更新问题)它把多个元素连续存放在一个数组里:

    一个 listpack 内存布局(存 ["a", "bb", "ccc"]):

    ┌────┬──────┬────┬─────┬─────┬─────┬─────┬─────┬──────┐
    │total│entry1│len1│entry2│len2 │entry3│len3 │back │ │
    │bytes│ "a" │ 1 │ "bb" │ 2 │"ccc" │ 3 │log │ 0 │
    │ 2B │ 1B │1B │ 2B │1B │ 3B │1B │ 2B │ 1B │
    └────┴──────┴────┴─────┴─────┴─────┴─────┴─────┴──────┘
    ↑ 一段连续内存,中间没有缝隙

  • 核心优势:元素在内存里紧挨着放,CPU 访问时缓存命中率高

  • 如果整个 List 就是一个超大的 listpack,每次中间插入都要搬移大量内存。所以 quicklist 把它切成多个小段:

Hash

  • 记住Hash 本质是 field → value 的映射关系

listpack编码

复制代码
HSET user:1 name zhang age 20

内存布局:
┌─────────────────────────────────────────────────────────────┐
│ entry1    │ entry2   │ entry3  │ entry4 │ entry5 │ entry6   │
│ "name"    │ "zhang"  │ "age"   │ "20"   │ (backlog)        │
│  4B       │  5B      │  3B     │  2B    │  ...             │
└─────────────────────────────────────────────────────────────┘
                         ↑ 一整块连续内存,没有任何指针
  • field 和 value 交替存放
  • 内存紧凑,无指针开销
  • 遍历时缓存命中率极高

hashtable / dict编码

复制代码
┌─────────────────────────────────────────────────────────────┐
│ dict                                                        │
│                                                             │
│  ht[0] ──→ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐      │
│            │  0  │  1  │  2  │  3  │ ... │     │65535│      │
│            └──┬──┴─────┴──┬──┴─────┴─────┴─────┴─────┘      │
│               │           │                                 │
│               ▼           ▼                                 │
│          dictEntry    dictEntry ──→ dictEntry               │
│          {field,val}  {field,val}  {field,val}              │
│           NULL          next           next                  │
│                                                             │
│  ht[1] ──→ (rehash 时才使用,平时是空的)                   │
└─────────────────────────────────────────────────────────────┘
  • 当 Hash 字段增多,listpack 的线性查找太慢,必须换成 hashtable

Set

  • Set 是无序、元素唯一的集合

intset(纯整数小集合)

复制代码
SADD myset 1 2 3 5 8

内存布局:
┌──────────────┬─────────┬───────────────────────────┐
│ header       │         │ data                      │
├──────┬───────┼─────────┼─────┬─────┬─────┬─────┬───┤
│encoding│ len │  保留    │  1  │  2  │  3  │  5  │ 8 │
│ 2B   │  2B  │  2B     │ 2B  │ 2B  │ 2B  │ 2B  │2B │
└──────┴───────┴─────────┴─────┴─────┴─────┴─────┴───┘
                    ↑ 整块连续内存,元素按值从小到大排序
  • 排序是因为查找的时候是用二分查找,比较快

hashtable(转成 dict)

复制代码
SADD myset a b c d ... (513个元素)

转换后:
robj → dict → ht[0] → [桶数组] → dictEntry{key, val}
                                     ↑
                                   val = NULL(Set 只需要 key)
  • Set 用 dict 但 value 是 NULL,因为Set只需要判断存不存在

ZSet

  • ZSet(有序集合)
  • 要满足查某一个,也要满足查范围

只用 dict(哈希表)编码

复制代码
dict: member → score
┌────────────────────────────────────┐
│  zhang → 90                        │
│  li    → 85                        │
│  wang  → 88                        │
│  ...                               │
└────────────────────────────────────┘

ZSCORE zhang → O(1) ✓ 完美

ZRANGEBYSCORE 80 90:
  遍历整个 dict(O(N))
  筛选分数在 80-90 的
  排序(O(M logM))

  总复杂度:O(N + M logM) ✗ 太慢

只用 skiplist(跳表)编码

复制代码
skiplist(按 score 排序):

Level 2:  head ──────────────────────────→ NULL
Level 1:  head ──→ [li:85] ────→ [wang:88] ──→ [zhang:90] ──→ NULL
Level 0:  head ──→ [li:85] ──→ [wang:88] ──→ [zhang:90] ──→ NULL

ZRANGEBYSCORE 80 90 → O(logN + M) ✓ 完美

ZSCORE zhang:
  从头遍历找 zhang(跳表按 score 排序,不是按 member)
  平均 O(N) ✗ 太慢
  • 跳表的致命弱点:它按 score 排序,无法快速定位到某个 member。
补充一下跳表是什么
  • 在有序链表上再叠几层"快线",越上层节点越稀疏、跨得越远

    Level 3: 1 ───────────────────────→ 9
    Level 2: 1 ────────→ 5 ──────────→ 9
    Level 1: 1 ─→ 3 ─→ 5 ─→ 7 ─→ 9 ← 最底层,全部元素

从最高层最左开始:

1.往右走,如果右边的节点仍然 ≤ 目标,就继续走;

2.否则下沉一层继续;

3.到达第 1 层还找不到就是不存在。

Redis两个都用

c 复制代码
ZADD rank 90 zhang 85 li 88 wang
  • 实际存的

    ┌─────────────────────────────────────────────────────────────┐
    │ dict │
    │ "zhang" → score=90 │
    │ "li" → score=85 │
    │ "wang" → score=88 │
    └─────────────────────────────────────────────────────────────┘

    ┌─────────────────────────────────────────────────────────────┐
    │ skiplist │
    │ │
    │ Level 2: head ────────────────────────────→ [zhang:90] │
    │ Level 1: head ──→ [li:85] ──────→ [wang:88] → [zhang:90] │
    │ Level 0: head ──→ [li:85] → [wang:88] → [zhang:90] → NULL │
    │ ↑ │
    │ score 从小到大排序 │
    └─────────────────────────────────────────────────────────────┘

  • 两种查询各走各的路

相关推荐
ss2731 小时前
PostgreSQL咨询锁的使用
数据库·postgresql
ycydynq1 小时前
django 添加信息时,对外键设置条件
数据库·python·django
m0_639906941 小时前
MySQL 索引底层原理|B + 树、聚簇索引、非聚簇索引、覆盖索引
数据库·mysql
全栈陈序员1 小时前
【Python】基础语法入门(二十八)—— 数据库操作入门:用 SQLite 持久化数据
数据库·人工智能·python
東隅已逝,桑榆非晚1 小时前
map和set的使用
数据结构·c++·笔记·学习
2601_970703831 小时前
除湿机原理研究报告:一台机器在物理上能走多远
大数据·数据结构·算法·数据库架构·推荐算法
jnrjian2 小时前
Oracle RAC crs stop时force 关闭DB,另一个node会立即进行recover
数据库·oracle
次次皮2 小时前
Oracle判空踩坑(对比mysql)
数据库·mysql·oracle
严谨的麻辣烫2 小时前
固定 IP 稳定性如何检测?使用 Python 实现网络连通性与耗时监控
开发语言·数据库·php