字符串的三种编码方式
Redis 字符串对象(robj)可以使用三种不同的编码方式存储:
switch(encoding) {
case REDIS_ENCODING_RAW: return "raw"; // 原始SDS
case REDIS_ENCODING_INT: return "int"; // 整数
case REDIS_ENCODING_EMBSTR: return "embstr"; // 嵌入式字符串
// 其他编码...
}
这三种编码各有特点:
- INT 编码:将字符串作为整数值直接存储
- EMBSTR 编码:短字符串的优化存储方式
- RAW 编码:常规字符串存储方式,适用于长字符串
编码选择策略
Redis 会根据字符串的内容和长度智能选择最合适的编码:
1. INT 编码(整数优化)
// 尝试编码为整数
if (len <= 21 && string2l(s, len, &value)) {
// 如果是0-9999之间的整数且未设置maxmemory,使用共享对象
if (server.maxmemory == 0 &&
value >= 0 &&
value < REDIS_SHARED_INTEGERS) {
// 返回共享整数对象
return shared.integers[value];
} else {
// 设置为INT编码
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
适用条件:
- 字符串长度 ≤ 21字节(64位整数的最大位数)
- 内容可以解析为有效整数
- 对于0-9999范围内的整数,Redis会使用预先创建的共享对象
2. EMBSTR vs RAW 编码(字符串长度)
#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(char *ptr, size_t len) {
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr, len);
else
return createRawStringObject(ptr, len);
}
选择标准:
- 长度 ≤ 39字节:使用EMBSTR编码
- 长度 > 39字节:使用RAW编码
这个39字节的界限是为了适应jemalloc内存分配器的64字节大小类别,确保redisObject和SDS头部加上字符内容能够恰好放入一个内存块中。
EMBSTR与RAW的内存布局对比
EMBSTR内存布局
+-------------------------+
| redisObject (16字节) | 一次分配的
+-------------------------+ 连续内存块
| sdshdr8 (3字节) |
+-------------------------+
| 实际字符数据 + 结束符 |
+-------------------------+
RAW内存布局
+-------------------------+
| redisObject (16字节) | 第一次分配
+-------------------------+
|
| ptr指针
v
+-------------------------+
| sdshdr (3+字节) | 第二次分配
+-------------------------+
| 实际字符数据 + 结束符 |
+-------------------------+
内存分配实现
// EMBSTR: 一次分配
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
void *p = zmalloc(sizeof(robj) + sizeof(sdshdr8) + len + 1);
// 一次性分配所有需要的内存
return o;
}
// RAW: 两次分配
robj *createRawStringObject(const char *ptr, size_t len) {
robj *o = zmalloc(sizeof(robj)); // 第一次分配
o->ptr = sdsnewlen(ptr, len); // 第二次分配
return o;
}
字符串修改的编码转换
关键细节:EMBSTR是只读的。当需要修改EMBSTR编码的字符串时,Redis会先将其转换为RAW编码:
// 当对EMBSTR字符串进行修改操作时
void catStringObject(robj *a, robj *b) {
// 如果是EMBSTR编码,先转换为RAW
if (a->encoding == REDIS_ENCODING_EMBSTR) {
a->ptr = sdsnewlen(a->ptr, sdslen(a->ptr));
a->encoding = REDIS_ENCODING_RAW;
}
// 然后进行修改操作
a->ptr = sdscatsds(a->ptr, b->ptr);
}
这种设计使得短字符串在只读场景下获得最佳性能,同时保持修改操作的灵活性。
各编码方式的优缺点对比
特性 | INT | EMBSTR | RAW |
---|---|---|---|
内存效率 | 最高 | 高 | 一般 |
缓存友好度 | 最佳 | 很好 | 一般 |
适用场景 | 整数值 | 短字符串,只读 | 长字符串,可修改 |
内存分配次数 | 0次 | 1次 | 2次 |
内存碎片 | 无 | 极少 | 可能存在 |
修改操作 | 需转换为RAW | 需转换为RAW | 直接修改 |
字符串长度限制 | ≤21字节 | ≤39字节 | ≤512MB |
字符串大小限制
Redis对字符串大小有严格限制,最大不能超过512MB:
static int checkStringLength(redisClient *c, long long size) {
if (size > 512*1024*1024) {
addReplyError(c,"string exceeds maximum allowed size (512MB)");
return REDIS_ERR;
}
return REDIS_OK;
}
设计哲学与性能影响
Redis字符串编码设计反映了几个关键设计哲学:
- 空间优化:通过选择最合适的编码方式,减少内存使用
- 速度优化:
- INT编码避免了字符串解析开销
- EMBSTR编码提高了缓存命中率
- 共享整数对象减少内存分配和引用计数操作
- 读写平衡:为只读和可修改字符串提供各自优化的编码
深入思考
这种设计带来的性能影响值得深思:
-
为什么EMBSTR是只读的? 由于EMBSTR将redisObject和SDS分配在一起,如果字符串长度增加需要重新分配整个对象,而非仅重新分配SDS部分
-
为什么使用39字节作为界限? 这与jemalloc的内存分配策略相关,为了使EMBSTR编码的对象(包括redisObject结构、SDS头部和字符串内容)恰好适应64字节的内存块
-
为何不让所有字符串都使用RAW编码? 短字符串使用EMBSTR可显著提高内存效率和访问速度,特别是在只读场景中