当前位置: 首页 > news >正文

【Redis从入门到精通】第19篇:String对象的七十二变——int/embstr/raw编码的切换逻辑

上一篇【第18篇】压缩列表——Redis的内存压缩黑科技
下一篇【第20篇】List对象底层大揭秘——ziplist和linkedlist的切换时机


你每天都在用Redis的SET/GET操作字符串,但你有没有想过:当你执行SET counter 42SET name "hello world"时,Redis内部的处理方式可能完全不同?一个存的是4字节的整数,另一个存的可能是连续内存中的紧凑字符串。Redis对String对象的编码优化,堪称"七十二变"。


一、先认识redisObject

在Redis内部,所有数据类型都不是裸存的,而是被包裹在一个叫redisObject的结构体里:

typedefstructredisObject{unsignedtype:4;// 类型:string/list/hash/set/zsetunsignedencoding:4;// 编码方式unsignedlru:LRU_BITS;// LRU时间戳或LFU数据intrefcount;// 引用计数void*ptr;// 指向实际数据的指针}robj;

五个字段,各司其职:

字段大小作用
type4比特对象类型(OBJ_STRING=0, OBJ_LIST=1, …)
encoding4比特编码方式(对String而言:int/embstr/raw)
lru24比特用于LRU淘汰或LFU统计
refcount4字节引用计数,用于内存回收
ptr8字节指向实际数据的指针

redisObject本身的大小:

type(4bit) + encoding(4bit) + lru(24bit) = 32bit = 4字节 refcount = 4字节 ptr = 8字节 总计 = 4 + 4 + 8 = 16字节

一个redisObject至少占16字节。这就是为什么Redis要在String上做编码优化——如果每个字符串都要16字节的object头,那存一个小数字也太浪费了。


二、String对象的三种编码

Redis的String对象有三种编码方式:

编码encoding值使用条件实际存储
intOBJ_ENCODING_INT值是整数且能用long表示ptr直接存整数值
embstrOBJ_ENCODING_EMBSTR字符串长度≤44字节redisObject和SDS连续存储
rawOBJ_ENCODING_RAW字符串长度>44字节redisObject和SDS分别分配
┌─────────────────────────────────────────────────┐ │ String 编码选择决策 │ │ │ │ 值是整数? │ │ ├─ 是,且在long范围内 → int编码 │ │ └─ 否 │ │ 字符串长度 ≤ 44字节? │ │ ├─ 是 → embstr编码 │ │ └─ 否 → raw编码 │ └─────────────────────────────────────────────────┘

三、int编码:整数的极致优化

当字符串的值是一个整数,并且可以用C语言的long类型表示时(在64位系统上是-263到263-1),Redis会使用int编码。

最巧妙的地方:此时redisObject.ptr字段不再存指针,而是直接把整数值存在ptr里(通过类型转换)。

int编码的内存布局: redisObject (16字节): ┌──────┬──────────┬────────┬──────────────┐ │type=0│encoding=1│ lru │ refcount=1 │ │string│ int │(24bit) │ │ └──────┴──────────┴────────┴──────┬───────┘ │ ▼ ┌──────────────────────────────────────┐ │ ptr = 整数值 │ │ (直接存数值,不是指针!) │ │ 例如:42 │ └──────────────────────────────────────┘ 总内存:16字节(只有一个redisObject,没有额外的SDS)

对比如果用raw编码存同样的整数42:

raw编码存"42": redisObject (16字节): ┌──────┬──────────┬────────┬──────────────┐ │type=0│encoding=0│ lru │ refcount=1 │ │string│ raw │(24bit) │ │ └──────┴──────────┴────────┴──────┬───────┘ │ ptr ▼ SDS (3+3+1字节): ┌─────────┬──────┬──────┬───┐ │ sdshdr8 │ len=2│ alloc│ \0│ │ (1byte) │(1byte)│(1byte)│ │ └─────────┴──────┴──────┴───┘ │ ▼ "42" 总内存:16 + 7 = 23字节

int编码比raw编码节省了7字节(约30%),而且后续的数值运算(INCR/DECR等)可以直接在ptr上操作,不需要字符串和整数的相互转换。

# 验证int编码127.0.0.1:6379>SET counter42OK127.0.0.1:6379>OBJECT ENCODING counter"int"127.0.0.1:6379>INCR counter(integer)43127.0.0.1:6379>OBJECT ENCODING counter"int"# INCR后仍然是int编码# 大整数仍然是int编码127.0.0.1:6379>SET big_num9223372036854775807OK127.0.0.1:6379>OBJECT ENCODING big_num"int"# int64的最大值,仍然可以用long存# 超出long范围127.0.0.1:6379>SET too_big9223372036854775808OK127.0.0.1:6379>OBJECT ENCODING too_big"embstr"# 超出范围,使用embstr编码

四、embstr编码:短字符串的紧凑存储

embstr(Embedded String)是Redis 3.2引入的编码方式,专门用于44字节及以下的短字符串

它的核心思想是:把redisObject和SDS分配在同一块连续内存中,只需要一次内存分配。

embstr编码的内存布局(连续的一块内存): ┌───────────────────────────────────────────────────────────┐ │ redisObject (16字节) │ SDS (header + data) │ ├──────┬──────────┬────────┬────┼─────┬──────┬──────┬──────┤ │type=0│encoding=2│ lru │ref │sdshdr│ len │alloc │ \0 │ │string│ embstr │(24bit) │ cnt │ (3B) │ (1B) │ (1B) │ (1B)│ └──────┴──────────┴────────┴────┼─────┼──────┴──────┼──────┤ │← 16字节 →│←────── 3字节 ────→│←──────── 字符串内容 ───────→│ └───────────────────────────────────────────────────────────┘ ptr 指向 SDS 的起始位置(也就是 sdshdr8 的位置) 总内存:16 + 3 + 1 + 1 + 1 + N = 22 + N 字节(N为字符串长度)

因为redisObject和SDS在同一块内存中:

  • 只需要一次内存分配malloc只调一次)
  • 释放时也只需要一次内存释放free只调一次)
  • 对CPU缓存更友好(连续内存,一次缓存行加载就能读完整个对象)

