- String的主要实现
- 内部编码模式
- 何为简单动态字符串
- SDS的底层实现
- C语言字符串和SDS的区别, SDS的优点有哪些?
- 常数获取字符串长度
- 避免缓冲区溢出,通过对数组的扩增实现空间增大
- 合理的资源分配,减少内存分配的次数
- 扩增时的空间预分配
- 缩短时的惰性空间释放
- 二进制安全
- 总结一下
- String类型在Redis中的常用指令及业务场景使用
- 常用字符串操作
- 计数操作
- 过期指令
- 判断插入
String的主要实现
String是Redis中最基础的key-Value结构,主要有两种实现方法,分别Int
类型和简单动态字符串类型。
Int
类型很好理解,其实现方式如下图所示
通过将encoding设置为Int模式实现,并通过ptr指向对应的数据实现整数的存储。这里数字则是直接使用C语言的整数或浮点类型。
当Redis存储字符串类型时,并没有直接使用C字符串作为存储的结构,而是设计了一种叫做简单动态字符串的结构体来增强Redis在操作字符串类型的性能,一般来说存储字符串时,Redis的String构造将如下图所示:
可以看到ptr将指向一个sdshdr的结构体,接下来我们将详细讲述一下这个sdshdr是一个什么结构。
内部编码模式
字符串对象的内部编码(encoding)有 3 种 :int、raw和 embstr。
- int:字符串对象保存的是整数值,并且这个整数值可以用long类型来表示
- embstr:符串对象保存的是一个字符串,并且这个字符申的长度小于等于 32 字节
- raw:字符串对象保存的是一个字符串,并且这个字符串的长度大于 32 字节
何为简单动态字符串
通常在一些Redis相关技术的文章会提及,如小林coding Redis图解,是Redis的一个入门知识点,英文全称为:simple dynamic string,SDS。作为Redis的String类型的底层结构的一种实现。
在实践中很容易使用到SDS的数据结构,基本上是第一次学习Redis时第一次接触的结构。
举个例子,如果客户端执行如下代码
127.0.0.1:6379> SET sds "hello"
OK
此时Redis将在数据库创建一个键值对,这键值对中字符串对象,底层都是使用SDS结构实现的。
当然,除了这些基础的字符串值的存储使用SDS实现,Redis中数据持久化AOF中的缓冲区模块也会使用SDS结构,所以深入理解SDS的结构实现对之后的学习也会有很大的帮助。
SDS的底层实现
Redis的底层实现是由C语言实现的,Redis的String类型并没有直接使用C语言的字符串模式,而是构建了一个新的结构模式。在sds.h中通过构建struct实现SDS结构
/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
其中各个数字分别表示不同的位数,简单提炼一下重复的部分
struct sdshdr {// 当前SDS的字符串长度uint len;// buf中剩余的空间uint alloc;// 字节数组,用于保存字符串char buf[ ];
}
对于上述结构,可使用下图来演示输入为'hello'情况下的结构内容。
C语言字符串和SDS的区别, SDS的优点有哪些?
为什么不直接使用C语言字符串,而是自己又构建了一个结构体去解决一些场景呢?大概想一下也比较容易想到,因为C语言的字符串不能满足Redis对字符串的使用要求,这些要求就如下列出。
常数获取字符串长度
C字符串不会记录自身的长度信息,所以每次读取长度时都需要遍历整个字符串,这样的操作在Redis这种对性能利用较高的数据库,会浪费一定的计算资源。所以为了实现更高的运行,Redis使用空间换时间的思想,在sdshdr
结构中记录了当前字符串的长度,此时获取一个SDS的字符串的长度的时间为O(1)。还是Hello 字符串的例子:
127.0.0.1:6379> SET sds "hello"
OK
127.0.0.1:6379> STRLEN sds
(integer) 5
执行上述命令会先保存键值对,STRLEN
表示Redis中对应的字符串的长度,其获取字符串的流程大致如下。
这种用空间换时间的好处可以确保获取字符串长度的命令不会成为Redis的性能瓶颈,使Redis可以把性能更好的利用到其他功能上,所以可以很容易的知道STRLEN
命令的时间复杂度为O(1)。
避免缓冲区溢出,通过对数组的扩增实现空间增大
关于缓冲区溢出的问题,我相信大家在学习C语言的时候肯定会注意到这个问题,在一些C语言习题以strcat
库函数中做坑来考察大家。为了避免出现像这样的情况,Redis通过对应的alloc
变量记录当前剩余缓冲空间,在执行拼接等操作时会先进行扩容,之后才进行拼接。
合理的资源分配,减少内存分配的次数
内存分配主要出现在两种类型的操作
- 增长字符串的操作:如拼接操作
- 缩短字符串的操作:如截断操作
Redis如何判断是否需要进行内存分配的呢?主要是根据alloc
的属性记录来判断是否进行扩充。下图表示在增加 world
字符串时,Redis进行内存分配的情况。
那么出现需要进行空间分配操作的命令时,内部是怎么进行这个内存分配的呢?
扩增时的空间预分配
即是在执行扩展的API时,不仅仅会扩展到对应的内存大小,还会额外的申请空间。并且根据实际的生产情况,按照小内存分配和大内存分配设置了两种情况。
- 扩增后内存小于1MB的情况:程序将分配和
len
属性相同大小的缓冲空间。 - 大于等于1MB的情况:将直接分配1MB大小的缓冲空间
两者操作的统一目标都是为了提前分配缓冲空间,不同之处在于怎么根据当前字符串使用情况按需分配。
通过这种分配策略可以,可以将连续N次增长的字符串的扩充次数,转变为最多N次的扩充次数,降低扩容所消耗的时间
缩短时的惰性空间释放
很容易理解,即在删除对应的数据时,不缩短空间,以保证后续的追加不会出现再进行分配的操作。当然为了防止空间的浪费,Redis4.0版本后提供了SHRINK
方法来采用异步进程来执行实际的SDS内存回收。
二进制安全
与C字符串不同的是,C字符串会进行转译读写,同样的情形会在SDS中的buf中则以单个字节并以数组的形式保存,并不会出现这样的转译影响。实现读写一致,所以说SDS是二进制安全的
总结一下
综上所述可以看到C字符串和SDS的区别,总结如下表所示
差异部分 | C字符串 | SDS |
---|---|---|
字符串长度获取时间复杂度 | O(n) | O(1) |
是否会出现缓冲溢出 | 会 | 不会 |
修改字符串长度N次需要执行的分配次数 | 必然N次 | 最多N次 |
可以保存什么数据 | 只能保存文本数据 | 可以保存文本和二进制数据 |
可以使用<String.h>中的库函数吗? | 全部可用 | 部分可用 |
String类型在Redis中的常用指令及业务场景使用
常用字符串操作
在业务中有两种对象缓存方式
- 缓存整个json,最后取出在代码中解码后使用
- 直接使用MSET存储,并用MGET范围获取值
# 设置String key-value 值
127.0.0.1:6379> SET name xiaohei
OK
# 根据Key 获取对应的value
127.0.0.1:6379> GET name
"xiaohei"
# 判断当前key是否存在,存在返回1,不存在返回0
127.0.0.1:6379> EXISTS name
(integer) 1
# 获取当前Key中Value字符串串的长度
127.0.0.1:6379> STRLEN name
(integer) 7
# 删除当前key-value值
127.0.0.1:6379> DEL name
# 批量创建Key-value
127.0.0.1:6379> MSET name xiaohei age 20
OK
# 批量获取
127.0.0.1:6379> MGET name age
1) "xiaohei"
2) "20"
(integer) 1
计数操作
由于Redis的单线程性,所以非常适合进行计数场景的统计,比如计算访问次数、点赞、转发、库存数量等操作。
# 设置数字
127.0.0.1:6379> SET cnt 0
OK
# 自加1
127.0.0.1:6379> INCR cnt
(integer) 1
# 加 9
127.0.0.1:6379> INCRBY cnt 9
(integer) 10
# 自减1
127.0.0.1:6379> DECR cnt
(integer) 9
# 减9
127.0.0.1:6379> DECRBY cnt 9
(integer) 0
过期指令
对于任何需要过期处理的场景,使用Redis可以有效缓解计时对系统的性能损耗,此外在分布式锁的实现中加入过期时间可以避免异常情况下无法释放锁。
# 设置key-value
127.0.0.1:6379> SET name xiaohei
OK
# 设置过期时间
127.0.0.1:6379> EXPIRE name 10
(integer) 1
# 查看存活周期 大于0时表示剩余时间, -1表示永久, -2 表示不存在
127.0.0.1:6379> TTL name
(integer) 3
127.0.0.1:6379> TTL name
(integer) -2# 两种直接创建并设置时间的操作
127.0.0.1:6379> SET name xiaohei EX 10
OK
127.0.0.1:6379> SETEX name 10 xiaohei
OK
判断插入
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁
# 判断当前key 是否存在,存在则插入并返回1,否则返回0
127.0.0.1:6379> SETNX name xiaohei
(integer) 1
127.0.0.1:6379> SETNX name xiaohei2
(integer) 0127.0.0.1:6379> DEL name
(integer) 0
# NX判断是否存在、PX表示过期时间
127.0.0.1:6379> SET name xiaohei NX PX 10
OK
本文是经过个人查阅相关资料后理解的提炼,可能存在理论上的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!