Redis的设计与实现(1)-SDS简单动态字符串

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 大多数情况下, Redis使用SDS(Simple Dynamic String, 简单动态字符串)作为字符串表示, 比起C字符串, SDS具有以下优点:1. 常数复杂度获取字符串长度;2. 杜绝缓冲区溢出;3. 减少修改字符串时带来的内存重分配次数;4. 二进制安全;5. 兼容部分C字符串函数.

现在在高铁上, 赶着春节回家过年, 无座站票, 电脑只能放行李架上, 面对着行李架撸键盘--看过这本书, 突然想起, 便整理下SDS的内容, 相对后面的章节, 算是比较简单的~

大多数情况下, Redis使用SDS(Simple Dynamic String, 简单动态字符串)作为字符串表示, 比起C字符串, SDS具有以下优点:

  1. 常数复杂度获取字符串长度;
  2. 杜绝缓冲区溢出;
  3. 减少修改字符串时带来的内存重分配次数;
  4. 二进制安全;
  5. 兼容部分C字符串函数.

以上列举的优点, 也算是这篇文章的主要内容. 先从定义说起吧:

1. SDS的定义

SDS结构定义如下(sds.h/sdshdr):

struct sdshdr {
    // 记录buf数组中已使用字节的数量, 等于SDS所保存字符串的长度
    int len;
    // 记录buf数组中未使用字节的数量
    int free;
    // 字节数组, 用于保存字符串
    char buf[];
}
  1. len属性记录了已使用的字节数量(字符串长度);
  2. free属性的值为0, 表示这个SDS没有未使用的空间;
  3. free属性的值为5, 表示这个SDS保存了一个5字节长的字符串;
  4. buf属性是一个char类型的数组, 数组的最后一个字节保存了空字符\0.

SDS遵循C字符串以空字符串结尾的惯例, 空字符不计入SDS的len属性, 即额外为空字符分配了1字节的空间, 并且添加空字符到字符串末尾均由SDS函数自动完成, 对使用者完全透明. 该特性带来的好处是, SDS可以直接复用C字符串函数库的部分函数.

2. SDS与C字符串的区别

2.1 常数复杂度获取字符串长度

  1. 由于C字符串不记录自身长度, 所以获取长度时需要遍历整个字符串, 直到遇到空字符\0为止, 该操作的复杂度为O(N);
  2. 由于SDS在len属性中记录了SDS本身的长度, 所以获取一个SDS的长度的复杂度为O(1).

Redis使用SDS, 将获取字符串长度所需的复杂度从O(N)降低到O(1), 确保获取字符串长度的工作不会成为Redis的性能瓶颈.

2.2 杜绝缓冲区溢出

由于C字符串不记录自身长度, 以函数strcat来说, 当执行该函数时, 都是认为已经为dest分配了足够的内存容纳src字符串, 但如果该假设不成立, 就会产生缓冲区溢出.

然而, SDS的字符串空间分配策略, 从根本上杜绝了缓冲区溢出的可能性: 当SDS-API需要对SDS进行修改时, API会先检查SDS的空间是否满足修改所需的需求, 如果不满足的话, API会自动扩容至所需大小, 再执行修改操作. 所以, SDS无需手工维护SDS的空间大小, 也不会产生缓冲区溢出的问题.

2.3 减少修改字符串时带来的内存重分配次数

由于C字符串不记录自身长度, 所以每次增长或缩减字符串, 需要对保存这个C字符串的数组进行一次内存重分配操作:

1.如果程序执行的是增长字符串的操作, 比如拼接操作append, 需要进行内存重分配操作, 扩展底层数组至合适大小, 否则将会产生缓冲区溢出;
2.如果程序执行的是缩短字符串的操作, 比如截断操作trim, 需要进行内存释放操作, 否则将会产生内存泄露.

Redis作为数据库, 经常用于速度要求严苛, 数据被频繁修改的场合, 减少内存的重分配次数能提高性能. SDS通过未使用空间解除了字符串长度底层数组长度之间的关联: 在SDS中, buf数组的长度不一定就是字符数加一, 数组里面可以包含未使用的字节, 而这些字节的数量就由SDS的free属性记录.

通过未使用空间, SDS实现了空间预分配和惰性空间释放两种优化策略.

1.空间预分配

当SDS-API对SDS进行修改, 并且需要对SDS进行空间扩展的时候, 程序不仅会为SDS分配修改所需的空间, 还会为SDS分配额外的未使用空间.

额外分配的未使用空间数量, 由以下公式决定:

  1. 如果对SDS进行修改之后, SDS的长度(即len属性)将小于1MB, 那么程序分配和len属性同样大小的未使用空间, 这时SDS的len属性的值将和free属性的值相同: 比如修改之后, SDS的len将变成13字节, 那么程序也会分配13字节的未使用空间, buf长度将变成 13Byte + 13Byte + 1Byte = 27Byte;
  2. 如果对SDS进行修改之后, SDS的长度将大于等于1MB, 那么程序会分配1MB的未使用空间: 比如修改之后, SDS的len将变成30MB, 那么程序会分配1MB的未使用空间, buf长度将变成 30MB + 1MB + 1Byte.

通过空间预分配策略, Redis可以减少连续执行字符串增长操作所需的内存重分配次数.

