Redis学习(一)简单动态字符串(SDS)

导读:本篇文章讲解 Redis学习(一)简单动态字符串(SDS),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1. 什么是简单动态字符串(SDS)

简单动态字符串(SDS),即:simple dynamic string
Redis 没有直接使用 C 语言传统的字符串表示,而是自己构建了一种简单动态字符串的抽象类型,并将 SDS 用作 Redis 的默认字符串表示。

2. SDS 的数据结构

struct sdshdr {
    // 记录buf数组中已使用字节的数量
    // 等于SDS所保存字符串的长度
    int len;
    
    // 记录buf数组中未使用字节的数量
    int free;
    
    // 字节数组,用于保存字符串
    char buf[];
};

2.1 示例

在这里插入图片描述

  • free = 0:表示这个 SDS 没有分配任何未使用空间;
  • len = 4:表示这个 SDS 保存了一个四字节长的字符串;
  • buf:是一个 char 类型的数组,最后一个字节和C语言字符串一样,保存了空字符 ‘\0’。

SDS 遵循 C 语言字符串以空字符结尾的惯例,保存空字符的 1 字节不计算在 SDS 的 len 属性里面,并且为空字符分配额外的 1 字节空间,以及添加空字符到字符串末尾等操作,都是由 SDS 函数自动完成的。

2.2 SDS 以 \0 结尾的好处

遵循空字符结尾这一惯例的好处是:SDS 可以直接重用一些 C语言字符串函数库里面的函数。

3. SDS 基本使用

3.1 SET

Redis SET 命令用于设置给定 key 的值。如果 key 已经存储其他值, SET 就覆写旧值,且无视类型。

3.1.1 语法

redis 127.0.0.1:6379> SET KEY_NAME VALUE

3.1.2 示例

# 对不存在的键进行设置
127.0.0.1:6379> SET myKey myValue
OK
127.0.0.1:6379> GET myKey
"myValue"

# 覆盖已存在的键进行设置
127.0.0.1:6379> SET myKey coverValue
OK
127.0.0.1:6379> GET myKey
"coverValue"

3.2 GET

Redis Get 命令用于获取指定 key 的值。如果 key 不存在,返回 nil 。如果key 储存的值不是字符串类型,返回一个错误。

3.2.1 语法

redis 127.0.0.1:6379> GET KEY_NAME

3.2.2 示例

# 获取已存在的Key
127.0.0.1:6379> GET myKey
"coverValue"

# 获取不存在的Key
127.0.0.1:6379> GET noExistsKey
(nil)

# 获取Key所存储的值非字符串类型
127.0.0.1:6379> LPUSH myList myValues
(integer) 1
127.0.0.1:6379> GET myList
(error) WRONGTYPE Operation against a key holding the wrong kind of value

3.3 GETRANGE

Redis Getrange 命令用于获取存储在指定 key 中字符串的子字符串。字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。

3.3.1 语法

redis 127.0.0.1:6379> GETRANGE KEY_NAME start end

3.3.2 示例

127.0.0.1:6379> GET myKey
"coverValue"
127.0.0.1:6379> GETRANGE myKey 0 1
"co"
127.0.0.1:6379> GETRANGE myKey 0 -1
"coverValue"
127.0.0.1:6379> GETRANGE myKey 0 0
"c"
127.0.0.1:6379> GETRANGE myKey 1 1
"o"

3.4 MSET

Redis Mset 命令用于同时设置一个或多个 key-value 对。

3.4.1 语法

redis 127.0.0.1:6379> MSET key1 value1 key2 value2 .. keyN valueN 

3.4.2 示例

127.0.0.1:6379> MSET key1 value1 key2 value2 key3 value3
OK
127.0.0.1:6379> MGET key1 key2 key3
1) "value1"
2) "value2"
3) "value3"

3.5 MGET

Redis Mget 命令返回所有(一个或多个)给定 key 的值。 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。

3.5.1 语法

redis 127.0.0.1:6379> MGET KEY1 KEY2 .. KEYN

3.5.2 示例

127.0.0.1:6379> GET myKey
"coverValue"
127.0.0.1:6379> GET myKey2
"myValue2"
127.0.0.1:6379> GET myKey3
(nil)
127.0.0.1:6379> MGET myKey myKey2 myKey3
1) "coverValue"
2) "myValue2"
3) (nil)

3.6 STRLEN

Redis Strlen 命令用于获取指定 key 所储存的字符串值的长度。当 key 储存的不是字符串值时,返回一个错误。

