Redis 源码分析简单字符串 (sds)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: SDS 是简单动态字符串(Simple Dynamic String)的缩写,也是 Redis 设计实现的基础。简单说明:本文主要是基于redis 源码的 unstable 分支说明,后续 Redis 设计相关文章不再特别说明。

数据结构



redis 为了节省内存,针对不同的长度数据采用不同的数据结构。


sds.h 中定义了如下共五种,通常 SDS_TYPE_5 并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放。


#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4


以 type = 1 为例


typedef char *sds;
/*
__attribute__ ((__packed__)) 的所用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用
 */
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 数据长度 */
    uint8_t alloc; /* 去掉头和 null 结束符,有效长度+数据长度 */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    // 变长数据
    char buf[];
};


展示一个数据例子:


image.png


  • free 属性的值为 0 表示这个 sds 没有分配任何未使用空间。


  • len 属性的值为 5 , 表示这个 sds 保存了一个 5 个长度的字符串。


  • buf 属性是一个 char 类型的数组,数组保存了前面5个字节的字符串 'R'、'e'、'd' 、'i'、's' 。最后一个字符则保存了空字符 '\0'。


对于数据压缩的演示:


image.png


空间拓容



  • 当前有效长度 >= 新增长度,直接返回;


  • 更新之后,判断新旧类型是否一致;


  • 一致使用 remailc, 否则使用 malloc + free
  • a. 当前有效长度 >= 新增长度,直接返回
  • 增加步长


  • 新增长度小于预分配长度(1024 * 1204),扩大一倍
  • 新增长度大于等于预分配的长度,每次加预分配长度(减少不必要的内存)


文件位置: sds.c/_sdsMakeRoomFor


#define SDS_MAX_PREALLOC (1024*1024)
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;
    /* Return ASAP if there is enough space left. */
    //当前有效长度 >= 新增长度,直接返回
    if (avail >= addlen) return s;
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    if (greedy == 1) {
        //新增后长度小于预分配长度(1024 * 1024,扩大一倍: SDS_MAX_PREALLOC = 1024 * 1024)
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
       // 新增后长度大约等于预分配的长度,每次增加预分配长度
        else
            newlen += SDS_MAX_PREALLOC;
    }
    type = sdsReqType(newlen);
    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    // 新老类型一致使用 remalloc, 否则使用 malloc + freea , 当前有效长度 >= 新增长度,直接返回
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        // 不一致则需要重新分配内存
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}


空间缩容



在 trim 操作时,采用的是惰性释放即:不会立即使用内存重新分配来回收缩短的字节,只是移动和标记,并修改数据。


文件位置 sds.c/sdstrim


sds sdstrim(sds s, const char *cset) {
    char *end, *sp, *ep;
    size_t len;
    sp = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (ep-sp)+1;
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}


真正的删除操作是在后续操作中见 tryObjectEncoding


文件位置 server.c/tryObjectEncoding


void trimStringObjectIfNeeded(robj *o) {
    if (o->encoding == OBJ_ENCODING_RAW &&
        sdsavail(o->ptr) > sdslen(o->ptr)/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
}


优点


  • 常量获取字符串长度(len)


  • 避免缓冲区溢出


  • 减少字符串修改带来的内存频繁重分配次数


  • 二进制操作安全:可以保持文本数据,也可以保持任意格式的二机制数据(如视频流等)


  • 以 '\0' 结尾,使其兼容部分 C 字符串函数


其他


  • sds 是 char* 的别名,可以理解为分配的是一块连续的内存(表头 + 数据),根据局部性原理可以提高访问速度。


  • 数据存储不使用 SDS_TYPE_5, 因为该类型每次新数据时,都需要进行拓充。


  • 利用 C 语言内存布局,在 SDS 结构体中使用了一个 0 长度的数组,既可以达到变长,又能保证内存也是连续的,因此在 SDS 一系列操作中,看到使用 s[-1] 这样的操作搞的很惊讶,当然这里 s 指向的是 buf 位置。


文件位置:sds.c/sdsalloc


/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->alloc;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->alloc;
    }
    return 0;
}


参考资料



  • 《Redis 设计与实现》 黄健宏



相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
存储 缓存 NoSQL
redis数据结构-字符串
redis数据结构-字符串
35 1
|
10天前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
25 4
|
2天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
2月前
|
存储 缓存 NoSQL
3)深度解密 Redis 的字符串
3)深度解密 Redis 的字符串
27 1
|
3月前
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
64 0
|
3月前
|
存储 NoSQL Redis
【Redis 探秘】SDS 简单动态字符串:揭秘 Redis 高效字符串处理的秘密武器!
【8月更文挑战第24天】Redis采用的简单动态字符串(SDS)是一种专为优化内存存储和字符串操作而设计的数据结构。相较于C语言的标准字符串,SDS改进了字符串长度计算、内存重分配及字符串比较等问题。其特性包括预分配冗余空间减少未来的内存重分配、显式存储长度以加快获取速度等。这些改进使Redis能更高效地管理字符串数据。例如,在Redis中,SDS被广泛应用于键值对的存储,显著提升了字符串操作的性能。了解SDS不仅对于深入理解Redis的工作原理至关重要,也是开发者技能树中的重要一环。
52 0
|
26天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
60 1
|
NoSQL Redis
Redis命令——字符串(String)
Redis 字符串数据类型的相关命令用于管理 redis 字符串值,基本语法如下: 语法 redis 127.0.0.1:6379> COMMAND KEY_NAME 实例 redis 127.0.0.1:6379> SET mykey redis OK redis 127.0.0.1:6379> GET mykey "redis" 在以上实例中我们使用了 SET 和 GET 命令,键为 mykey。
839 0
|
NoSQL Redis
redis必杀命令:字符串(String)
题记: Redis 字符串数据类型的相关命令用于管理 redis 字符串值,基本语法如下。 redis 127.0.0.1:6379> COMMAND KEY_NAME 字符串命令 序号 命令及描述 1 SET key value 设置指定 key 的值 2 GET key 获取指定 key 的值。
875 0
|
26天前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
37 2
数据的存储--Redis缓存存储(二)