五、raw编码:长字符串的通用存储

当字符串长度超过44字节时,Redis使用raw编码。此时redisObject和SDS分别独立分配

raw编码的内存布局(两次独立的内存分配): 内存区域1: ┌──────┬──────────┬────────┬──────────────┐ │type=0│encoding=0│ lru │ refcount=1 │ │string│ raw │(24bit) │ │ └──────┴──────────┴────────┴──────┬───────┘ │ ptr(指向另一块内存) │ ▼ (可能完全不连续!) 内存区域2: ┌─────────┬───────┬───────┬───┬───────────────────────┐ │ sdshdr8 │ len │ alloc │ \0│ 字符串内容 │ │ (1byte) │(4byte)│(4byte)│ │ (>44字节) │ └─────────┴───────┴───────┴───┴───────────────────────┘ 总内存:16 + 1 + 4 + 4 + 1 + N = 26 + N 字节 而且有两次独立的内存分配!

注意:raw编码下的SDS header使用了sdshdr8而不是更小的header。sdshdr8lenalloc各占4字节(uint32_t),这比sdshdr8小header版本(各1字节)要大一些。之所以用大header,是因为长字符串的长度和分配量可能超过1字节能表示的255。


六、embstr vs raw 对比

对比维度embstrraw
字符串长度限制≤ 44字节> 44字节
内存分配次数1次2次
内存释放次数1次2次
内存连续性连续(redisObject+SDS一体)不连续(两块独立内存)
是否可修改否(只读)
CPU缓存友好性好(一次缓存行加载)差(可能跨缓存行)
内存碎片有(两块独立分配)
GC效率高(一次释放)低(两次释放)
适用场景短字符串、缓存键名等长文本、JSON文档等
为什么embstr是只读的? embstr设计为只读的原因: 1. 如果修改embstr中的字符串(如APPEND),可能导致SDS需要扩容 2. 扩容意味着重新分配SDS内存,但SDS和redisObject在同一块内存中 3. 要保持连续性就必须把整个redisObject+SDS重新分配——代价太大 4. 所以Redis直接选择:修改时转成raw编码,一劳永逸 embstr的设计目标就是"短、小、快、只读"——它是一个写时复制的优化

