Redis数据结构—String

161 阅读6分钟

Redis数据结构—String

一、简单介绍

Redis 的 String 数据结构底层实现是 SDS(Simple Dynamic String),它具有以下特点:

  • 获取字符串长度的复杂度为 O(1):SDS 中使用 len 属性记录了字符串的长度,因此获取字符串长度的时间复杂度为 O(1),而 C 语言中需要遍历字符串来获取长度,时间复杂度为 O(n)。

  • 杜绝缓冲区溢出:SDS 在进行字符串操作时,会自动检查空间是否足够,如果不够会自动扩展空间,从而避免缓冲区溢出的问题。

  • 减少内存重分配次数:SDS 通过预分配和惰性空间释放的策略,减少了字符串修改时的内存重分配次数。

  • 二进制安全:SDS 可以存储任意二进制数据,而不仅仅是文本数据,因为它不依赖于空字符来判断字符串的结束。

  • 兼容部分 C 字符串函数:SDS 的 buf 数组以空字符结尾,因此可以兼容部分 C 字符串函数。 SDS 的定义如下:

    struct sdshdr { 
        int len; // 记录 buf 数组中已使用字节的数量,等于 SDS 所保存字符串的长度 
        int free; // 记录 buf 数组中未使用字节的数量 
        char buf[]; // 字节数组,用于保存字符串 
    };
    

二、应用场景

Redis 的字符串结构非常灵活,可以用于多种应用场景:

  1. 缓存
    • 最常见的用途之一是作为缓存系统,存储例如网页、图片或是其他计算密集型操作的结果,以减少数据库的访问次数和降低延迟。
  2. 计数器
    • 利用 Redis 的原子操作,字符串可以用作计数器,例如网站的访问次数、商品的浏览量等。
  3. 会话存储
    • 在 Web 应用中,可以使用 Redis 存储用户会话信息。由于 Redis 是内存数据库,读写速度快,非常适合此类用途。
  4. 分布式锁
    • 通过设置带有过期时间的字符串键,可以实现分布式锁的功能,用于多个进程或服务器之间的资源同步。
  5. 配置管理
    • 使用字符串存储应用配置信息,可以快速读取和更新配置,而不需要重启应用。
  6. 全局唯一ID生成器
    • 利用 Redis 的原子性操作,可以生成全局唯一的ID,如订单号、用户ID等

三、实现原理

Redis 字符串对象可以使用三种不同的内部编码方式:intembstrraw。这些编码方式是优化内存使用和提高性能的手段。

  1. int编码

    • 当字符串对象可以被解析为一个整数值时,Redis 会选择使用 int 编码。
    • 这种编码方式将字符串直接存储为整数值,而不是字符数组,从而节省内存。
    • 适用于表示数字的小字符串,因为它们可以直接以整数形式存储在内存中。

    如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int

  2. embstr编码

    • 用于存储较小的字符串(长度小于等于 44 个字符,这个值可能根据 Redis 版本和平台有所不同)。
    • embstr 编码将 Redis 对象和实际的字符串数据存储在单个连续的内存块中。
    • 这种编码方式减少了内存分配的次数(只需要一次内存分配),并且因为数据局部性提高了缓存效率。
    • embstr 编码的字符串是只读的,如果需要修改,必须先转换为 raw 编码。
  3. raw编码

    • 用于存储较大的字符串或需要修改的字符串。

    • raw 编码使用两次内存分配:一次用于 Redis 对象本身,另一次用于实际的字符串数据。

    • 这种编码方式允许字符串被修改,因为字符串数据是独立于对象结构的。

Redis 在运行时根据字符串的内容和大小动态选择最合适的编码方式。例如,当一个字符串对象被创建时,如果它可以被解析为一个整数,Redis 将使用 int 编码。如果字符串较小,它将使用 embstr 编码以节省内存和提高效率。对于较大或需要修改的字符串,将使用 raw 编码。这种灵活的编码策略是 Redis 高效内存使用的关键之一。

flowchart TD
    A[开始] --> B{它是字符串对象吗?}
    B -->|否| B1[返回原对象]
    B -->|是| C{它是RAW或EMBSTR编码吗?}
    C -->|否| C1[返回原对象]
    C -->|是| D{引用计数 > 1吗?}
    D -->|是| D1[返回原对象]
    D -->|否| E{能表示为长整型吗?}
    E -->|否| F{长度 <= EMBSTR限制吗?}
    E -->|是| G{最大内存为0或允许共享整数吗?}
    G -->|否| H{编码是RAW吗?}
    H -->|是| I[转换为INT编码,释放RAW,赋值]
    H -->|否| J{编码是EMBSTR吗?}
    J -->|是| K[从值创建新的INT编码对象]
    J -->|否| J1[返回原对象]
    G -->|是| L{值在共享整数范围内吗?}
    L -->|否| H
    L -->|是| M[使用共享整数对象]
    F -->|否| N{尝试修剪吗?}
    F -->|是| O{编码是EMBSTR吗?}
    O -->|是| O1[返回原对象]
    O -->|否| P[创建EMBSTR编码对象]
    N -->|是| Q[如果需要,修剪字符串对象]
    N -->|否| R[返回原对象]
    Q --> R
    I --> R
    K --> R
    M --> R
    P --> R
/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncodingEx(robj *o, int try_trim) {
    long value;
    sds s = o->ptr;
    size_t len;

    /* 确保这是一个字符串对象,这是在此函数中编码的唯一类型。
     * 其他类型使用编码的内存高效表示,但由实现类型的命令处理。 */
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);

    /* 我们只尝试对仍然由实际字符数组表示的对象进行一些特殊编码,
     * 也就是说,仍然是RAW或EMBSTR编码的对象。 */
    if (!sdsEncodedObject(o)) return o;

    /* 对共享对象进行编码是不安全的:共享对象可以在Redis的“对象空间”中被共享,
     * 并可能最终处于它们不被处理的地方。只将它们作为键空间中的值来处理。 */
     if (o->refcount > 1) return o;

    /* 检查是否可以将这个字符串表示为一个长整数。
     * 一个大于20个字符的字符串不可能表示为32位或64位整数。 */
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* 这个对象可以编码为长整数。尝试使用共享对象。
         * 注意,当使用maxmemory时,避免使用共享整数,
         * 因为每个对象都需要有一个私有的LRU字段,以便LRU算法能够很好地工作。 */
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }

    /* 如果字符串很小且仍然是RAW编码,
     * 尝试EMBSTR编码,这更高效。
     * 在这种表示中,对象和SDS字符串被分配在同一块内存中,以节省空间和缓存未命中。 */
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;

        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }

    /*不能编码对象...
     * 做最后的尝试,至少优化内部的SDS字符串 */
    if (try_trim)
        trimStringObjectIfNeeded(o, 0);

    /* 返回原始对象。 */
    return o;
}