Redis String类型详解

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

SDS的定义如下:

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

    // 记录 buf 数组中未使用字节的数量
    int free;

    // 字节数组, 用于保存字符串
    char buf[];
};

SDS 与 C 字符串的区别

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

因为 C 字符串不记录自身长度信息,所以为了获取一个 C 字符串的长度,程序必须遍历整个字符串, 对每个字符进行计数,直到遇到代表结尾的空字符为止,这个操作的复杂度为O(N). 和 C 字符串不同, 因为 SDS 在 len 属性 中记录了 SDS 本身的长度,所以获取一个 SDS 长度的复杂度仅为O(1).

通过使用SDS而不是C字符串,Redis将获取字符串长度所需的复杂度从O(N)降到了O(1),这确保获取字符串长度的工作不会成为Redis的性能瓶颈。


  • 杜绝缓冲区溢出

        除了获取字符串长度复杂度高之外, C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。例如在执行<string.h> / strcat 函数:char *strcat(char *dest, const char *src) 时, 若没有为dest分配足够多的内存, 就会产生缓冲区溢出。例如有如下在内存中紧邻着的 C 字符串 S1 和 S2, 如果执行 strcat(s1, " Cluster");

        原意是将 s1 的内容修改为 "Redis Cluster", 但由于没有给 S1 分配足够的空间, 导致 S2 的内容被意外的修改,如下图,即S1的内容溢出到S2所在的位置上。


        与 C 字符串不同, SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性。当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求, 如果不满足,API 会自动将 SDS 的空间扩展至执行修改所需要的大小,然后才执行实际的修改操作。


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

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

        如果是增长字符串的操作,程序需要先通过内存重分配来扩展底层数组的空间大小,如果忘了这一步就会出现如上所说的缓冲区溢出。

        如果是缩短字符串的操作,程序需要通过内存重分配来释放字符串不再使用的那部分空间,如果忘了这一步就会产生内存泄漏。


        在一般程序中, 如果修改字符串长度的情况不太长出现,那么每次修改都执行一次内存重分配是可以接受的。

        但是 Redis 作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合。如果每次修改字符串长度都要执行一次内存重分配的话,可能对性能造成影响。


为了避免 C 字符串这种缺陷, SDS 通过未使用空间解除了字符串长度和地产数组长度之间的关联:在 SDS 中, buf 数组可以包含未使用的字节, 由 SDS 的 free 属性记录。通过未使用空间, SDS 实现了 空间预分配惰性空间释放 两种优化策略。

        1、空间预分配

空间预分配用于优化 SDS 的字符串增长操作:当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所需要的空间,还会为 SDS 分配额外的未使用空间。修改后 SDS(len 属性) 的长度小于 1M 时, free值设置和len值相同, 大于 1M 时,free 值设置为 1M.

        2、惰性空间释放

当 SDS 的 API 需要缩短 SDS保存的字符串时,程序并不立即使用内存重分配来回收多出来的字节,而是使用 free 属性将这些字节的数量记录起来并等待将来使用。于此同时,SDS 也提供了相应的 API,让我们在需要的时候真正的释放 SDS 未使用空间,所以不用担心惰性空间释放策略会造成内存浪费。


  • 二进制安全

        C 字符串的字符必须符合某种编码(比如 ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则会被误认为字符串结尾。这使得 C 字符串只能保存文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制文件。例如如下字符串,C 字符串只会识别到 Redis, 而忽略后面的内容。

        虽然数据库一般用于保存文本数据,但也有一些情况是需要数据库来保存二进制数据的。因此,为了确保 Redis 可以适用于各种不同的使用场景, SDS 的 API 都是二进制安全的。由于 SDS 使用 len 值记录长度,所以不会造成遇见空字符提前结束的情况。


  • 兼容部分 C 字符串函数

        虽然 SDS 的 API 都是二进制安全的, 但他们一样遵循 C 字符串以空字符为结尾的惯例;这是为了让那些保存文本数据的 SDS 可以重用一部分 <string.h> 库定义的函数。这样 Redis 就不用重复编写 SDS 的相关函数了。


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

  • 相关指令
127.0.0.1:6379> set name "zhangsan"   // 设置值, 键值对存储, name 为键, zhangsan 为值, 均为 SDS 类型
OK
127.0.0.1:6379> get name   // 获得值
"zhangsan"
127.0.0.1:6379> exists name  // 判断某一个 值 是否存在
(integer) 1
127.0.0.1:6379> append name "!" // 追加某一个值
(integer) 9
127.0.0.1:6379> get name
"zhangsan!"
127.0.0.1:6379> strlen name // 获得某个值的长度
(integer) 9
127.0.0.1:6379> keys * // 获得当前所有的 键
1) "name"
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views  // 自增 1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views // 自减 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incrby views 10 // 增加任意值
(integer) 11
127.0.0.1:6379> get views
"11"
127.0.0.1:6379> decrby views 10 // 减少任意值
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> set key "hello, linux"
OK
127.0.0.1:6379> get key
"hello, linux"
127.0.0.1:6379> getrange key 0 5  // 截取字符串 闭区间
"hello,"
127.0.0.1:6379> getrange key 0 -1 // 获得全部的字符串
"hello, linux"
127.0.0.1:6379> setrange key 7 unix  // 修改指定偏移量位置的值
(integer) 12
127.0.0.1:6379> get key
"hello, unixx"
127.0.0.1:6379> setex key1 10 10sdead  //设置过期时间
OK
127.0.0.1:6379> ttl key1
(integer) -2
127.0.0.1:6379> setnx key1 "hello"  // 如果不存在 再设置
(integer) 1
127.0.0.1:6379> setnx key1 "hello"
(integer) 0
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 // 同时设置多个值
OK
127.0.0.1:6379> mget k1 k2 k3  // 同时获得多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k4 v4 k1 v1 // msetnx 是个原子性的操作
(integer) 0
127.0.0.1:6379> get k4
(nil)
127.0.0.1:6379> set name zhangsan
OK
127.0.0.1:6379> getset name lisi  // 先 get  再 set, 若不存在, 返回nil
"zhangsan"
127.0.0.1:6379> get name
"lisi"

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值