Redis 字符串:SDS

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis 字符串:SDS

总结 Redis 封装 C 字符串为 SDS 的实现。


SDS 结构

结构定义

SDS 全称 Simple Dynamic String(简单动态字符串),是 Redis 对 C 原生字符串的封装,结构定义如下:

// sds 是 char * 的类型别名,用于指向 sdshdr 头部的 buf 字符串
typedef char *sds;
// Redis 保存字符串对象的结构
struct sdshdr {
  int len;    // buf 已占用空间长度
  int free;   // buf 剩余可用空间长度
  char buf[]; // sds 二进制字节数组,C99 支持将 struct 最后一个成员定义为无长度数组,不自动分配内存
};
  • len:有符号 int 类型,占 4 字节,最大能表示 2^31 B = 2^21 KB = 2^11 MB = 2 GB 大的数据。但 Redis 把单 key 字符串值最长限制在了 512MB


  • buf:声明时无长度的柔性数组,是 C99 标准中的不完整类型,虽然结构体中的各字段在内存上是连续的,但柔性数组空间并不计入结构体总内存:
printf("%zu\n", sizeof(struct sdshdr)); // 8 


内存布局

假设存入字符串 “Redis”,其内存布局如下:

64 位 Linux 上各内存段长度验证:

int main() {
  char *sds = sdsnew("Redis");
  char *free = sds - sizeof(int);
  char *len = free - sizeof(int);
  char *prefixSize = len - sizeof(size_t);
  printf("used_memory: %zu\n", zmalloc_used_memory());
  printf("prefix_size: %d\nlen: %d\nfree: %d\n", *prefixSize, *len, *free);
}


SDS API 实现

源码:sds.c,几个重要的 API 实现:


sdslen

static inline size_t sdslen(const sds s) {
  struct sdshdr *sh = (void *) (s - (sizeof(struct sdshdr))); // sds - 8
  return sh->len;
}


sdsnew

注意 zmalloc 没有为 buf 柔性数组分配内存,其值是用 memcpy 高效复制字符串的值进行初始化的:


// 新建 sds
sds sdsnew(const char *init) {
  size_t initlen = (init == NULL) ? 0 : strlen(init);
  return sdsnewlen(init, initlen);
}
// 根据字符串 init 及其长度 initlen 创建 sds
// 成功则返回 sdshdr 地址,失败返回 NULL
sds sdsnewlen(const void *init, size_t initlen) {
  struct sdshdr *sh;
  if (init) {
    sh = zmalloc(sizeof(struct sdshdr) + initlen + 1); // 有值则不初始化内存,+1 是为 '\0' 预留
  } else {
    sh = zcalloc(sizeof(struct sdshdr) + initlen + 1); // 空字符串则 SDS 初始化为全零
  }
  if (sh == NULL) return NULL;
  sh->len = initlen;
  sh->free = 0; // 新 sds 不预留空闲空间
  if (initlen && init)
    memcpy(sh->buf, init, initlen); // 复制字符串 init 到 buf
  sh->buf[initlen] = '\0'; // 以 \0 结尾
  return (char *) sh->buf; // buf 部分即 sds
}


sdsclear


惰性删除,将 SDS 清空为空字符串,未释放的空间会保留给下次分配使用:

void sdsclear(sds s) {
  struct sdshdr *sh = (void *) (s - (sizeof(struct sdshdr)));
  sh->free += sh->len; // 全部可用
  sh->len = 0;
  sh->buf[0] = '\0'; // 手动截断 buf
}

sdsMakeRoomFor

Redis 的内存预分配策略根据新内存字节数决定:

  • [0, 1 MB):翻倍增长
  • [1, ∞):每次仅增长 1 MB
// 扩展 sds 空间增加 addlen 长度,进行内存预分配
sds sdsMakeRoomFor(sds s, size_t addlen) {
  struct sdshdr *sh, *newsh;
  size_t free = sdsavail(s);
  size_t len, newlen;
  if (free >= addlen) return s; // sdsclear 惰性删除保留的内存够用,无须扩展
  len = sdslen(s);
  sh = (void *) (s - (sizeof(struct sdshdr)));
  newlen = (len + addlen); // 新长度不把 free 算入,和初始化时一样恰好够用就行
  // 空间预分配策略:新长度在 (..., 1MB) 则成倍增长,[1MB, ...) 则每次仅增长 1 MB
  if (newlen < SDS_MAX_PREALLOC)
    newlen *= 2;
  else
    newlen += SDS_MAX_PREALLOC;
  // 重分配
  newsh = zrealloc(sh, sizeof(struct sdshdr) + newlen + 1);
  if (newsh == NULL) return NULL;
  newsh->free = newlen - len; // 更新 free 但不更新 len
  return newsh->buf;
}


sdscat

memcpy 高效地拷贝内存,将字符串连接到 SDS 尾部,其使用 sdsMakeRoomFor 进行空间预分配:

// 将长度为 len 的字符串 t 追加到 sds
sds sdscatlen(sds s, const void *t, size_t len) {
  struct sdshdr *sh;
  size_t curlen = sdslen(s);
  s = sdsMakeRoomFor(s, len);
  if (s == NULL) return NULL;
  sh = (void *) (s - (sizeof(struct sdshdr)));
  memcpy(s + curlen, t, len);  // 复制 t 中的内容到字符串后部
  sh->len = curlen + len;
  sh->free = sh->free - len;
  s[curlen + len] = '\0';
  return s;
}
// 追加字符串到 sds
sds sdscat(sds s, const char *t) {
  return sdscatlen(s, t, strlen(t));
}


注意相近的 sdscpy 函数会将字符串覆盖式地拷贝到 SDS 中。


sdsfree

释放 SDS 整块内存:

void sdsfree(sds s) {
  if (s == NULL) return;
  zfree(s - sizeof(struct sdshdr)); // 同样左移寻址
}


SDS 优点

结合如上的 API 实现,总结下 SDS 相比 C 原生字符串的 4 个优点:


O(1) 复杂度获取字符串长度

C 字符串是最后一个元素为 \0 的字符数组,获取长度需从头到尾 O(N) 遍历。


SDS 结构中用 len 字段记录了字符串长度,并在各种增删操作中动态维护其长度,使用 strlen 直接读取字段值即可。


避免缓冲区溢出

C 字符串的 strcpy 操作,若 dst 未分配足够内存,应用有 crash 或被缓冲区溢出攻击的风险。


SDS 在操作字符串前会检查其空间,若不够则预分配,从根源上杜绝溢出。


二进制安全

C 以 '\0' 作为字符串分隔符,故不能保存如图片等穿插大量空字符的数据。


SDS API 用 len 长度来界定字符串边界,存入什么就取出什么,故操作二进制数据是安全的。但 SDS 同样以 '\0' 作为字符串的分隔符,方便直接重用 string.h 中的丰富库函数。


内存预分配与惰性释放

C 的字符串每次增长或缩减,都要 realloc 重分配内存。


SDS 每次以成倍或加 1MB 的方式扩展空间,且清空时不释放内存预留下次使用。从而将 N 次字符串操作,内存重分配次数从必定 N 次减少为最多 N 次。


总结

Redis 封装 C 原生字符串为 SDS,并实现了取长度、复制、比较、内存预分配等 API 供上层使用,可以看到 API 实现中对 buf 直接进行内存拷贝等操作,十分高效。


因为封装,SDS 相比原生字符串中间隔了一层取地址等操作,但其 API 耗时并未成为 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
目录
相关文章
|
5月前
|
存储 缓存 NoSQL
redis数据结构-字符串
redis数据结构-字符串
47 1
|
3月前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
52 4
|
2月前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
4月前
|
存储 缓存 NoSQL
3)深度解密 Redis 的字符串
3)深度解密 Redis 的字符串
46 1
|
5月前
|
NoSQL 安全 Java
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
这篇文章深入探讨了Redis中的String数据类型,包括键操作的命令、String类型的命令使用,以及String在Redis中的内部数据结构实现。
Redis6入门到实战------ 三、常用五大数据类型(字符串 String)
|
5月前
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
118 0
|
5月前
|
存储 NoSQL Redis
【Redis 探秘】SDS 简单动态字符串:揭秘 Redis 高效字符串处理的秘密武器!
【8月更文挑战第24天】Redis采用的简单动态字符串(SDS)是一种专为优化内存存储和字符串操作而设计的数据结构。相较于C语言的标准字符串,SDS改进了字符串长度计算、内存重分配及字符串比较等问题。其特性包括预分配冗余空间减少未来的内存重分配、显式存储长度以加快获取速度等。这些改进使Redis能更高效地管理字符串数据。例如,在Redis中,SDS被广泛应用于键值对的存储,显著提升了字符串操作的性能。了解SDS不仅对于深入理解Redis的工作原理至关重要,也是开发者技能树中的重要一环。
74 0
|
5月前
|
存储 JSON NoSQL
揭秘Redis字符串String的隐藏技能!从基础到进阶,让你的数据存储操作秒变高大上!
【8月更文挑战第24天】Redis中的字符串类型作为其基石,不仅能够存储从简单文本到复杂格式如JSON的各种数据,还能通过多样化的命令实现包括但不限于自增自减、设置过期时间等高级功能,极大提升了其实用性和灵活性。例如,使用`SET`命令可添加或更新键值对,`GET`获取值,`DEL`删除键;同时,`INCR`和`DECR`支持对整数值的原子性增减操作,非常适合实现计数器等功能;通过`EXPIRE`命令设置过期时间,则适用于需要限时存储的应用场景。尽管名为“字符串”,但实际上还可存储图片、音频文件的Base64编码等形式的数据,为开发者提供了强大而灵活的工具。
62 0
|
5月前
|
NoSQL Java Redis
Redis字符串数据类型之INCR命令,通常用于统计网站访问量,文章访问量,实现分布式锁
这篇文章详细解释了Redis的INCR命令,它用于将键的值增加1,通常用于统计网站访问量、文章访问量,以及实现分布式锁,同时提供了Java代码示例和分布式锁的实现思路。
160 0
|
6月前
|
存储 NoSQL Redis
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX
Redis07命令-String类型字符串,不管是哪种格式,底层都是字节数组形式存储的,最大空间不超过512m,SET添加,MSET批量添加,INCRBY age 2可以,MSET,INCRSETEX