七、编码转换触发条件

Redis String的编码转换遵循只升级不降级的原则(和intset一样):

7.1 int → raw

当对一个int编码的字符串执行追加非数字字符的操作时:

127.0.0.1:6379>SET num42OK127.0.0.1:6379>OBJECT ENCODING num"int"127.0.0.1:6379>APPEND num" is the answer"(integer)15127.0.0.1:6379>GET num"42 is the answer"127.0.0.1:6379>OBJECT ENCODING num"raw"# 追加非数字后变为raw(不是embstr!)

注意:int追加字符串后直接变成raw,而不是embstr——即使结果字符串只有15字节(<44)。因为追加操作已经需要修改,而embstr是只读的,所以直接跳到raw。

7.2 embstr → raw

当对embstr编码的字符串执行任何修改操作时:

127.0.0.1:6379>SET msg"hello"OK127.0.0.1:6379>OBJECT ENCODING msg"embstr"# 5字节 < 44127.0.0.1:6379>APPEND msg" world"(integer)11127.0.0.1:6379>OBJECT ENCODING msg"raw"# 修改后变raw127.0.0.1:6379>SET greeting"hi"OK127.0.0.1:6379>OBJECT ENCODING greeting"embstr"127.0.0.1:6379>SETRANGE greeting0"H"(integer)2127.0.0.1:6379>OBJECT ENCODING greeting"raw"# SETRANGE也是修改操作

7.3 不会降级的情况

编码转换方向(不可逆): 创建时 │ ├─ 整数值 → int │ │ │ └─ 追加非数字 → raw ✓ │ │ │ └─ 即使结果 < 44字节,也不会变回embstr! │ ├─ 短字符串(≤44B) → embstr │ │ │ └─ 任何修改 → raw ✓ │ │ │ └─ 即使缩短到 < 44字节,也不会变回embstr! │ └─ 长字符串(>44B) → raw

完整的转换规则表

操作当前编码结果编码条件
SET整数值-int值在long范围内
SET短字符串(≤44B)-embstr长度≤44
SET长字符串(>44B)-raw长度>44
SET(覆盖)int/embstr/raw重新判断根据新值重新选择编码
APPEND非数字intraw追加后不再是纯整数
APPENDembstrrawembstr不支持修改
SETRANGEembstrrawembstr不支持修改
INCR/DECRintint数值运算,保持int
INCR/DECRembstr/raw报错非int编码不能INCR

八、44字节魔数:背后的CPU缓存行对齐

为什么是44字节这个特定的数字?这不是随便定的,而是和**CPU缓存行(Cache Line)**有关。

现代CPU的缓存行大小通常是64字节。当CPU从内存加载数据时,是以64字节为单位加载的。如果你的数据能放在一个缓存行里,访问速度会快得多。

让我们算一下:

一个缓存行 = 64字节 embstr的总内存 = redisObject + SDS header + 字符串内容 + '\0' = 16 + 3 + 1 + 1 + 1 + N + 1 = 23 + N 要让embstr ≤ 64字节: 23 + N ≤ 64 N ≤ 41 但Redis选择了 N ≤ 44,为什么呢?

实际上Redis 3.2最初是使用SDS_HDR(5, s)的header(5字节:type+len+alloc各1字节+预留2字节),后来改为更紧凑的sdshdr8

