Redis 字符串对象的极致优化(造轮子)

 

字符串的三种编码方式

Redis 字符串对象(robj)可以使用三种不同的编码方式存储:

switch(encoding) {
    case REDIS_ENCODING_RAW: return "raw";        // 原始SDS
    case REDIS_ENCODING_INT: return "int";        // 整数
    case REDIS_ENCODING_EMBSTR: return "embstr";  // 嵌入式字符串
    // 其他编码...
}

这三种编码各有特点:

  1. INT 编码:将字符串作为整数值直接存储
  2. EMBSTR 编码:短字符串的优化存储方式
  3. 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字符串编码设计反映了几个关键设计哲学:

  1. 空间优化:通过选择最合适的编码方式,减少内存使用
  2. 速度优化
    • INT编码避免了字符串解析开销
    • EMBSTR编码提高了缓存命中率
    • 共享整数对象减少内存分配和引用计数操作
  3. 读写平衡:为只读和可修改字符串提供各自优化的编码

深入思考

这种设计带来的性能影响值得深思:

  1. 为什么EMBSTR是只读的? 由于EMBSTR将redisObject和SDS分配在一起,如果字符串长度增加需要重新分配整个对象,而非仅重新分配SDS部分

  2. 为什么使用39字节作为界限? 这与jemalloc的内存分配策略相关,为了使EMBSTR编码的对象(包括redisObject结构、SDS头部和字符串内容)恰好适应64字节的内存块

  3. 为何不让所有字符串都使用RAW编码? 短字符串使用EMBSTR可显著提高内存效率和访问速度,特别是在只读场景中