Redis String的底层实现:你以为只是字符串?其实是变形金刚!

127 阅读4分钟

Redis String的底层实现:你以为只是字符串?其实是变形金刚!

—— 从源码级解剖,看String如何“七十二变”


一、SDS:Redis的“超级字符串”

Redis的String类型底层用**SDS(Simple Dynamic String)**实现,和C语言的字符串(char[])相比,它更像一个“智能机器人”:

SDS结构体(C语言版)

struct sdshdr {  
    int len;      // 已用长度(O(1)获取字符串长度!)  
    int alloc;    // 总分配空间(不包括header和结尾的\0)  
    char buf[];   // 实际数据(可存储任意二进制数据,包括\0)  
};  

SDS的“超能力”

  1. 长度秒读:C字符串要遍历到\0才能算长度(O(n)),而SDS直接读len属性(O(1))。
  2. 杜绝缓冲区溢出:拼接字符串前自动检查空间,不够就扩容(C语言需要手动处理)。
  3. 二进制安全:允许中间出现\0(比如存一张猫图),彻底摆脱C字符串的限制。
  4. 内存管理黑科技
    • 空间预分配:扩容时,<1MB则翻倍(比如从10KB→20KB),≥1MB则每次+1MB。
    • 惰性释放:缩短字符串时,不立刻释放内存,而是留着alloc字段记录空闲空间(下次直接用)。

比喻

  • C字符串像“固定尺寸行李箱”——装不下就崩溃。
  • SDS像“智能伸缩行李箱”——装不下自动扩容,装少了也不急着压缩。

二、编码切换:String的“三副面孔”

Redis会根据存储内容,自动切换String的编码模式,内存占用和性能天差地别!

编码类型触发条件特点适用场景
int8字节以内的整数直接存数值,无额外指针开销计数器(INCR操作)
embstr≤44字节的字符串RedisObject和SDS“同居”内存短字符串、状态标记
raw>44字节的字符串或修改过的embstrRedisObject和SDS“分居”内存长字符串、二进制数据

为什么是44字节?

  • RedisObject占16字节,SDS头占3字节(sdshdr8),加上结尾的\0,总内存对齐到64字节:
    64 - 16 (RedisObject) - 3 (SDS头) - 1 (\0) = 44字节
  • (不同Redis版本可能有微小差异,但原理一致)

编码切换的“秘密规则”

  • 对embstr字符串做修改(如APPEND),会自动转成raw编码(因为embstr内存不可变)。
  • 数字超过8字节(如9223372036854775808)会从int转成embstr或raw。

三、内存布局:从“蜗居”到“豪宅”

1. int编码(极致紧凑)

[RedisObject]type=String, encoding=int, ptr直接指向整数  
  • 无SDS结构,内存占用最小(但只能存整数)。

2. embstr编码(甜蜜同居)

[RedisObject][sdshdr][buf...]  
  • RedisObject和SDS连续存储在同一内存块(减少内存碎片,提升CPU缓存命中率)。

3. raw编码(分居两地)

[RedisObject] → ptr → [sdshdr][buf...]  
  • RedisObject和SDS分散存储,适合大字符串(但内存占用稍高)。

内存开销对比(示例)

  • 存数字12345678
    • int编码:16字节(RedisObject) + 0(无SDS) = 16字节
    • 若用String存为"12345678":16 + (3 + 8 + 1) = 28字节(浪费43%!)

四、设计哲学:为什么Redis这么“抠门”?

1. 极致的空间利用

  • int编码省去指针和SDS头,适合高频访问的计数器。
  • embstr编码减少内存碎片,提升CPU缓存效率。

2. 性能与成本的平衡

  • 小字符串用embstr(快速访问),大字符串用raw(灵活修改)。
  • 空间预分配 vs 惰性释放:用空间换时间(减少内存重分配次数)。

3. 隐藏的“内存刺客”

  • 存一个10位数字符串(如手机号),实际内存消耗≈64字节(RedisObject + SDS头 + 数据)!
  • 解决方案:改用Hash存储小数据(比如HSET user:1 phone 13800138000),内存立省50%+!

五、实战技巧:如何让String“瘦身”?

  1. 数字优先用int编码
    • SET key 123而非SET key "123"(前者自动转int,省内存)。
  2. 短字符串控长≤44字节
    • 比如用户状态标记用active而非is_user_active(省下10字节)。
  3. 大文本拆分成Hash
    • 用户信息用HMSET user:1 name "老王" age 18,而非JSON字符串。
  4. 慎用APPEND
    • 对embstr做APPEND会触发转码成raw,内存占用飙升!

彩蛋

  • 如果SDS会说话,它一定会吐槽:“那些用String存10万用户JSON的,你们的良心不会痛吗?”
  • Redis的源码(sds.h、object.c)其实比你想的简单,感兴趣可以挑战一下!(作者亲测,头发还在)

附:SDS内存分配示意图(embstr编码)

|----------- 64字节内存块 -----------|  
| RedisObject (16B) | sdshdr (3B) | buf (44B) | \0 (1B) |  

(一室一厅,紧凑高效!)