通俗易懂Redis - String数据类型详解

146 阅读8分钟

1、前言

欢迎大家来到通俗易懂Redis,先从redis的基础数据结构说起,给大家简单的进行剖析。Redis有5种基础数据类型,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。Redis 底层的数据结构一共有6种,如下图第二排部分,它和数据类型对应关系也如下图: 未命名文件 (3).png 今天主要给大家分享我对String数据类型的一点见解,有错误的地方请大家指正。

2、String (字符串)- 介绍

String字符串我们在日常的开发的中应该会比较频繁的使用,它的键值对中的key是字符串,value也是字符串。它的作用非常的广泛,比如说缓存场景,我们将热点信息结构体使用JSON序列化成字符串,然后将序列化后的字符串塞进Redis来缓存,通过缓存来提高的读取速度,减少数据库的压力;分布式锁场景,我们通常设计分布式锁来防止分布式系统之间同步访问共享资源,彼此之间的干扰,以保证一致性,通常分布式锁也是通过redis的String来实现的。

3、String (字符串)- 基本使用

3.1、添加/获取字符串
//设置key为name,value=tsyd(格式:set <key> <value>)
> set name tsyd 
OK 
//获取key为name的value值(格式:get <key>)
> get name 
"tsyd"
3.2、是否存在/删除字符串
//查看redis中是否存在key为name的数据(格式:exists <key>)
> exists name 
(integer) 1 
//删除key为name的值(格式:del <key>)
> del name 
(integer) 1 
//删除后再次获取,返回空
> get name 
(nil)
3.3、批量操作
//批量设置(格式:mset <key1> <value1> <key2> <value2>....)
> mset name1 animal name2 dog name3 cat 
//批量获取(格式:mset:mget <key1> <key2> <key3>...)
> mget name1 name2 name3 # 返回一个列表
1) "animal" 
2) "dog" 
3) "cat"

批量对多个字符串进行读写,可以节省网络耗时开销。

3.4、过期删除操作
> set name test 
> get name 
"test" 
//使用expire设置过期时间
//设置5s 后过期(格式 expire <key> <time>)
> expire name 5 
//...wait for 5s 
> get name (nil)

//使用setex设置过期时间
//设置5s 后过期,等价于 set+expire (格式:setex <key> <time> <value>)
> setex name 5 test 
> get name 
"test" 
//... wait for 5s 
> get name 
(nil)
3.5、设置前判断
//如果 name 不存在就执行 set 创建 (格式:setnx <key> <value>)
> setnx name test 
(integer) 1 
> get name 
"test" 
//在name存在的情况下再次设置
> setnx name test1 
(integer) 0 # 因为 name 已经存在,所以 set 创建不成功 
> get name 
"test" # 没有改变
3.6、计数操作
> set number 100
OK 
//incr计数默认自增1(格式:incr <key>)
> incr number 
(integer) 101 
//incr计数设置增长值(格式:incr <key> <number>)
> incrby number 5 
(integer) 106 
//incr计数设置增长值为负数,则减少操作(格式:incr <key> <number>)
> incrby number -5 
(integer) 101 
3.7、子串操作
> set name "hello world"
OK 
//通过setrange操作子串(格式:setrange <key> <offset> <substr>)
> setrange name 6 redis
(integer) 11 
> get name
"hello redis"

//追加子串(格式:append <key> <substr>)
> append name test
(integer) 14
> get name
"hello redistest"

4、String (字符串)- 原理SDS

Redis是用C语言实现的,但是它没有直接使用C语言的char*字符数组来实现字符串,而是自己封装了动态字符串(SDS)类型,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。

4.1、为什么封装SDS

需要讨一下,为什么不使用C语言原生的字符数组,而是自己封装一套SDS?既然Redis设计了SDS结构来表示字符串,肯定是为了改进C语言的字符数组存在一些缺陷。首先看一下具体的缺陷以及Redis针对缺陷的解决方案:

缺陷一:

C语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符,就像如下结构:

