Appearance
String类型详解
一、String类型的基础特性
String是Redis中最基础、最常用的数据类型,也是其他复杂类型(如List、Hash)的底层构建块。其核心特性包括:
- 二进制安全:
支持存储任意二进制数据(如文本、图片、序列化对象、甚至\0
字符),因为Redis不依赖字符编码(如UTF-8)来解析内容,仅通过长度判断数据边界。
例如:SET key "\x00hello\x00"
可以正确存储和获取,不会因\0
截断。 - 值大小限制:
单个String值的最大容量为512MB(redis.conf
中的max-string-size
参数可调整,但通常不建议修改)。 - 原子操作:
所有String命令(如SET
、INCR
、APPEND
)都是原子性的,适合分布式场景(如计数器、分布式锁)。 - 过期时间:
支持为String设置过期时间(通过EX
/PX
参数或EXPIRE
命令),过期后会被Redis自动删除(遵循惰性删除+定期删除策略)。
二、底层实现:SDS(Simple Dynamic String)
Redis没有使用C语言的原生字符串(以\0
结尾的字符数组),而是自定义了SDS(简单动态字符串)结构,解决了C字符串的三大痛点:
- 无法高效获取长度(需遍历到
\0
,O(n)时间); - 修改时容易内存溢出(如
strcat
未提前分配足够空间); - 不支持二进制安全(
\0
会被视为字符串结束)。
1. SDS的结构定义
SDS的结构体(以Redis 6.0为例)如下:
c
typedef struct sdshdr {
int len; // 已使用的字节数(字符串长度)
int free; // 未使用的字节数(剩余空间)
char buf[]; // 字符数组(存储实际数据)
} sdshdr;
len
:记录字符串的实际长度,获取长度的时间复杂度为O(1)(对比C字符串的O(n))。free
:记录buf
中未使用的空间,用于减少内存分配次数(见下文“空间预分配”)。buf
:存储字符串的字节数组,末尾会自动添加\0
(兼容C语言字符串函数,如printf
),但不依赖\0
判断长度(由len
决定)。
2. SDS vs C字符串的核心优势
特性 | C字符串 | SDS |
---|---|---|
长度获取 | O(n)(遍历到\0 ) | O(1)(直接读len ) |
内存溢出 | 可能(如strcat 未扩容) | 自动扩容(无溢出风险) |
二进制安全 | 不支持(\0 截断) | 支持(len 判断边界) |
修改效率 | 低(频繁分配/回收内存) | 高(空间预分配+惰性释放) |
三、String类型的编码策略
Redis为String类型设计了三种编码方式,根据值的类型和长度自动切换,以优化内存占用和访问效率:
编码类型 | 适用场景 | 内存结构 | 转换触发条件 |
---|---|---|---|
int | 整数值(范围:-2^63 ~ 2^63-1 ) | 直接存储为long long 类型(8字节) | 值超过long long 范围,或添加非数字字符(如APPEND "a" ) |
embstr | 短字符串(默认≤44字节) | SDS结构与buf 连续存储(分配1次内存) | 字符串变长超过44字节,或执行修改操作(如APPEND 导致扩容) |
raw | 长字符串(>44字节)或非整数 | SDS结构与buf 分开存储(分配2次内存) | 无(默认长字符串或非整数) |
1. int
编码
- 场景:存储整数(如计数器、序号)。
- 优化:直接用
long long
类型存储,无需SDS结构的额外开销(len
+free
共8字节),内存占用最小(仅8字节)。 - 示例:
SET counter 100
→ 用int
编码;INCR counter
→ 仍为int
;SET counter "100a"
→ 转raw
编码。
2. embstr
编码
- 场景:存储短字符串(如缓存用户昵称、配置项)。
- 优化:将SDS的
sdshdr
结构与buf
数组连续分配在同一块内存(仅需1次malloc
),减少内存碎片,且缓存命中率更高(连续内存更易被CPU缓存)。 - 限制:
embstr
是只读的,修改时会自动转换为raw
编码(因为修改需要重新分配内存,无法保持连续性)。 - 示例:
SET name "Alice"
(6字节)→ 用embstr
编码;APPEND name "Smith"
(变为11字节,仍≤44)→ 自动转换为raw
编码;APPEND name "..."
(超过44字节)→ 转raw
编码。
3. raw
编码
- 场景:存储长字符串(如缓存文章内容、二进制文件)或非整数(如JSON字符串)。
- 结构:
sdshdr
结构与buf
数组分开分配(需2次malloc
),适合频繁修改的长字符串(修改时仅需调整buf
的空间,无需移动sdshdr
)。 - 示例:
SET article "<html>...</html>"
(10KB)→ 用raw
编码;APPEND article "<footer>...</footer>"
→ 自动扩容(见下文“空间预分配”)。
四、String类型的关键机制
1. 空间预分配(减少内存分配次数)
当执行APPEND
、SETRANGE
等修改操作时,SDS会根据当前长度自动分配额外空间,避免频繁调用malloc
:
- 若修改后
len
< 1MB:free
分配与len
相等的空间(总容量=len
× 2)。
例如:len=10
→ 修改后len=20
→free=20
→ 总容量=40。 - 若修改后
len
≥ 1MB:free
分配1MB的空间(总容量=len
+ 1MB)。
例如:len=2MB
→ 修改后len=3MB
→free=1MB
→ 总容量=4MB。
2. 惰性空间释放(避免频繁回收内存)
当执行TRIM
、SET
等缩短字符串的操作时,SDS不会立即释放多余的空间,而是将其保留在free
中,供后续修改使用:
- 例如:
SET key "hello world"
(len=11
,free=0
)→ 执行TRIM key 0 4
(保留前5字符“hello”)→len=5
,free=6
(未释放多余的6字节)。 - 优势:避免频繁调用
free
(昂贵的系统调用),提升修改效率;若后续需要扩展字符串,可直接使用free
空间,无需重新分配。
3. 二进制安全的保证
SDS通过len
字段判断字符串的结束,而非\0
,因此可以存储任意二进制数据:
- 例如:
SET key "\x00\x01\x02"
→len=3
,buf
中存储0x00
、0x01
、0x02
,获取时会正确返回3字节的数据(对比C字符串会截断为0字节)。
五、String类型的应用场景
String类型的灵活性使其适用于多种场景,以下是常见案例:
缓存:
存储高频访问的热点数据(如用户信息、商品详情),减少数据库压力。
示例:SET user:1001 "{\"name\":\"Alice\",\"age\":25}" EX 3600
(缓存1小时)。计数器:
利用INCR
/DECR
原子操作实现计数器(如文章阅读量、点赞数、接口调用次数)。
示例:INCR article:1001:views
(阅读量+1)。分布式锁:
利用SETNX
(SET if Not eXists)命令实现分布式锁(仅当键不存在时设置,保证原子性)。
示例:SET lock:order:1001 "token" EX 10 NX
(获取锁,过期时间10秒)。存储二进制数据:
存储图片、音频、视频等二进制文件(需注意大小限制,建议≤10MB,避免占用过多内存)。
示例:SET image:1001 "\x89PNG\r\n\x1a\n..."
(存储PNG图片的二进制内容)。配置项存储:
存储系统配置(如开关、阈值),支持快速读取和修改。
示例:SET config:max_login_attempts 5
(最大登录尝试次数)。
六、总结
Redis的String类型是灵活、高效、二进制安全的基础数据类型,其底层通过SDS解决了C字符串的缺陷,通过三种编码策略优化了内存占用和访问效率,通过空间预分配和惰性释放提升了修改性能。
关键结论:
- 短字符串(≤44字节)用
embstr
编码,内存连续,效率高; - 长字符串(>44字节)用
raw
编码,适合频繁修改; - 整数用
int
编码,内存占用最小; - 二进制安全使其能存储任意数据,是缓存、计数器、分布式锁等场景的首选。