Redis 学习笔记之动态字符串(SDS)

Redis 学习笔记之动态字符串(SDS)

SDS(simple dynamid string)

一、Redis 的 String 的基本概念

1.1 二进制安全

Redis String 是二进制安全的。
二进制安全是指,在传输数据时,保证二进制数据的信息安全,也就是不被篡改、破译等,如果被攻击,能够及时检测出来。

1.2 Redis String 值的最大长度为?

字符串值的最大长度为 512 MB

二、SDS 的定义

2.1 数据结构定义

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

};

2.2 SDS 与 C 字符串的区别?

2.2.1 获取字符串长度方面
  • C 语言获取字符串的长度是通过遍历字符串得到,时间复杂度为 O(N),每次调用都需要遍历

  • Redis 中的 SDS 使用了变量 len 保存了字符串的长度,时间复杂度为 O(1), 比 C 提高了效率

2.2.2 防止字符串缓存区溢出
  • C 字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出(buffer overflow)

假设程序里有两个在内存中紧邻着的 C 字符串 s1 和 s2 , 其中 s1 保存了字符串 "Redis" , 而 s2 则保存了字符串 "MongoDB" , 如图 2-7 所示。

此时,需要调用 strcat(s1, " Cluster"),将 s1 的内容修改为 "Redis Cluster" , 但却忘了在执行 strcat 之前为 s1 分配足够的空间, 那么在 strcat 函数执行之后, s1 的数据将溢出到 s2 所在的空间中, 导致 s2 保存的内容被意外地修改, 如图 2-8 所示。

2.2.3 优化内存重分配次数
  • C 语言字符串内存重分配操作(会频繁进行内存重新分配,N 次修改 N 次分配)
    • 增长字符串的操作时,需要先通过内存重分配来扩展底层数组的空间大小 —— 如果忘了这一步就会产生缓冲区溢出。
    • 缩短字符串的操作时,需要通过内存重分配来释放字符串不再使用的那部分空间 —— 如果忘了这一步就会产生内存泄漏。
  • Redis SDS 为了优化内存重分配次数做了以下操作(N 次修改,最多 N 次分配)
    • 空间预分配

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

  • 惰性空间释放

惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。当然 SDS 也提供了相应的 API,真正地释放 SDS 里面的未使用空间,避免造成内存浪费。

2.2.4 二进制安全问题

C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
以下例子,在 C 字符串中遇到 '\0' 会被误认为是字符串结尾, 最后读取的字符串只有 Redis,而忽略之后的 "Cluster" 。


而在 Redis 的 SDS 中, 所有 API 都是二进制安全的,判断字符串结尾是使用字段 len 判断的,所以不会出现 C 字符串中的情况, 可以保存字符串和二进制数据。

总结

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

三、Redis Strings常用命令了解

# 连接redis 
redis-cli -h 127.0.0.1 -p 6379 
#验证密码
auth 123456

3.1 set 命令

#添加一个key=test, value = java
127.0.0.1:6379> set test java
OK

3.2 get 命令

# 获取键的值
127.0.0.1:6379> get test
"java"

3.3 APPEND 命令

#命令返回字符串的长度
#key 存在时,追加值到已有字符串的尾部
127.0.0.1:6379> APPEND test  tutorial
(integer) 12
127.0.0.1:6379> get test
"javatutorial"

#当key 不存在时,创建key,并设置空字符串,并追加12到尾部
127.0.0.1:6379> APPEND age 12
(integer) 2

3.4 DECR

  • 对存储的key值进行减一操作
  • 如果操作的key 不存在,则会先初始化key,并设置key的值为0,再减一
  • 如果key的value类型错误或者是个不能表示成数字的字符串,就返回错误。
  • 这个操作最多支持64位有符号的正型数字。
###对已有key 操作
127.0.0.1:0>set num 10
"OK"
127.0.0.1:0>decr num
"9"

###对不存在的key 操作
127.0.0.1:0>decr num1
"-1"

###对于不是整型的key 操作
127.0.0.1:0>set name Java95
"OK"
127.0.0.1:0>decr name
"ERR value is not an integer or out of range"

###对于超出64位的值操作
127.0.0.1:0>set num2  9999999999999999999999
"OK"
127.0.0.1:0>decr num2
"ERR value is not an integer or out of range"

3.5 GETSET 命令

  • 自动将key对应到value并且返回原来key对应的value。
  • 如果key存在但是对应的value不是字符串,就返回错误。
  • 如果key不存在,则会创建key,并设置value,返回null
#获取存在的key, 并设置新的值,返回旧的值
127.0.0.1:0>getset num 123
"9"
127.0.0.1:0>getset num 456
"123"

#获取不存在的值
127.0.0.1:0>getset num3 123
null
127.0.0.1:0>getset num3 456
"123"

3.6 INCR 命令

  • 对存储在指定key的数值执行原子的加1操作
  • 如果指定的key不存在,那么在执行incr操作之前,会先将它的值设定为0
  • 如果key的value类型错误或者是个不能表示成数字的字符串,就返回错误。
  • 这个操作仅限于64位的有符号整型数据。
#存在的key 
127.0.0.1:0>get num
"456"
127.0.0.1:0>incr num
"457"

#不存在的key
127.0.0.1:0>incr num4
"1"
127.0.0.1:0>incr num4
"2"

3.7 SETEX 命令

  • 设置key对应字符串value,并且设置key在给定的seconds时间之后超时过期。
#设置key,给定超时时间30秒,value 为java
127.0.0.1:0>setex expireKey 30  java
"OK"
##使用ttl 命令查看key 的过期时间
127.0.0.1:0>ttl expireKey
"15"
127.0.0.1:0>get expireKey
"java"
127.0.0.1:0>ttl expireKey
"4"

#当过期时间为-2时,证明key已经被删除
127.0.0.1:0>ttl expireKey
"-2"
127.0.0.1:0>get expireKey
null

3.8 SETNX(SET if Not eXists)命令

  • 将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。
  • 返回1, key 已存在,0 key不存在
  • 可以使用该命令实现分布式锁
127.0.0.1:0>SETNX mykey "Hello"
"1"
127.0.0.1:0>SETNX mykey "World"
"0"
127.0.0.1:0>get mykey
"Hello"

参考

posted @ 2020-08-30 13:13  JiaJianHuang  阅读(154)  评论(0编辑  收藏  举报