Redis设计与实现

本文深入探讨Redis的设计与实现,包括内部数据结构如简单动态字符串(sds)、双端链表、字典和跳跃表,内存映射数据结构如整数集合(intset)和压缩列表,以及Redis数据类型如字符串、哈希表、列表、集合和有序集的实现细节。此外,还介绍了Redis的事务、订阅与发布、Lua脚本和慢查询日志等功能的实现机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


第一部分:内部数据结构

简单动态字符串(simple dynamic string)

在 Redis 中, 一个字符串对象除了可以保存字符串值之外, 还可以保存 long 类型的值

Sds 在 Redis 中的主要作用有以下两个:

  • 实现字符串对象(StringObject);
  • 在 Redis 程序内部用作 char* 类型的替代品;

在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。它并不能高效地支持长度计算和追加(append)这两种操作:

  • 每次计算字符串长度(strlen(s))的复杂度为 θ(N) 。
  • 对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。

考虑到这两个原因, Redis 使用 sds 类型替换了 C 语言的默认字符串表示


在前面的内容中, 我们一直将 sds 作为一种抽象数据结构来说明, 实际上, 它的实现由以下两部分组成:
其中,类型 sds 是 char * 的别名(alias),而结构 sdshdr 则保存了 len 、 free 和 buf 三个属性。

typedef char *sds;


struct sdshdr {
   

    // buf 已占用长度
    int len;

    // buf 剩余可用长度
    int free;

    // 实际保存字符串数据的地方
    char buf[];
};

sds.c/sdsMakeRoomFor 函数描述了 sdshdr 的这种内存预分配优化策略, 以下是这个函数的伪代码版本:

def sdsMakeRoomFor(sdshdr, required_len):

    # 预分配空间足够,无须再进行空间分配
    if (sdshdr.free >= required_len):
        return sdshdr

    # 计算新字符串的总长度
    newlen = sdshdr.len + required_len

    # 如果新字符串的总长度小于 SDS_MAX_PREALLOC
    # 那么为字符串分配 2 倍于所需长度的空间
    # 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
    if newlen < SDS_MAX_PREALLOC:
        newlen *= 2
    else:
        newlen += SDS_MAX_PREALLOC

    # 分配内存
    newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)

    # 更新 free 属性
    newsh.free = newlen - sdshdr.len

    # 返回
    return newsh

伪代码解释:如果剩余空间足够,则不进行内存重新分配。如果不够则会计算当前字符串的长度=原字符串长度+新增的字符串长度。
如果计算的当前字符串的长度没有超过一个固定值SDS_MAX_PREALLOC,就分配给他两倍自身长度的空间,如果超过了就分配一个固定值长度的空间。
这个固定值SDS_MAX_PREALLOC大小为1MB,是能够分配空间的最大值。
应用举例:

redis> SET msg "hello world"
OK

redis> APPEND msg " again"
(integer) 17

redis> GET msg
"hello world again"

当SET这个msg时,会保存到一个sdshdr中,记录长度、剩余空间、数据,值得注意的是,SET命令不会分配剩余空间,只有对字符串进行追加才时才会分配剩余空间。

struct sdshdr {
   
    len = 11;
    free = 0;
    buf = "hello world\0";
}

当执行 APPEND 命令时,相应的 sdshdr 被更新,字符串 " again" 会被追加到原来的 “hello world” 之后:它将原本长度为11的字符串加上更新的字符串长度作为新字符串的长度,并且赋给此长度的剩余空间。如果再次执行APPEND,添加的字符串长度小于剩余空间时,不会再进行内存的分配。

struct sdshdr {
   
    len = 17;
    free = 17;
    buf = "hello world again\0";// 空白的地方为预分配空间,共 17 + 17 + 1 个字节
}
这种分配策略会浪费内存吗?
执行过 APPEND 命令的字符串会带有额外的预分配空间, 这些预分配空间不会被释放, 除非该字符串所对应的键被删除, 或者等到关闭 Redis 之后, 再次启动时重新载入的字符串对象将不会有预分配空间。

因为执行 APPEND 命令的字符串键数量通常并不多, 占用内存的体积通常也不大, 所以这一般并不算什么问题。

另一方面, 如果执行 APPEND 操作的键很多, 而字符串的体积又很大的话, 那可能就需要修改 Redis 服务器, 让它定时释放一些字符串键的预分配空间, 从而更有效地使用内存。

双端链表

Redis 列表使用两种数据结构作为底层实现:

  • 双端链表
  • 压缩列表

因为双端链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表作为底层实现, 并且在有需要的时候, 才从压缩列表实现转换到双端链表实现。

双端链表的实现由 listNode 和 list 两个数据结构构成.

typedef struct listNode {
   

    // 前驱节点
    struct listNode *prev;

    // 后继节点
    struct listNode *next;

    // 值
    void *value;

} listNode;
typedef struct list {
   

    // 表头指针
    listNode *head;

    // 表尾指针
    listNode *tail;

    // 节点数量
    unsigned long len;

    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值