3.6.1 语法

redis 127.0.0.1:6379> STRLEN KEY_NAME

3.6.2 示例

# 获取已存在的Key
127.0.0.1:6379> GET myKey
"coverValue"
127.0.0.1:6379> STRLEN myKey
(integer) 10

# 获取Key所存储的值非字符串
127.0.0.1:6379> STRLEN myList
(error) WRONGTYPE Operation against a key holding the wrong kind of value

# 获取不存在的Key
127.0.0.1:6379> STRLEN noExistsKey
(integer) 0

3.7 SETNX

Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
设置成功,返回 1 。 设置失败,返回 0 。

3.7.1 语法

redis 127.0.0.1:6379> SETNX KEY_NAME VALUE

3.7.2 示例

# 查看myKey4是否存在 ->127.0.0.1:6379> exists myKey4
(integer) 0

# 给myKey4设值
127.0.0.1:6379> SETNX myKey4 myValue4
(integer) 1

# 尝试覆盖myKey4 -> 失败
127.0.0.1:6379> SETNX myKey4 myValue5
(integer) 0

# 再次获取myKey4,存储的值没变
127.0.0.1:6379> GET myKey4
"myValue4"

3.8 SETEX

Redis Setex 命令为指定的 key 设置值及其过期时间。如果 key 已经存在, SETEX 命令将会替换旧的值。
设置成功时返回 OK 。

3.8.1 语法

redis 127.0.0.1:6379> SETEX KEY_NAME TIMEOUT VALUE

3.8.2 示例

# 设值myKey5的过期时间为:5s
127.0.0.1:6379> SETEX myKey5 10 myValue5
OK

# 查看myKey5的过期剩余时间
127.0.0.1:6379> TTL myKey5
(integer) 7

# 10s内获取myKey5
127.0.0.1:6379> GET myKey5
"myValue5"

# 10s后获取myKey5
127.0.0.1:6379> GET myKey5
(nil)

3.9 INCR

Redis Incr 命令将 key 中储存的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内。

3.9.1 语法

redis 127.0.0.1:6379> INCR KEY_NAME 

3.9.2 示例

# myKey6存在时
127.0.0.1:6379> EXISTS myKey6
(integer) 0
127.0.0.1:6379> INCR myKey6
(integer) 1
127.0.0.1:6379> GET myKey6
"1"

# myKey6存在时
127.0.0.1:6379> INCR myKey6
(integer) 2
127.0.0.1:6379> GET myKey6
"2"

# 操作非字符串值时
127.0.0.1:6379> INCR myList
(error) WRONGTYPE Operation against a key holding the wrong kind of value

3.10 INCRBY

Redis Incrby 命令将 key 中储存的数字加上指定的增量值。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内。

3.10.1 语法

redis 127.0.0.1:6379> INCRBY KEY_NAME INCR_AMOUNT

3.10.2 示例

127.0.0.1:6379> EXISTS myKey7
(integer) 0
127.0.0.1:6379> INCRBY myKey7 10
(integer) 10
127.0.0.1:6379> INCRBY myKey7 10
(integer) 20

3.11 DECR

Redis Decr 命令将 key 中储存的数字值减一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内。

3.11.1 语法

redis 127.0.0.1:6379> DECR KEY_NAME 

3.11.2 示例

127.0.0.1:6379> EXISTS myKey8
(integer) 0
127.0.0.1:6379> DECR myKey8
(integer) -1
127.0.0.1:6379> DECR myKey8
(integer) -2
127.0.0.1:6379> DECR myList
(error) WRONGTYPE Operation against a key holding the wrong kind of value

3.12 DECRBY

Redis Decrby 命令将 key 所储存的值减去指定的减量值。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。
如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
本操作的值限制在 64 位(bit)有符号数字表示之内

3.12.1 语法

redis 127.0.0.1:6379> DECRBY KEY_NAME DECREMENT_AMOUNT

3.12.2 示例

127.0.0.1:6379> EXISTS myKey9
(integer) 0
127.0.0.1:6379> DECRBY myKey9 10
(integer) -10
127.0.0.1:6379> DECRBY myKey9 10
(integer) -20

4. SDS 与 C语言字符串的区别

4.1 获取字符串长度

4.1.1 C语言字符串

因为C 语言字符串本身不记录长度信息,所以为了获取字符串长度,程序必须遍历整个字符串,直到遇到 \0 结束符为止,所以它获取长度的复杂度为:O(N)

