vault backup: 2026-05-26 14:33:05
This commit is contained in:
@@ -51,6 +51,7 @@ flowchart LR
|
|||||||
|
|
||||||
```c
|
```c
|
||||||
// 以 sdshdr8 为例(适用于字符串长度 < 256 字节的情况)
|
// 以 sdshdr8 为例(适用于字符串长度 < 256 字节的情况)
|
||||||
|
// 注意:len 和 alloc 都是 unsigned,能表示 0~255
|
||||||
struct __attribute__ ((__packed__)) sdshdr8 {
|
struct __attribute__ ((__packed__)) sdshdr8 {
|
||||||
uint8_t len; // 已使用的字节数(不含末尾 '\0')
|
uint8_t len; // 已使用的字节数(不含末尾 '\0')
|
||||||
uint8_t alloc; // 分配的总字节数(不含末尾 '\0' 和 header)
|
uint8_t alloc; // 分配的总字节数(不含末尾 '\0' 和 header)
|
||||||
@@ -65,6 +66,9 @@ struct __attribute__ ((__packed__)) sdshdr8 {
|
|||||||
> [!tip] `__attribute__((packed))` 的作用
|
> [!tip] `__attribute__((packed))` 的作用
|
||||||
> 编译器默认会对结构体做**内存对齐**(padding),`packed` 告诉编译器"不要加任何填充字节"。SDS header 可能只有 3~5 字节,对齐填充会浪费大量空间。
|
> 编译器默认会对结构体做**内存对齐**(padding),`packed` 告诉编译器"不要加任何填充字节"。SDS header 可能只有 3~5 字节,对齐填充会浪费大量空间。
|
||||||
|
|
||||||
|
> [!note] 版本历史:这套体系从 Redis 3.2 开始
|
||||||
|
> Redis 3.2 之前只有一个统一的 `struct sdshdr`(8B header,`len` 和 `alloc` 都是 `int`),无论字符串多短都要付出 8B header 的代价。antirez 在 3.2 引入了 5 级 header + `packed` 属性,**让短字符串的内存开销直接砍掉了几个字节**。在亿级 key 的场景下,这个优化可以节省 GB 级内存。
|
||||||
|
|
||||||
### 五种 sdshdr 类型
|
### 五种 sdshdr 类型
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -90,7 +94,7 @@ flowchart TB
|
|||||||
|
|
||||||
| 类型 | `len` 占用 | `alloc` 占用 | flags 占用 | header 总大小 | 最大长度 |
|
| 类型 | `len` 占用 | `alloc` 占用 | flags 占用 | header 总大小 | 最大长度 |
|
||||||
|------|:---------:|:----------:|:--------:|:-----------:|:-------:|
|
|------|:---------:|:----------:|:--------:|:-----------:|:-------:|
|
||||||
| `sdshdr5` | 0B | 0B | 1B | **3B** | 31B |
|
| `sdshdr5` | 0B | 0B | 1B | **1B** | 31B |
|
||||||
| `sdshdr8` | 1B | 1B | 1B | **3B** | 255B |
|
| `sdshdr8` | 1B | 1B | 1B | **3B** | 255B |
|
||||||
| `sdshdr16` | 2B | 2B | 1B | **5B** | 64KB |
|
| `sdshdr16` | 2B | 2B | 1B | **5B** | 64KB |
|
||||||
| `sdshdr32` | 4B | 4B | 1B | **9B** | 4GB |
|
| `sdshdr32` | 4B | 4B | 1B | **9B** | 4GB |
|
||||||
@@ -234,6 +238,19 @@ static inline size_t sdslen(const sds s) {
|
|||||||
> [!question] `s[-1]` 是什么魔法?
|
> [!question] `s[-1]` 是什么魔法?
|
||||||
> SDS 的 `sds` 类型指向的是 `buf[]` 的起始位置(而不是 header 的起始位置)。所以 `s[-1]` 往回退一个字节就是 `flags` 字段。这种设计让 `sds` 指针可以直接作为普通 `char *` 传给任何 C 函数——**零拷贝兼容**。
|
> SDS 的 `sds` 类型指向的是 `buf[]` 的起始位置(而不是 header 的起始位置)。所以 `s[-1]` 往回退一个字节就是 `flags` 字段。这种设计让 `sds` 指针可以直接作为普通 `char *` 传给任何 C 函数——**零拷贝兼容**。
|
||||||
|
|
||||||
|
### 更多 API
|
||||||
|
|
||||||
|
```c
|
||||||
|
// printf 风格格式化(避免先 snprintf 再 sdscat 的两次拷贝)
|
||||||
|
s = sdscatfmt(s, "age:%i name:%s", 25, "Tom");
|
||||||
|
|
||||||
|
// 修剪:移除指定字符集中的前后字符
|
||||||
|
sdstrim(s, " \t\n"); // 去除空白
|
||||||
|
|
||||||
|
// 子串(原地截取 buf[start..end],更新 len)
|
||||||
|
sdsrange(s, 1, -2); // 只保留第 1 到倒数第 2 个字节(去掉首尾)
|
||||||
|
```
|
||||||
|
|
||||||
## 五、SDS 在 Redis 中的无处不在
|
## 五、SDS 在 Redis 中的无处不在
|
||||||
|
|
||||||
> [!question] SDS 只用在 String 类型里吗?
|
> [!question] SDS 只用在 String 类型里吗?
|
||||||
@@ -272,7 +289,44 @@ flowchart TB
|
|||||||
> [!insight] 为什么小 key 的内存效率很重要?
|
> [!insight] 为什么小 key 的内存效率很重要?
|
||||||
> Redis 里可能有上亿个 key。如果每个 key 浪费 10 字节,一亿个 key 就浪费 ~1GB。SDS 的多级 header 设计就是在这种场景下尽可能压缩每一字节的开销。
|
> Redis 里可能有上亿个 key。如果每个 key 浪费 10 字节,一亿个 key 就浪费 ~1GB。SDS 的多级 header 设计就是在这种场景下尽可能压缩每一字节的开销。
|
||||||
|
|
||||||
## 六、与 C 字符串的对比总结
|
> [!warning] 实际内存开销要算上 jemalloc 对齐
|
||||||
|
> Redis 使用 **jemalloc** 作为默认内存分配器,它会按 size class 对齐分配。上面理论值 30B,但 jemalloc 的最小分配粒度是 **8B**,小对象按 8/16/32/48/64B 对齐。所以这个 30B 的 SDS 实际会占用 **32B**(对齐到 32B 的 size class)。在用 `redis-cli --memory` 或 `MEMORY USAGE` 命令排查内存时要注意这点。
|
||||||
|
|
||||||
|
## 六、SDS 与 String 编码的关系
|
||||||
|
|
||||||
|
> [!question] SDS 就是 Redis String 类型的底层实现吗?
|
||||||
|
> 是,但不完全是。Redis String 类型有三种底层编码,SDS 只在其中两种中出现:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
CMD["SET key value"] --> CHECK{"value 是什么?"}
|
||||||
|
CHECK -->|"能解析为 ≤ 20 位整数"| INT["OBJ_ENCODING_INT, 不使用 SDS"]
|
||||||
|
CHECK -->|"字符串 ≤ 44 字节"| EMB["OBJ_ENCODING_EMBSTR, SDS + redisObject 连续内存"]
|
||||||
|
CHECK -->|"字符串 > 44 字节"| RAW["OBJ_ENCODING_RAW, SDS + redisObject 分开分配"]
|
||||||
|
|
||||||
|
classDef intC fill:#e8f5e9,stroke:#4caf50
|
||||||
|
classDef embC fill:#e1f5fe,stroke:#2196f3
|
||||||
|
classDef rawC fill:#fff3e0,stroke:#ff9800
|
||||||
|
classDef checkC fill:#f3e5f5,stroke:#9c27b0
|
||||||
|
class INT intC
|
||||||
|
class EMB embC
|
||||||
|
class RAW rawC
|
||||||
|
class CHECK checkC
|
||||||
|
```
|
||||||
|
|
||||||
|
| 编码 | 条件 | 是否使用 SDS | 特点 |
|
||||||
|
|------|------|:----------:|------|
|
||||||
|
| `int` | 值能表示为 ≤ 20 位整数 | ❌ | 直接用 `redisObject` 的 `ptr` 字段存整数,零 SDS 开销 |
|
||||||
|
| `embstr` | 字符串 ≤ 44 字节(Redis ≥ 3.2) | ✅ | `redisObject` + SDS header + `buf` **一次 `malloc`** 连续分配,CPU cache 友好 |
|
||||||
|
| `raw` | 字符串 > 44 字节 | ✅ | `redisObject` 和 SDS **分开 `malloc`**,两次分配 |
|
||||||
|
|
||||||
|
> [!tip] embstr 的阈值变化
|
||||||
|
> Redis 3.2 之前 embstr 阈值是 **39 字节**(旧 SDS header 8B)。引入 sdshdr8 后 header 缩到 3B,多出 5B 空间给数据,所以阈值提升到 **44 字节**。embstr 是只读的——一旦你 `APPEND` 或修改 embstr 编码的值,Redis 会自动升级为 `raw` 编码。
|
||||||
|
|
||||||
|
> [!insight] SDS 在 active defragmentation 中的角色
|
||||||
|
> Redis 4.0+ 支持主动碎片整理(activedefrag)。当 jemalloc 报告某个内存页碎片率高时,Redis 会对 SDS 执行 `sdsdup` ——分配新 buffer → 拷贝数据 → 释放旧 buffer,把碎片化的内存"压实"。这是 SDS 在内存优化层面的又一个重要应用场景。
|
||||||
|
|
||||||
|
## 七、与 C 字符串的对比总结
|
||||||
|
|
||||||
| 维度 | C 字符串 (`char *`) | SDS |
|
| 维度 | C 字符串 (`char *`) | SDS |
|
||||||
|------|:------------------:|:---:|
|
|------|:------------------:|:---:|
|
||||||
@@ -283,13 +337,13 @@ flowchart TB
|
|||||||
| 兼容 C 函数 | ✅ 原生 | ✅ 末尾保留 `\0` |
|
| 兼容 C 函数 | ✅ 原生 | ✅ 末尾保留 `\0` |
|
||||||
| 内存开销 | 无 header | 3~17B header |
|
| 内存开销 | 无 header | 3~17B header |
|
||||||
|
|
||||||
## 七、总结
|
## 八、总结
|
||||||
|
|
||||||
> [!summary] SDS 的设计哲学
|
> [!summary] SDS 的设计哲学
|
||||||
> SDS 是一个"看起来简单、细节极其讲究"的结构。它的核心思想是:
|
> SDS 是一个"看起来简单、细节极其讲究"的结构。它的核心思想是:
|
||||||
>
|
>
|
||||||
> 1. **用一点 header 空间换取所有常见操作的效率提升**——`len` 字段让长度查询从 O(N) 变 O(1)
|
> 1. **用一点 header 空间换取所有常见操作的效率提升**——`len` 字段让长度查询从 O(N) 变 O(1)
|
||||||
> 2. **按需选型,极致省内存**——5 种 header 类型,短字符串用 3 字节 header,长字符串用 17 字节
|
> 2. **按需选型,极致省内存**——5 种 header 类型,最短仅 1 字节 header(sdshdr5),最长 17 字节
|
||||||
> 3. **预分配 + 惰性释放减少系统调用**——对 Redis 这种单线程模型,每次系统调用都可能引入延迟抖动
|
> 3. **预分配 + 惰性释放减少系统调用**——对 Redis 这种单线程模型,每次系统调用都可能引入延迟抖动
|
||||||
> 4. **兼容但不受限于 C**——保留 `\0` 能用 C 库,但核心逻辑不依赖它
|
> 4. **兼容但不受限于 C**——保留 `\0` 能用 C 库,但核心逻辑不依赖它
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user