【Redis】 简单动态字符串

Metadata

title: 【Redis】 简单动态字符串
date: 2023-07-09 08:28
tags:
  - 行动阶段/完成
  - 主题场景/数据存储
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/数据存储/Redis
categories:
  - 数据存储
keywords:
  - 数据存储/Redis
description: 【Redis】 简单动态字符串

概述

Redis 中,涉及可以被修改的字符串值时,都用简单动态字符串(simple dynamic string,SDS)来实现。比如包含字符串值的键值对在底层的实现。C 字符串(C 语言中传统字符串,以空字符串结尾的字符数组)则用于无须对字符串进行修改的地方,比如日志打印。

SDS 还被用作缓冲区,比如 AOF 模块中的 AOF 缓冲区,客户端状态中的输入缓冲区。

Redis只会使用C字符串作为字面量,在大多数情况下,Redis使用SDS(Simple Dynamic String,简单动态字符串)作为字符串表示。

比起C字符串,SDS具有以下优点

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

SDS 定义

struct sdshdr{
    //buf已使用的字节数
    int len;
    //buf未使用的字节数
    int free;
    //字节数组,用于保存字符串
    char buf[];
}

buf 遵循 C 字符串以空字符串结尾的惯例,保存空字符串的 1 字节空间不计算在 SDS 的 len 属性里面,并为空字符分配额外 1 字节空间,对用户来说是透明的。

如中展示了 SDS 的数据结构,5 字节未使用空间,已使用 5 字节,buf 存储了字符串值,最后一个字节保存了空字符'\0'。这里要注意的是,free 和 len 的计算不涉及空字符。

SDS 与 C 字符串的区别

  1. SDS 有常数级的时间复杂度获取字符串长度。
    由于 C 字符串不会记录自身长度,因此只能遍历,直到遇到结尾的空字符为止, 时间复杂度为 O(N)。而 SDS 对于字符串长度的记录都是在其 API 中执行的,所以时间复杂度为 **O(1)**。
  2. SDS 杜绝缓冲区溢出。
    由于 C 字符串未记录自身长度,容易导致缓冲区溢出。在执行字符串拼接时,如果没有足够的空间,并且相邻内存地址被其他字符串占用时,字符串的数据将溢出,且容易意外修改相邻的字符串内容。相比而言,SDS 会将这种情况扼杀在摇篮之中,SDS API 先判断空间是否满足,如果不满足则将空间扩展至执行修改所需的大小
  3. SDS 拥有内存分配策略,详见 1.3。
  4. SDS API 都是二进制安全的。
    C 字符串的字符必须符合某种编码,并且中间不能有空字符,否则读取时会被误以为是字符串结尾。种种局限使得 C 字符串只能存文本,不能存图片,音频,视频,压缩文件等二进制数据。 为确保 Redis 对不同使用场景的支持,SDS API 都是二进制安全的,也就是所有 SDS API 都会以二进制的方式存取 buf 中的数据,数据的写入和读出都是一个样的。由于 SDS 读取时并不是依靠空字符来判断结束的,而是 len 属性,所以是二进制安全的。
  5. 兼容部分 C 字符串函数
    SDS 虽然都是二进制安全的,但也遵循以空字符结尾的习惯。SDS API 总会在 buf 数组分配空间时多分配一个字节用于容纳空字符,这是为了保存文本的 SDS 重用一部分 <string.h> 库函数,避免代码重复。

内存分配策略

由于 C 字符并不记录自身长度,并且需要一个字符空间保存空字符串,因此每次增长或缩短字符串时,就要对其进行一次内存重分配操作。增长字符串时要看空间是否够用,否则会有缓冲区溢出;缩短字符串要释放不用的空间,否则会有内存泄漏

Redis 经常被用于速度要求严苛,数据被频繁修改的场合,每次修改字符串都要重新分配内存,就会占用很多时间。为避免这个问题,redis 采用了空间预分配惰性空间释放两种策略。

空间预分配

空间预分配用于优化 SDS 字符串增长操作。在扩展 SDS 空间前,SDS API 会先检查未使用空间够不够,如果不够,则进行空间预分配。此时,程序不仅会为 SDS 分配修改所必须要的空间,还为其分配额外未使用的空间。

  • 修改后的 SDS<1MB,程序分配和 len 属性同样大小的未使用空间,此时 SDS 的 len 与 free 大小相等。比如修改后实际存储字符串的空间变为 13 字节,那么 len=13,free=13,buf 数组整体的长度 = 13+13+1(额外 1 字节保存空字符)。
  • 修改后 SDS>=1MB。程序会分配 1MB 的未使用空间。比如修改后实际存储字符串的空间变为 2MB,那么 len=2M,free=1MB,buf 数组整体的长度 = 2MB+1MB+1byte。

通过空间的预分配,将连续增长 N 次字符串需要的内存分配次数从一定需要 N 次变为最多 N 次。因而可以减少连续执行字符串增长操作所需的内存重分配的次数。

惰性空间释放

惰性空间的释放用于优化 SDS 字符串缩短操作。当 SDS API 需要缩短保存的字符串时,程序并不立即回收这部分内存,而是使用 free 属性将字节的数量记录,等待使用。与此同时,SDS 提供了相关 API,在有需要时,真正释放未使用空间,不需要担心惰性空间造成的内存浪费。

C 字符串与 SDS 的区别简单来说:

C字符串 SDS
获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
“API是不安全的,可能会造成缓冲区溢出” API是安全的,不会造成缓冲区溢出
修改字符串长度N次必然需要执行N次重分配 修改字符串长度N次最多需要执行N次重分配
只能保存文本数据 可以保存文本或二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

SDS 时间复杂度

SDS 相关操作及时间复杂度:

操作 时间复杂度
创建/释放/拷贝一个的SDS “O(N),N为C字符串长度”
清空SDS内容 “情性空闫释放,O(1)”
给定C字符率拼接到SDS尾部 “O(N),N为被拼接字符串长度”

SDS API

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