未命名文件.png 这是字符串“ainmal”的字符数组结构,大家会不会好奇为什么最后一个字符是“\0”?在C语言里,对字符串操作时,char*指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束。因此,C语言标准库中字符串的操作函数,就通过判断字符是不是“\0”,如果不是说明字符串还没结束,可以继续操作,如果是则说明字符串结束了,停止操作。因此,如果要获取字符串的长度,需要从头遍历到“\0”,时间复杂度妥妥的是o(n),如果字符串长度过长,那就非常消耗时间了。

缺陷二:

C 语言的字符串是不会记录自身的缓冲区大小的,这就意味着在开发过程中很容易发生缓冲区溢出将可能会造成程序运行终止

缺陷三:

从上面我们知道C语言字符串是以“\0”为结束符的,假设我们字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,不能获取到完整的字符串,这明显不能保证二进制安全,还有一个限制就是用char*字符串中的字符必须符合某种编码(比如ASCII)。所以综合这些,C语言的字符串只能保存文本数据,不能保存像图片这样的二进制数据,非二进制安全

现在回答上面那个问题,为什么redis要封装成自己的SDS,就是为了解决上面这些问题。

4.2、SDS结构

我们以redis6.*为例,看一下SDS具体的结构设计,虽然不同版本的redis,SDS结构可能会有差异,但是都大同小异。先看一下一个整体的结构:

未命名文件 (1).png

  • len: SDS 所保存的字符串长度。我们获取字符串长度,可以直接读取这个变量,时间复杂度只需要O(1)。这就解决了上面提到的c语言字符串缺陷一,不再需要O(n)的时间复杂度就可以获取长度。
  • alloc: 分配给字符数组的空间长度。有了这个变量,在修改字符串的时候,先通过alloc - len计算出剩余的空间大小,如果剩余空间满足修改需求,则直接修改,如果不满足,就会先自动扩容,然后才执行实际的修改操作,所以SDS通过alloc可以有效缓冲区溢出的问题。这也解决了上面提到的c语言字符串缺陷二。
  • flags: SDS 类型,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
  • buf[]: 字节数组,用来保存实际数据。不需要用 “\0” 字符来标识字符串结尾了,而是直接将其作为二进制数据处理,这样设计既可以保存文本数据,也可以保存二进制数据。这也解决了上面提到的c语言字符串缺陷三。

4.3、SDS优势

经过上面分析,我们通过SDS和c语言字符串的对比,来简单介绍一下SDS的优势

优势一:O(1)时间复杂度获取字符串长度

C语言字符串不记录自身的长度,获取字符串的长度必须遍历整个字符串,以“\0”为结束符,整个操作的时间复杂度为O(N)。而我们使用SDS封装字符串则直接获取len属性值即可,时间复杂度为O(1)。

未命名文件 (2).png

优势二:二进制安全,不局限于只保存文本数据

C语言字符串中的字符除了末尾字符为'\0'外其他字符不能为空字符,并且如果字符串中有'\0'的话也会被认为是是字符串结尾。这限制了C字符串只能保存文本数据,而不能保存二进制数据。而SDS中不再以'\0'为结束符,不会出现C语言字符串中的问题,这样设计既可以保存文本数据,也可以保存二进制数据。注*在SDS中使用len属性的值判断字符串是否结束。

优势三:有效的防止缓冲区溢出,减少内存分配次数

C语言的字符串是不会记录自身的缓冲区大小,所以很容易发生缓冲区溢出。而SDS本身的alloc属性记录了已经分配的空间长度,再配合自动扩容机制,可以有效的防止缓冲区溢出。对SDS进行修改时,会先检查 SDS的空间是否满足修改所需的要求,如果不满足,会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。在扩展SDS空间之前,SDS API会优先检查未使用空间是否足够,如果不够话,API不仅会为SDS分配修改所必须的空间,还会给SDS分配额外的未使用空间。这样的好处是,下次再操作SDS时,如果SDS空间够的话,API就会直接使用未使用空间,而无须执行内存分配,有效的减少内存分配次数。

优势四:节省内存空间

SDS 结构中有个flags成员变量,表示的是 SDS 类型。Redos一共设计了5种类型,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同,之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。

以上就是今天全部的内容了,主要目的是让大家了解redis为什么自定义SDS类型,后续有新的知识点会继续补充!!!感谢大家!!!