4.1.2 SDS

从 SDS 的数据结构中可知道,它用 len 字段保存了字符串的长度,所以获取一个 SDS 长度的复杂度为:O(1)

4.2 杜绝缓冲区溢出

4.2.1 C语言字符串

<string.h>strcat 函数可以将 src 字符串中的内容拼接到 dest 字符串的末尾:
char *strcat(char *dest, const char *src)

假设某个程序中有两个在内存中紧邻着的 C字符串 s1s2,其中 s1 保存了字符串 Redis,而 s2 则保存了字符串 MongoDB,如图所示:
在这里插入图片描述

那么在执行 strcat(s1, 'Cluster') 时,如果没有为 s1 分配足够的空间,那么在执行 strcat 方法后,s1 的数据将溢出到 s2 所在的空间中,导致 s2 保存的内容被意外地修改。如图:
在这里插入图片描述

4.2.2 SDS

当 SDS API 需要对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足修改所需的要求,如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以不会出现C语言字符串中的缓冲区溢出问题。

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

4.3.1 C语言字符串

因为 C 字符串并不记录自身长度,且末尾总是包含 \0 的结束符。所以,每次增长或者缩短一个 C 字符串,程序都要对保存这个 C 字符串的数组进行一次内存重分配操作。

  • 如果程序执行的是增长字符串的操作,那么在执行之前,需要先通过内存重分配来扩展底层数组的大小,不然会产生缓冲区溢出的问题。
  • 如果程序执行的是缩短字符串的操作,那么在执行之后,需要通过内存重分配来释放字符串的空闲空间,不然会产生内存泄漏的问题。

4.3.2 SDS

为了避免 C 字符串存在的问题,SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联:在 SDS 中,buf 数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由 free 属性记录。
通过未使用空间,SDS 实现了空间预分配惰性空间释放两种优化策略。

4.3.2.1 空间预分配

空间预分配用于优化 SDS 的字符串增长操作:当 SDS API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必须的空间,还会为 SDS 分配额外的未使用空间。
预分配的空间大小由以下公式决定:

  • 如果对 SDS 进行修改之后,SDS 的长度小于 1MB。那么程序会分配和 len 属性同样大小的未使用空间。如:修改后,SDS len = 10 字节 < 1 MB,那么程序会额外分配多 10 字节。所以最终结果为:10 + 10 + 1 = 21 字节。
  • 如果对 SDS 进行修改之后,SDS 的长度大于 1MB,那么程序将分配 1MB 的未使用空间。

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

4.3.2.2 惰性空间释放

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

4.4 二进制安全

C 字符串中的字符必须符合某种编码(比如:ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串的结尾。有着这个限制,使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
而 SDS API 都是二进制安全的,所有的 SDS API 都会以处理二进制的方式来处理 SDS 存在在 buf 数组里的数据。

4.5 兼容部分 C 字符串函数

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

4.6 总结

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

5. SDS API

函数 作用 时间复杂度
sdsnew 创建一个包含给定 C 字符串的 SDS O(N),N 为给定 C 字符串的长度
sdsfree 释放给定的 SDS O(N),N 为被释放 SDS 的长度
sdsdup 创建一个给定 SDS 的副本 O(N),N 为给定 SDS 的长度
sdscat 将给定 C 字符串拼接到 SDS 字符串的末尾 O(N),N 为被拼接 C 字符串的长度
sdscatsds 将给定 SDS 字符串拼接到另一个 SDS 字符串的末尾 O(N),N 为被拼接 SDS 字符串的长度
sdscpy 将给定的 C 字符串复制到 SDS 里面,覆盖 SDS 原有的字符串 O(N),N 为被复制 C 字符串的长度
sdsgrowzero 用空字符将 SDS 扩展至给定长度 O(N),N 为扩展新增的字节数
sdsrange 保留 SDS 给定区间内的数据,不在区间内的数据会被覆盖或清除 O(N),N 为被保留数据的字节数
sdstrim 接受一个 SDS 和一个 C 字符串作为参数,从 SDS 左右两端分别移除所有在 C 字符串中出现过的字符 O(M*N),M 为 SDS 的长度,N 为给定 C 字符串的长度
sdscmp 对比两个 SDS 字符串是否相同 O(N),N 为两个 SDS 中较短的那个 SDS 的长度

这里只列出了 SDS API中时间复杂度不为:O(1) 的API

6. 参考

7. 其他相关文章

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/77875.html

(0)
小半的头像小半

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!