Redis 4.0+ 的sdshdr8结构: struct __attribute__((packed)) sdshdr8 { uint8_t len; // 1字节(已用长度) uint8_t alloc; // 1字节(已分配长度,不算header和\0) unsigned char flags; // 1字节(SDS类型标记) char buf[]; // 柔性数组 }; // header大小 = 3字节 完整embstr布局: redisObject: 16字节 sdshdr8: 3字节 buf内容: N字节 '\0': 1字节 总计: 20 + N 字节 要让总大小 ≤ 64字节: 20 + N ≤ 64 N ≤ 44 ← 这就是44字节魔数的由来!
44字节正好让embstr对象恰好填满一个64字节的缓存行: ┌─────────────── 64字节缓存行 ──────────────┐ │ redisObject(16B) │SDS hdr(3B)│内容(44B)│\0(1B)│ ├─────────────────┼──────────┼────────┼──────┤ │ │ │ │ │ │ 16字节 │ 3字节 │ 44字节 │ 1字节│ └─────────────────┴──────────┴────────┴──────┘ = 64字节,完美对齐!

踩坑提示:这个44字节是在64位系统、使用sdshdr8的情况下计算出来的。如果你在32位系统上运行(指针只有4字节),redisObject只有12字节,那这个阈值可能会不同。不过现在32位系统已经很少见了。


九、实际演示:三种编码的转换全过程

让我们通过redis-cli完整验证编码转换:

# === int编码 ===127.0.0.1:6379>SET age25OK127.0.0.1:6379>OBJECT ENCODING age"int"127.0.0.1:6379>TYPE age"string"127.0.0.1:6379>INCR age(integer)26127.0.0.1:6379>OBJECT ENCODING age"int"# INCR后还是int# === embstr编码 ===127.0.0.1:6379>SET name"Alice Johnson"OK127.0.0.1:6379>OBJECT ENCODING name"embstr"# 13字节 < 44127.0.0.1:6379>STRLEN name(integer)13# embstr边界测试127.0.0.1:6379>SET short"12345678901234567890123456789012345678901234"OK127.0.0.1:6379>STRLEN short(integer)44127.0.0.1:6379>OBJECT ENCODING short"embstr"# 正好44字节,还是embstr127.0.0.1:6379>SET long"123456789012345678901234567890123456789012345"OK127.0.0.1:6379>STRLEN long(integer)45127.0.0.1:6379>OBJECT ENCODING long"raw"# 45字节 > 44,直接raw# === 编码转换 ===127.0.0.1:6379>SET mutable"hello"OK127.0.0.1:6379>OBJECT ENCODING mutable"embstr"127.0.0.1:6379>APPEND mutable" world"(integer)11127.0.0.1:6379>OBJECT ENCODING mutable"raw"# embstr → raw(不可逆)127.0.0.1:6379>GET mutable"hello world"# === int → raw ===127.0.0.1:6379>SET counter100OK127.0.0.1:6379>OBJECT ENCODING counter"int"127.0.0.1:6379>APPEND counter"ms"(integer)5127.0.0.1:6379>OBJECT ENCODING counter"raw"# int → raw(直接跳过embstr)127.0.0.1:6379>GET counter"100ms"

十、总结

Redis String对象的三种编码是Redis对性能和内存优化的经典案例:

┌───────────────────────────────────────────────────┐ │ String 编码全景图 │ │ │ │ ┌───────┐ 修改(非纯数字) ┌──────────┐ │ │ │ int │ ──────────────────→ │ raw │ │ │ │16字节 │ │ 26+N字节 │ │ │ │整数优化 │ │ 通用存储 │ │ │ └───────┘ └──────────┘ │ │ ↑ SET整数 ↑ 修改/长字符串 │ │ │ │ │ │ 创建时选择 ──→ ┌───────┐ ────→│ │ │ │embstr │ 修改 │ │ │ │22+N字节│ │ │ │ │≤44B │ │ │ │ │只读 │ │ │ │ └───────┘ │ │ │ ↑ SET≤44B │ │ │ │ │ │ │ 创建时选择 ────────┘ │ │ SET>44B ────────────────────────→│ └───────────────────────────────────────────────────┘
编码内存占用最佳场景可修改
int16字节纯整数(计数器、ID等)仅数值运算
embstr22+N字节短字符串(≤44B,缓存键名等)
raw26+N字节长字符串(JSON、HTML等)

这些优化对用户完全透明——你不需要(也无法)手动指定编码方式。Redis会根据数据内容自动选择最优编码。这也是Redis的一个设计哲学:把复杂度留给实现,把简单留给用户。

下一篇,我们把目光从String转向List,看看Redis列表对象在底层经历了怎样的编码演进——从ziplist到quicklist再到listpack,每一步都是为了解决什么问题。


上一篇【第18篇】压缩列表——Redis的内存压缩黑科技
下一篇【第20篇】List对象底层大揭秘——ziplist和linkedlist的切换时机


http://www.jsqmd.com/news/922525/

相关文章:

  • 8款网盘高速下载助手:一键获取真实下载链接告别限速烦恼
  • 从.proto文件到前端调用:手把手教你用Protobuf+TypeScript打造全栈类型安全
  • 别再只用纯色了!用CSS linear-gradient和radial-gradient给你的网站加点‘氛围感’(附5个实战代码片段)
  • VASP计算跑完了,OUTCAR、CONTCAR、DOSCAR...这些输出文件到底怎么看?手把手教你提取关键结果
  • 3分钟搞定百度网盘高速下载:免费直链解析终极方案
  • 2026北京老书古书回收诚信靠谱TOP5排行 避坑必看诚信榜单 - 品牌排行榜单
  • 天猫超市卡回收价格,慢慢打听自有分寸 - 京顺回收
  • 八大网盘直链下载助手终极指南:告别限速,免费获取高速下载链接
  • 告别操作盲区:3分钟掌握Keyviz,让键盘鼠标操作透明化
  • 量子控制中的动态李代数与通用量子计算
  • “人工智能+零售业”面临的主要挑战
  • 抖音批量下载终极指南:5分钟免费下载无水印视频
  • 保姆级教程:用Docker Compose一键部署WVP-PRO+ZLM+录像服务,告别繁琐配置
  • C166开发中的内存区域定位技术解析与应用
  • 5分钟快速解锁VMware macOS虚拟机:Unlocker 3.0终极指南
  • 终极指南:RPFM自动翻译功能文本截断问题深度解析与完美修复方案
  • 用 BAPI_PO_CREATE1 创建带自定义字段的采购订单,一次把 EXTENSIONIN 讲透
  • 如何5分钟搭建专业级在线LaTeX写作环境:WebLaTeX完全指南
  • 5分钟永久备份:GetQzonehistory让你轻松导出QQ空间所有历史说说
  • VinXiangQi:如何用深度学习技术革新传统象棋对弈体验
  • 别再死记硬背了!用Python手把手实现感知器算法,从鸢尾花分类到决策边界可视化
  • 3大实战策略:用OpenCore Legacy Patcher深度解锁老旧Mac的macOS升级潜能
  • 如何用qmcflac2mp3终极解锁QQ音乐加密文件:完整转换指南
  • 从游戏挂机到办公自动化:深入聊聊按键精灵里数字、文本、真假值互相转换的那些门道
  • 原神60帧限制终于被打破!这份完整指南教你如何免费解锁120帧流畅体验
  • 如何3步快速解密网易云音乐NCM文件:免费高效转换工具全攻略
  • 别再被1e-9搞懵了!Python科学计数法实战避坑指南(附数据处理案例)
  • 告别无效日志!手把手教你用CPAL脚本的writeToLog和writeToLogEx函数,打造可读性超强的自动化测试报告
  • Online-disk-direct-link-download-assistant:网盘直链解析技术深度解析与实战指南
  • 5步掌握SMUDebugTool:开源AMD Ryzen硬件性能优化终极指南