2.1 SDS:会记住自己长度的字符串


文档摘要

2.1 SDS:会记住自己长度的字符串 本节摘要:Redis 的字符串不是 C 的字符数组,而是 SDS(简单动态字符串)——头部记录长度与分配额的结构体加一块连续缓冲区。它换来 O(1) 取长度、二进制安全、预分配防抖动三个特性,是理解 String 命令复杂度的钥匙。 C 字符串差在哪 C 的字符串以空字符结尾,带来两个先天缺陷。求长度要遍历到结尾,O(n);内容里不能出现空字符,存图片、序列化对象这类二进制数据就废了。Redis 需要把键、值、协议参数全都当字符串处理,这两个缺陷是致命的,于是 SDS 登场。 SDS 结构与 C 字符串对比 SDS 结构与 C 字符串对比 结构里的 alloc 减 len 就是空闲余量。

2.1 SDS:会记住自己长度的字符串

本节摘要:Redis 的字符串不是 C 的字符数组,而是 SDS(简单动态字符串)——头部记录长度与分配额的结构体加一块连续缓冲区。它换来 O(1) 取长度、二进制安全、预分配防抖动三个特性,是理解 String 命令复杂度的钥匙。

C 字符串差在哪

C 的字符串以空字符结尾,带来两个先天缺陷。求长度要遍历到结尾,O(n);内容里不能出现空字符,存图片、序列化对象这类二进制数据就废了。Redis 需要把键、值、协议参数全都当字符串处理,这两个缺陷是致命的,于是 SDS 登场。

SDS 结构与 C 字符串对比

SDS 结构与 C 字符串对比

结构里的 alloc 减 len 就是空闲余量。追加字符串时,如果余量够,一次分配都不发生——这是 SDS 抗抖动的核心。

三种编码:一个 String 键的变脸

OBJECT ENCODING 是本章最常用的一条诊断命令,它直接报告键当前的底层编码:

> SET msg "hi" > OBJECT ENCODING msg "embstr" # 短字符串:SDS 头与内容一次分配,紧挨着放 > SET count 100 > OBJECT ENCODING count "int" # 纯整数:值直接存整数字面量,连 SDS 都省了 > SET big 一段超过四十四字节的字符串…… > OBJECT ENCODING big "raw" # 长字符串:SDS 头与内容分成两次分配 > APPEND msg " there" > OBJECT ENCODING msg "raw" # embstr 是只读的,一旦被修改就转 raw,回不去了
编码 触发条件 特点
int 值是 64 位内的整数 最省内存,INCR 只对它合法
embstr 长度 44 字节以内 单次分配、缓存友好,但只读
raw 超 44 字节或被追加修改 两次分配,适合长内容与频繁追加

44 这个分界不是拍脑袋:对象头加 SDS 头加内容拼起来恰好装满分配器的一格 64 字节块——阈值是按内存分配器的尺寸分级量身裁的。这也解释了 embstr 为什么只读:内容与头挤在一块内存里,追加就得整体搬家,那不如直接升级成 raw 一劳永逸。

预分配的账,用数字算一遍

小于 1MB 翻倍、大于 1MB 只加一兆,规则一句话就完,但推演一遍才有体感:一个从零开始、每次追加一个字节的键,第一次追加触发分配,之后连续十几次追加都不再分配——alloc 与 len 的差值就是"还能白写几次"的余量;余量耗尽时一次分配直接翻倍。均摊下来,每字节的分配成本是常数级,这正是"预分配防抖动"的数学底气。反方向同理:内容缩短时内存不立刻归还,惰性释放赌的就是"过会儿还要写回来"。

副作用要在监控里认得出来:高频追加的键,内存曲线是锯齿状缓涨;大批量写大字符串时,内存涨幅会大于数据本身的体积——多出来的部分是分配余量与对象头,做容量预估时把这个系数算进去,别到容量评审时才发现账没算平。

排错与边界

INCR 报"不是整数"。INCR 只接受能整体解析为整数的值,小数、带引号的数字、科学计数法都会被拒绝。浮点计数用 INCRBYFLOAT,它会把编码转成 raw 存十进制文本。

GETRANGE 大字符串是全量代价。GETRANGE 虽只返回一小段,服务端仍要把整个值读出来再切片,百兆级大键上它照样进慢日志。分段读取的真实需求,该用 Hash 拆字段,而不是拿 String 硬切。

单键 1MB 是工程红线。协议给单值留的理论上限是 512MB,但翻倍分配放大内存峰值、主从复制整块搬运、响应序列化占住主线程,三笔账加起来,社区经验值就是单值 10KB 舒适、100KB 告警、1MB 红线。把这条规则写进发布检查单,比事后处理大键便宜百倍。

MEMORY USAGE 是量化上述边界的尺子:传键名返回总账(含对象头与 SDS 头),配合第 8 章的大键扫描,就是容量治理的常规武器组合。

空间预分配与惰性释放

增长时新分配额有讲究:长度小于 1MB 时翻倍,大于 1MB 时只多给 1MB。缩短时不立刻还内存,只把 len 调小,等将来复用。两个策略方向相反但目标一致:把 malloc 的次数压到最低。代价是 SDS 会比实际内容多占一些内存,Redis 用 jemalloc 的尺寸分级来缓解碎片。

命令层面的验证

redis-cli -p 6390 > SET user:1 "hello" > STRLEN user:1 # 读 len 字段,O(1) (integer) 5 > APPEND user:1 " world" # 预留空间内追加,零分配 (integer) 11 > SET photo "\x89PNG\r\x1a\n..." # 二进制安全,空字符不截断 > SET counter 100 > INCR counter # 字符串当计数器:识别为整数后原子自增 (integer) 101 > SETBIT sign:2026 227 1 # 字符串还能当位数组用,见 3.1

一个 String 键能扮演三种角色:文本、计数器、位数组——因为底层就是一块"带管理头的字节缓冲",语义由命令赋予。这正是数据结构透视的趣味所在。

场景与边界

  • 计数器(INCR/DECR):阅读数、限流配额,单键原子自增,无竞争问题。
  • 分布式锁(SET NX EX):依赖"不存在才写"这一原子写原语。
  • 分片计数:计数热度极高时按取模拆成 N 个计数键轮转 INCR,汇总求和,避免单键热点。

分片计数展开说一遍:某接口每次调用要给全局调用量加一,单键 INCR 到每秒十万次时会成为主线程热点。做法是拆成 64 个分片键,写入方随机或轮转挑一个 INCR,看板要显示总量时用 MGET 拉全部分片求和。写入压力摊到 64 个键上,读取聚合只有一处且频率低——用结构的组合把"单点写热"拆散,这是 String 计数器在极端热度下的标准变式。分片数取二的幂,轮转逻辑最简单,代价只是看板多一次聚合。

⚠️ 常见坑:单值别超过 1MB 级别。大字符串追加时的翻倍分配、网络传输、主从复制的瞬时带宽都会被放大,社区约定单值 10KB 以内为舒适区。

本节要点回顾

  • SDS = 头部三字段加连续缓冲区,len 让取长度变成读一个字段
  • 二进制安全:内容以长度界定,可存任意字节
  • 预分配翻倍、惰性收缩,本质是减少内存分配次数
  • String 是多面手:文本、计数器、位数组共用同一结构
  • 单值体积上限不是协议限制,是工程纪律

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U