2.惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作: 当SDS-API需要缩短SDS字符串时, 程序并不会立即回收内存, 而是使用free属性将这些字节的数量记录起来, 并等待将来使用.

当然, SDS也提供了相应的API真正地释放SDS的未使用空间, 无需担心该策略带来的内存浪费问题.

2.4 二进制安全

C字符串除了末尾, 不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾, 所以C字符串只能保存文本数据, 而不能保存像图片, 音频, 视频, 压缩文件这样的二进制数据.

Redis为了可以适用于各种不同的场景, SDS-API都是二进制安全的(binary-safe), 所有SDS-API都会以二进制的方式处理buf数组里面的数据, 不限制, 不过滤, 不假设, 写入什么就读到什么.

2.5 兼容部分C字符串函数

虽然SDS-API是二进制安全的, 但它们一样遵循C字符串以空字符结尾的惯例: 这些API总是将SDS保存的数据末尾数组为空字符, 并且总会在为buf分配空间时多分配一个字节来容纳这个空字符, 以复用库定义的函数.

3. SDS的API

函数 作用 复杂度
sdsnew 创建一个包含给定C字符串的SDS O(N), N为给定C字符串的长度
sdsempty 创建一个不包含任何内容的空SDS O(1)
sdsfree 释放给定的SDS O(N), N为被释放SDS的长度
sdslen 返回SDS的已使用空间字节数 这个值可以通过读取SDS的len属性来直接获得, 复杂度为O(1)
sdsavail 返回SDS的未使用空间字节数 这个值可以通过读取SDS的free属性来直接获得, 复杂度为O(1)
sdsdup 创建一个给定SDS的副本(copy) O(N), N为给定C字符串的长度
sdsclear 清空SDS保存的字符串内容 因为惰性空间释放策略, 复杂度为O(1)
sdscat 将给定C字符串拼接到另一个SDS字符串的末尾 O(N), N为被拼接C字符串的长度
sdscatsds 将给定SDS字符串拼接到另一个SDS字符串的末尾 O(N), N为被拼接SDS字符串的长度
sdscpy 将给定的C字符串复制到SDS里面, 覆盖SDS原有的字符串 O(N), N为被复制SDS字符串的长度
sdsgrowzero 用空字符将SDS扩展至给定长度 O(N), N为扩展新增的字节数
sdsrange 保留SDS给定区间内的数据, 不在区间内的数据会被覆盖或清除 O(N), N为被保留数据的字节数
sdstrim 接受一个SDS和一个C字符串作为参数, 从SDS中移除所有在C字符串中出现过的字符 O(N^2), N为给定C字符串的长度
sdscmp 对比两个SDS字符串是否相同 O(N), N为两个SDS钟较短的那个SDS的长度

4. 总结

  • Redis在大多数情况下使用SDS作为字符串的表示;

  • 相比C字符串, SDS具有以下优点:

  1. 常熟复杂度获取字符串长度;
  2. 杜绝缓冲区溢出;
  3. 减少修改字符串长度时所需的内存重分配次数;
  4. 二进制安全;
  5. 兼容部分C字符串函数.
  • C字符串和SDS之间的区别:
C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
API是不安全的,可能造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次内存重分配 修改字符串长度N次最多需啊哟执行N次内存重分配
可以使用所有库中的函数 可以使用一部分库中的函数

以上笔记都是整理自, 真的很感谢作者, 也希望自己能坚持写下去^_^.


文章来源于本人博客,发布于 2018-02-15,原文链接:https://imlht.com/archives/106/

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
3月前
|
存储 缓存 NoSQL
redis数据结构-字符串
redis数据结构-字符串
38 1
|
29天前
|
NoSQL Redis
Redis 字符串(String)
10月更文挑战第16天
37 4
|
21天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
2月前
|
存储 缓存 NoSQL
3)深度解密 Redis 的字符串
3)深度解密 Redis 的字符串
32 1
|
3月前
|
C# 开发者 UED
WPF开发者必备秘籍:深度解析文件对话框使用技巧,打开与保存文件原来如此简单!
【8月更文挑战第31天】在WPF应用开发中,文件操作是常见需求。本文详细介绍了如何利用`Microsoft.Win32`命名空间下的`OpenFileDialog`和`SaveFileDialog`类来正确实现文件打开与保存功能。通过示例代码展示了如何设置文件过滤器、初始目录等属性,并使用对话框进行文件读写操作。正确使用文件对话框能显著提升用户体验,使应用更友好易用。
80 0
|
3月前
|
存储 NoSQL Redis
【Redis 探秘】SDS 简单动态字符串:揭秘 Redis 高效字符串处理的秘密武器!
【8月更文挑战第24天】Redis采用的简单动态字符串(SDS)是一种专为优化内存存储和字符串操作而设计的数据结构。相较于C语言的标准字符串,SDS改进了字符串长度计算、内存重分配及字符串比较等问题。其特性包括预分配冗余空间减少未来的内存重分配、显式存储长度以加快获取速度等。这些改进使Redis能更高效地管理字符串数据。例如,在Redis中,SDS被广泛应用于键值对的存储,显著提升了字符串操作的性能。了解SDS不仅对于深入理解Redis的工作原理至关重要,也是开发者技能树中的重要一环。
59 0
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
9天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
10天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构