Redis字符串
Redis字符串
字符串类型的全称是 Simple Dynamic Strings 简称 SDS,中文意思是:简单动态字符串。它是以键值对 key-value 的形式进行存储的,根据 key 来存储和获取 value 值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。
字符串类型的功能
- 字符串存储和操作
- 整数类型和浮点类型的存储和计算
Demo
1. 设置和获取值
SET name "Alice"
GET name
执行结果:
127.0.0.1:6379> SET name "Alice"
OK
127.0.0.1:6379> GET name
"Alice"
2. 设置带过期时间的键
SET session_token "abc123" EX 60
EX 60表示设置 60 秒后过期
执行结果:
127.0.0.1:6379> SET session_token "abc123" EX 60
OK
3. 批量设置/获取键值
MSET key1 "value1" key2 "value2"
MGET key1 key2
执行结果:
127.0.0.1:6379> MSET key1 "value1" key2 "value2"
OK
127.0.0.1:6379> MGET key1 key2
1) "value1"
2) "value2"
4. 判断 key 是否存在
EXISTS name
执行结果:
127.0.0.1:6379> EXISTS name
(integer) 1
5. 字符串追加(Append)
APPEND name " Smith"
GET name
执行结果:
127.0.0.1:6379> APPEND name " Smith"
(integer) 11
127.0.0.1:6379> GET name
"Alice Smith"
6. 获取字符串长度
STRLEN name
执行结果:
127.0.0.1:6379> STRLEN name
(integer) 11
7. 原子递增 / 递减(通常用于计数)
INCR page_view
INCRBY score 10
DECR stock
DECRBY balance 20
- 如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行加整数值的操作
执行结果:
127.0.0.1:6379> INCR page_view
(integer) 1
127.0.0.1:6379> INCRBY score 10
(integer) 10
127.0.0.1:6379> DECR stock
(integer) -1
127.0.0.1:6379> DECRBY balance 20
(integer) -20
8. 获取/设置某一部分字符串(偏移量
SET greeting "Hello Redis"
GETRANGE greeting 6 10
SETRANGE greeting 6 "World"
GET greeting
执行结果:
127.0.0.1:6379> SET greeting "Hello Redis"
OK
127.0.0.1:6379> GETRANGE greeting 6 10
"Redis"
127.0.0.1:6379>
127.0.0.1:6379> SETRANGE greeting 6 "World"
(integer) 11
127.0.0.1:6379> GET greeting
"Hello World"
9. 如果 key 不存在时才设置(防止覆盖)
SETNX lock:user:123 1
执行结果:
127.0.0.1:6379> SETNX lock:user:123 1
(integer) 1
10. 设置并返回旧值(常用于更新)
GETSET name "Bob"
执行结果:
127.0.0.1:6379> get name
"Alice Smith"
127.0.0.1:6379> GETSET name "Bob"
"Alice Smith"
127.0.0.1:6379> get name
"Bob"
为什么不复用C语言的Char *?
这是一个非常经典的问题!Redis 没有直接复用 C 语言的 char*,而是自己实现了一套叫做 SDS(Simple Dynamic String) 的字符串结构,主要是因为:
- 原生 char* 太“原始”,无法满足 Redis 对性能、安全性、功能性的需求。
C 语言 char* 的问题
- 无法保存长度
-
char* 本质上就是一个指向字符数组的指针,长度必须靠 strlen 逐字节遍历 \0 才能知道,时间复杂度 O(n)。
-
Redis 读写字符串频繁,不能每次都扫描一遍,太慢了。
- 容易越界和缓冲区溢出
-
char* 没有内存管理能力,你不知道分配了多少空间,append 时极容易写过界,造成严重的安全漏洞(buffer overflow)。
-
Redis 是对外暴露接口的服务器,必须保证健壮和安全。
- 不支持二进制安全
-
原生 C 字符串要求以 \0 结尾,不能含有 \0 字节(否则 strlen 失效),所以不能存储 任意二进制数据。
-
Redis 的 string 类型是支持二进制安全的,比如可以存储图片、压缩包、哈希值等。
Redis 的 SDS 解决方案
Redis 设计了自己的 SDS
| 特性 | 说明 |
|---|---|
| 长度 O(1) 获取 | 结构体中带 len 字段 |
| 动态扩容 | 带 alloc 字段,支持 append 自动扩容 |
| 空间预留 | 扩容时预留一定空间,避免频繁 realloc |
| 二进制安全 | 不依赖 \0 结尾,字符串中可以有任何字节 |
| 多种 header 类型 | sdshdr5/8/16/32/64 根据实际长度自动选择结构,节省内存 |
兼容 char* |
buf[] 字段就是普通 char 数组,使用上兼容 char* |
源码
1. 数据结构
Redis 并不是直接使用 C 语言的 char* 字符串,而是封装了一种更安全高效的结构 —— SDS,具备以下优点:
- 支持二进制安全(可存储任意字节)
- 常数时间获取长度 O(1)
- 减少 realloc 的频率(空间预分配)
- 避免缓冲区溢出问题
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
结构体sdshdr8详解
| 字段 | 类型 | 大小 | 含义 |
|---|---|---|---|
len |
uint8_t |
1 字节 | 当前已使用的字符串长度(不包含结尾 '\0') |
alloc |
uint8_t |
1 字节 | 分配的 buf 长度(不包含 header 和 '\0') |
flags |
unsigned char |
1 字节 | 低 3 位存储 SDS 类型,比如 SDS_TYPE_8 |
buf[] |
char 数组 |
n 字节 | 真正的字符串内容 + \0 |
- 总共固定部分是 3 字节,字符串数据和 \0 跟在后面。因为使用了
__attribute__((__packed__)),结构体不会有对齐填充。
举个例子:
假设我们保存字符串 "hello",长度为 5。
struct sdshdr8 {
uint8_t len = 5;
uint8_t alloc = 10; // 提前预分配了 10 字节空间
unsigned char flags = 0x01; // SDS_TYPE_8
char buf[11] = "hello\0...";
};
内存布局如下(总共 3 + alloc + 1 字节): 结构体大小是 3 字节 header + alloc 字节的 buf+1 字节 '\0'
| len=5 | alloc=10 | flags=0x01 | 'h' | 'e' | 'l' | 'l' | 'o' | \0 | ...剩余空间 |
1B 1B 1B ← buf[0] 开始
flags 字段的编码
flags = (type & 0x7) | (unused << 3);
- 低 3 位:表示 SDS 类型(例如:SDS_TYPE_8 = 1)
- 高 5 位:暂时未用(为以后扩展保留)
Redis 会根据字符串长度选择不同的结构(sdshdr8, sdshdr16, sdshdr32, sdshdr64)
static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
case SDS_TYPE_8:
return sizeof(struct sdshdr8);
case SDS_TYPE_16:
return sizeof(struct sdshdr16);
case SDS_TYPE_32:
return sizeof(struct sdshdr32);
case SDS_TYPE_64:
return sizeof(struct sdshdr64);
}
return 0;
}
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}
static inline size_t sdsTypeMaxSize(char type) {
if (type == SDS_TYPE_5)
return (1<<5) - 1;
if (type == SDS_TYPE_8)
return (1<<8) - 1;
if (type == SDS_TYPE_16)
return (1<<16) - 1;
#if (LONG_MAX == LLONG_MAX)
if (type == SDS_TYPE_32)
return (1ll<<32) - 1;
#endif
return -1; /* this is equivalent to the max SDS_TYPE_64 or SDS_TYPE_32 */
}
| Header Type | Max String Size | Header Size | When Used |
|---|---|---|---|
| sdshdr5 | 31 bytes | 1 byte | Tiny strings (special case) |
| sdshdr8 | 255 bytes | 3 bytes | Small strings |
| sdshdr16 | 64KB - 1 byte | 5 bytes | Medium strings |
| sdshdr32 | 4GB - 1 byte | 9 bytes | Large strings |
| sdshdr64 | Unlimited | 17 bytes | Enormous strings |
为什么长度为 0 时不使用 sdshdr5
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
size_t usable;
....
因为sdshdr5 的设计对“字符串追加”非常不友好。
由于它没有 alloc 字段(也没有 len 字段!),每次修改都必须:
-
解包长度(从 flags 高位提取)
-
重新分配新的 SDS(新建一个更长的)
-
拷贝旧数据,写入新数据
而如果你用 sdshdr8,就有: -
len 和 alloc 字段
-
可以直接就地 append,减少拷贝,性能更高
所以当 initlen == 0 时,Redis 推测你之后大概率是要 append 的,干脆直接创建 sdshdr8,提升未来写性能。
那为什么还保留 sdshdr5?
因为它 在存储短字符串(1~31 字节)时是最省空间的!
-
sdshdr5 结构只占 1 字节 header + n 字节数据 + 1 字节 \0
-
比 sdshdr8 少了 2 个字节的内存浪费
例如,存 "OK"(2 字节字符串)时:
-
sdshdr5:只占 4 字节(flags + "OK" + \0)
-
sdshdr8:占 6 字节(3 header + 2 数据 + \0)
2. 字符串对象编码
可以使用 object encoding key 命令来查看对象(键值对)存储的对象编码
1. int
127.0.0.1:6379> set key 666
OK
127.0.0.1:6379> object encoding key
"int"
2. embstr 类型
127.0.0.1:6379> set key abc
OK
127.0.0.1:6379> object encoding key
"embstr"
3.raw 类型
127.0.0.1:6379> set key abcdefghigklmnopqrstyvwxyzabcdefghigklmnopqrs
OK
127.0.0.1:6379> object encoding key
"raw"
- 整数类型对应的就是 int 类型
- 字符串则对应是 embstr 类型
- 字符串长度大于 44 字节时,会变为 raw 类型存储
raw 和 embstr的区别
| 特性 | embstr 编码 | raw 编码 |
|---|---|---|
| 结构 | redisObject + SDS 一次性分配 |
redisObject 和 SDS 分开分配 |
| 内存分配 | 一次 malloc,更快,缓存友好 |
两次 malloc |
| 最大长度 | 最多 44 字节的字符串(含 SDS header) | 任意长度 |
| 优化目的 | 高效处理小字符串 | 支持长字符串 |
为什么是44个字节?
- 因为 Redis 的数据类型有很多,而且,不同数据类型都有些相同的元数据要记录(比如最后一次访问的时间、被引用的次数等),所以,Redis 会用一个 RedisObject 结构体来统一记录这些元数据,同时指向实际数据。
我们来估算一下大小限制:
- redisObject 结构体(通常约 16 字节)
typedef struct redisObject {
unsigned type:4; // 4 bit
unsigned encoding:4; // 4 bit
unsigned lru:24; // 3 个字节
int refcount; // 4 个字节
void *ptr;// 8 个字节
} robj;
它的参数说明如下:
- type:对象的数据类型,例如:string、list、hash 等,占用 4 bits 也就是半个字符的大小;
- encoding:对象数据编码,占用 4 bits;
- lru:记录对象的 LRU(Least Recently Used 的缩写,即最近最少使用)信息,内存回收时会用到此属性,占用 24 bits(3 字节);
- refcount:引用计数器,占用 32 bits(4 字节);
- *ptr:对象指针用于指向具体的内容,占用 64 bits(8 字节)。
redisObject 总共占用 0.5 bytes + 0.5 bytes + 3 bytes + 4 bytes + 8 bytes = 16 bytes(字节)。
- sdshdr8(embstr 使用的是 sdshdr8)
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 1 byte
uint8_t alloc; // 1 byte
unsigned char flags; // 1 byte
char buf[];
};
可以看出除了内容数组(buf)之外,其他三个属性分别占用了 1 个字节,最终分隔字符等于 64 字节,减去 redisObject 的 16 个字节,再减去 SDS 自身的 3 个字节,再减去结束符 \0 结束符占用 1 个字节,最终的结果是 44 字节(64-16-3-1=44)

浙公网安备 33010602011771号