楔子
下面来解密 Redis 的字符串,整篇文章分为三个部分。
- Redis 字符串的相关命令;
- Redis 字符串的应用场景;
- Redis 字符串的实现原理;
Redis 字符串的相关命令
先来看看字符串的相关命令,我们首先要学会如何使用它。
set key value:给指定的 key 设置 value
127.0.0.1:6379> set name satori OK # 如果字符串之间有空格,可以使用双引号包起来 127.0.0.1:6379> set name "kemeiji satori" OK 127.0.0.1:6379>
对同一个 key 多次设置 value 相当于更新,会保留最后一次设置的 value。设置成功之后会返回一个 OK,表示设置成功。除此之外,set 还可以指定一些可选参数。
- set key value ex 60:设置的时候指定过期时间为 60 秒,等价于 setex key 60 value
- set key value px 60:设置的时候指定过期时间为 60 毫秒,等价于 psetex key 60 value
- set key value nx:只有 key 不存在的时候才会设置,存在的话会设置失败,而如果不加 nx 则会覆盖,等价于 setnx key value
- set key value xx:只有 key 存在的时候才会设置,不存在的话会设置失败。注意:没有 setxx key value
我们发现使用 set 已经足够了,因此未来可能会移除 setex、psetex、setnx。另外我们可以对同一个 key 多次 set,相当于对原来的值进行了覆盖。
get key:获取指定 key 对应的 value
127.0.0.1:6379> get name "kemeiji satori" 127.0.0.1:6379> get age (nil) 127.0.0.1:6379>
如果 key 不存在,则返回 nil,也就是空。存在的话,则返回 key 对应的 value。
del key1 key2 ···:删除指定 key,可以同时删除多个
127.0.0.1:6379> set age 16 OK # 虽然设置的是一个数值 # 但在 Redis 中都是字符串格式 127.0.0.1:6379> get age "16" 127.0.0.1:6379> get name "kemeiji satori" # 会返回删除的 key 的个数,表示有效删除了两个 # 而 gender 不存在,所以无法删除一个不存在的 key 127.0.0.1:6379> del name age gender (integer) 2 127.0.0.1:6379> get name (nil) 127.0.0.1:6379> get age (nil) 127.0.0.1:6379>
append key value:追加
如果 key 存在,那么会将 value 追加到 key 对应的值的末尾;如果不存在,那么会重新设置,等价于 set key value。
127.0.0.1:6379> set name komeiji OK 127.0.0.1:6379> get name "komeiji" # 追加一个字符串 # 并返回追加之后的字符串长度 127.0.0.1:6379> append name " satori" (integer) 14 127.0.0.1:6379> get name "komeiji satori" 127.0.0.1:6379> set age 1 OK 127.0.0.1:6379> get age "1" 127.0.0.1:6379> append age 6 (integer) 2 127.0.0.1:6379> get age "16" 127.0.0.1:6379>
strlen key:查看 key 对应的 value 的长度
127.0.0.1:6379> strlen name (integer) 14 127.0.0.1:6379> strlen age (integer) 2 127.0.0.1:6379> strlen not_exists (integer) 0 127.0.0.1:6379>
incr key:为 key 存储的值自增 1,必须可以转成整型,否则报错。如果不存在 key,默认先将该 key 的值设置为 0,然后自增 1
127.0.0.1:6379> get age "16" # 返回自增后的结果 127.0.0.1:6379> incr age (integer) 17 127.0.0.1:6379> get age "17" # 值不存在的话,会先设置为 0 # 然后再自增 1 127.0.0.1:6379> get length (nil) 127.0.0.1:6379> incr length (integer) 1 127.0.0.1:6379> get length "1" # name 无法转成整型 127.0.0.1:6379> incr name (error) ERR value is not an integer or out of range 127.0.0.1:6379>
decr key:为 key 存储的值自减 1,必须可以转成整型,否则报错。如果不存在 key,默认先设置该 key 值为 0,然后自减 1
127.0.0.1:6379> get age "17" 127.0.0.1:6379> decr age (integer) 16 127.0.0.1:6379> get age "16" 127.0.0.1:6379> 127.0.0.1:6379> get age2 (nil) 127.0.0.1:6379> decr age2 (integer) -1 127.0.0.1:6379> get age2 "-1" 127.0.0.1:6379>
incrby key number:为 key 存储的值自增 number,必须可以转成整型,否则报错,如果不存在的话,默认先将该值设置为 0,然后自增 number
127.0.0.1:6379> get age "16" 127.0.0.1:6379> incrby age 5 (integer) 21 127.0.0.1:6379> get age "21" 127.0.0.1:6379> incrby age -5 (integer) 16 127.0.0.1:6379> get age "16" 127.0.0.1:6379>
相信你已经猜到了,除了 incrby 之外还有 decrby,两者用法是一样的。如果number 为负数,那么 incrby 的效果等价于 decrby。
getrange key start end:获取指定范围的 value
注意:redis 的索引都是包含结尾的,不管是这里的 getrange,还是后续的列表操作,索引都是包含两端的。
127.0.0.1:6379> set name satori OK 127.0.0.1:6379> getrange name 0 -1 "satori" 127.0.0.1:6379> getrange name 0 4 "sator" 127.0.0.1:6379> getrange name -3 -1 "ori" 127.0.0.1:6379> getrange name -3 10086 "ori" 127.0.0.1:6379> getrange name -3 -4 "" 127.0.0.1:6379>
索引既可以从前往后数,也可以从后往前数。
setrange key start value:从索引为 start 的地方开始,将 key 对应的值替换为 value,替换的长度等于 value 的长度
127.0.0.1:6379> get name "satori" # 从索引为 0 的地方开始替换三个字符 # 并返回替换之后字符串的长度 127.0.0.1:6379> setrange name 0 SAT (integer) 6 127.0.0.1:6379> get name "SATori" # 从索引为 10 的地方开始替换 # 但是字符串索引最大为 6,因此会使用 \x00填充 127.0.0.1:6379> setrange name 10 ORI (integer) 13 127.0.0.1:6379> get name "SATori\x00\x00\x00\x00ORI" # 对于不存在的 key 也是如此 127.0.0.1:6379> setrange myself 3 gagaga (integer) 9 127.0.0.1:6379> get myself "\x00\x00\x00gagaga" 127.0.0.1:6379> set name satori OK # 替换的字符串长度没有限制,会自动扩充 127.0.0.1:6379> setrange name 0 'komeiji koishi' (integer) 14 127.0.0.1:6379> get name "komeiji koishi"
mset key1 value1 key2 value2:同时设置多个 key value
这是一个原子性操作,要么都设置成功,要么都设置不成功。注意:这些都是会覆盖原来的值的,如果不想这样的话,可以使用 msetnx,这个命令只会在所有的 key 都不存在的时候才会设置。
mget key1 key2:同时返回多个 key 对应的 value
如果有的 key 不存在,那么返回 nil。
127.0.0.1:6379> mset name koishi age 15 OK 127.0.0.1:6379> mget name age gender 1) "koishi" 2) "15" 3) (nil) 127.0.0.1:6379>
getset key value:先返回 key 的旧值,然后设置新值
127.0.0.1:6379> getset name satori "koishi" 127.0.0.1:6379> get name "satori" 127.0.0.1:6379> 127.0.0.1:6379> getset ping pong (nil) 127.0.0.1:6379> get ping "pong" 127.0.0.1:6379>
返回旧值的同时设置新值,如果 key 不存在,那么会返回 nil,然后设置。
另外,Redis 里面还有一些关于 key 的操作,这些操作不是专门针对 String 类型的,但是有必要提前说一下。
keys pattern:查看所有名称满足 pattern 的 key,至于 key 对应的 value 则可以是 Redis 的任意类型
# 查看所有的 key 127.0.0.1:6379> keys * 1) "hello" 2) "length" 3) "age2" 4) "ping" 5) "age" 6) "name" 7) "myself" # 查看包含 a 的 key 127.0.0.1:6379> keys *a* 1) "age2" 2) "age" 3) "name" # 查看以age开头、总共 4 个字符的key 127.0.0.1:6379> keys age? 1) "age2" 127.0.0.1:6379>
exists key:判断某个 key 是否存在
# 查看 key 是否存在 # 存在返回 1,不存在返回 0 127.0.0.1:6379> exists name (integer) 1 127.0.0.1:6379> exists names (integer) 0 # 也可以指定多个 key,返回存在的 key 的个数 # 但是此时无法判断到底是哪个 key 存在 127.0.0.1:6379> exists name name1 (integer) 1 127.0.0.1:6379>
ttl key:查看还有多少秒过期,-1 表示永不过期,-2 表示已过期
127.0.0.1:6379> ttl name (integer) -1 127.0.0.1:6379> ttl name2 (integer) -2 127.0.0.1:6379>
key 是可以设置过期时间的,如果过期了就不能再用了。但我们看到 name2 这个 key 压根就不存在,返回的也是 -2,因为过期了就相当于不存在了。而 name 是 -1,表示永不过期。
expire key 秒钟:为给定的 key 设置过期时间
# 设置 60s,设置成功返回 1 127.0.0.1:6379> expire name 60 (integer) 1 # 查看时间,还剩下 55 秒 127.0.0.1:6379> ttl name (integer) 55 # NAME 不存在,设置失败,返回 0 127.0.0.1:6379> expire NAME 60 (integer) 0 127.0.0.1:6379>
这里设置 60s 的过期时间,另外设置完之后,在过期时间结束之前是可以再次设置的。比如我先设置了 60s,然后快结束的时候我再次设置 60s,那么还会再持续 60s。
type key:查看你的 key 是什么类型
# name过期了,相当于不存在了 # 因此为 none 127.0.0.1:6379> type name none # 类型为 string 127.0.0.1:6379> type age string 127.0.0.1:6379>
move key db:将 key 移动到指定的 db 中
# 清空当前库,将所有 key 都删除 # 如果是清空所有库,可以使用 flushall # 当然后面都可以加上 async,表示异步删除 127.0.0.1:6379> flushdb OK 127.0.0.1:6379> set name satori OK 127.0.0.1:6379> keys * 1) "name" # 将 name 移动到索引为3的库中 127.0.0.1:6379> move name 3 (integer) 1 # 当前库已经没有 name 这个 key 了 127.0.0.1:6379> keys * (empty array) # 切换到索引为 3 的库中 127.0.0.1:6379> select 3 OK # keys * 查看,发现 name 已经有了 127.0.0.1:6379[3]> keys * 1) "name" # 切换回来 127.0.0.1:6379[3]> select 0 OK 127.0.0.1:6379>
Redis 字符串的应用场景
讨论完字符串的相关命令之后,我们还要明白字符串要用在什么地方。
1)页面数据缓存
我们知道,一个系统最宝贵的资源就是数据库资源,随着公司业务的发展壮大,数据库的存储量也会越来越大,并且要处理的请求也越来越多。然而当数据量和并发量到达一定级别之后,数据库就变成了拖慢系统运行的 “罪魁祸首”。
为了避免这种情况发生,我们可以把查询结果放入缓存(Redis)中,让下次同样的查询直接去缓存系统取结果,而非查询数据库,这样既减少了数据库的压力,同时也提高了程序的运行速度。
这也是 Redis 用途最广泛的地方。
2)数据计算与统计
Redis 可以用来存储整数和浮点数类型的数据,并且可以通过命令直接累加并存储整数信息,这样就省去了每次先要取数据、转换数据、运算、再存入数据的麻烦,只需要使用一个命令就可以完成此流程。比如:微博、哔哩哔哩等社交平台,我们经常会点赞,然后还有点赞数。每点一个赞,点赞数就加 1,这个功能就完全可以交给 Redis 实现。
3)共享 Session 信息
通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
例如用户 A 的 Session 信息存储在第一台服务器,但第二次访问时用户 A 的请求被分配到第二台服务器,这个时候该服务器并没有用户 A 的 Session 信息,就会出现需要重复登录的问题。
由于分布式系统每次会把请求随机分配到不同的服务器,因此我们需要借助缓存系统对这些 Session 信息进行统一的存储和管理。这样无论请求发送到哪台服务器,服务器都会去统一的缓存系统获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
虽然这也是 Redis 使用场景之一,只不过在现在的 web 开发中已经很少会使用共享 Session 的方式了。
Redis 字符串的实现原理
下面我们就来分析字符串的底层数据结构了,我们说键值对中的键是字符串类型,值有时也是字符串类型。
Redis 是用 C 语言实现的,但它没有直接使用 C 的字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是说 Redis 的 String 数据类型的底层数据结构是 SDS。
既然 Redis 设计了 SDS 结构来表示字符串,肯定是 C 语言的字符串存在一些缺陷。
C 字符串的缺陷
C 的字符串其实就是一个字符数组,即数组中每个元素都是字符串的一个字符,比如下图就是字符串 "koishi" 的字符数组结构:
s 只是一个指针,它指向了字符数组的起始位置,那么问题来了,C 要如何得知一个字符数组的长度呢?于是 C 会默认在每一个字符数组后面加上一个 \0,来表示字符串的结束。
因此 C 语言标准库中的字符串函数就是通过判断字符是不是 \0 来决定要不要停止操作,如果当前字符不是 \0 ,说明字符串还没结束,可以继续操作;如果当前字符是 \0,则说明字符串结束了,就要停止操作。
举个例子,C 语言获取字符串长度的函数 strlen,就是通过遍历字符数组中的每一个字符,并进行计数。当遇到字符 \0 时停止遍历,然后返回已经统计到的字符个数,即为字符串长度。下图显示了 strlen 函数的执行流程:
如果用代码实现的话:
#include <stdio.h> size_t strlen(const char *s) { size_t count = 0; while (*s++ != '\0') count++; return count; } int main() { printf("%lu\n", strlen("koishi")); // 6 }
很明显,C 语言获取字符串长度的时间复杂度是 O(N),并且使用 \0 作为字符串结尾标记有一个缺陷。如果某个字符串中间恰好有一个 \0,那么这个字符串就会提前结束,举个例子:
#include <stdio.h> #include <string.h> int main() { // 字符串相关操作函数位于标准库 string.h 中 printf("%lu\n", strlen("abcdefg")); // 7 printf("%lu\n", strlen("abc\0efg")); // 3 }
所以在 C 中 \0 为字符串是否结束的标准,因此如果使用 C 的字符数组,只能让 C 在字符串结尾自动帮你加上 \0,我们创建的字符串内部是不可以包含 \0 的,否则就会出问题,因为字符串会提前结束。这个限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频之类的二进制数据。
另外 C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。举个例子,strcat 函数可以将两个字符串拼接在一起。
#include <stdio.h> #include <string.h> //将 src 字符串拼接到 dest 字符串后面 //char *strcat(char *dest, const char* src); int main() { char buf[12] = "hello "; printf("%s\n", buf); // hello strcat(buf, "world"); printf("%s\n", buf); // hello world }
"hello world" 占 11 个字符,加上 \0 一共 12 个,buf 的长度也为 12,刚好能容纳的下。但如果我们将 buf 的长度改成 11,就会发生缓冲区溢出,可能造成程序终止。因此 C 语言的字符串不会记录自身的缓冲区大小,它假定我们在执行这个函数时,已经为 dest 分配了足够多的内存。
而且 strcat 函数和 strlen 函数类似,时间复杂度也是 O(N) 级别,也是需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,所以整个字符串的操作效率是不高的。
我们还是手动实现一下 strcat,看一看整个过程:
#include <stdio.h> char *strcat(char *dest, const char *src) { char *head = dest; // 遍历字符串,直到结尾 while (*head != '\0') head++; // 循环结束之后,head 停在了 \0 的位置 // 然后将 src 对应的字符数组中的字符逐个拷贝过去 while ((*head++ = *src++) != '\0'); // 最后返回 dest return dest; } int main() { char buf[12] = "hello "; printf("%s\n", buf); // hello strcat(buf, "world"); printf("%s\n", buf); // hello world }
好了, 通过以上的分析,我们可以得知 C 字符串的不足之处以及可以改进的地方。
- 获取字符串长度的时间复杂度为 O(N);
- 字符串的结尾是以 \0 作为字符标识,使得字符串里面不能含有 \0 字符,因此不能保存二进制数据;
- 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止;
而 Redis 实现的 SDS 结构体就把上面这些问题解决了,接下来我们一起看看 Redis 是如何解决的。
SDS 的定义
先来看一看 SDS 长什么样子?
解释一下里面的每个成员变量代表什么含义:
- len:记录了字符串的长度,这样后续在获取的时候只需返回这个成员变量的值即可,时间复杂度为 O(1)。
- alloc:分配给字符数组的空间长度,这样后续对字符串进行修改时(比如追加一个字符串),可以通过 alloc 减去 len 计算出剩余空间大小,来判断空间是否满足修改需求。如果不满足,就会自动将 SDS 内的 buf 进行扩容(所谓扩容就是申请一个更大的 buf,然后将原来 buf 的内容拷贝到新的 buf 中,最后将原来的 buf 给释放掉),再执行修改操作。通过这种方式就避免了缓冲区溢出的问题,而且事先可以申请一个较大的 buf,避免每次追加的时候都进行扩容。
- flags:用来表示不同类型的 SDS,SDS总共有 5 种,分别是 sdshdr5, sdshdr8, sdshdr16, sdshdr32 和 sdshdr64,后面说明它们之间的区别。所以 SDS 只是一个概念,它并不是真实存在的结构体,而 sdshdr5, sdshdr8, sdshdr16, sdshdr32 和 sdshdr64 才是底层定义好的结构体,相当于 SDS 的具体实现,当然它们都可以叫做 SDS。
- buf[]:字符数组,用来保存实际数据,不仅可以保存字符串,也可以保存二进制数据。之所以可以保存二进制数据是因为在计算字符串长度的时候不再以 \0 为依据,因为 SDS 中的 len 字段在时刻维护字符串的长度。
总的来说,Redis 的 SDS 在原本的字符数组之上,增加了三个元数据:len, alloc, flags,用来解决 C 语言字符串的缺陷。
SDS 是怎么解决 C 字符串缺陷的
我们说 C 字符串有一系列缺陷,而 SDS 将它们全解决了,那么是如何解决的呢?
1)O(1) 时间复杂度获取字符串长度
因为 C 字符串并不记录自身的长度信息,所以为了获取一个 C 字符串的长度,程序必须遍历整个字符串,对遇到的每个字符进行计数,直到遇到代表字符串结尾的 \0 为止,这个操作的复杂度为 O(N)。
而 Redis 的 SDS 因为加入了 len 成员变量,会时刻维护字符串的长度,所以获取字符串长度的时候,直接返回该成员变量的值就行,因此复杂度只有 O(1)。
2)避免缓冲区溢出
C 字符串不记录自身的长度,所以在使用 strcat 函数追加的时候,会假定已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容。然而一旦这个假定不成立时,就会产生缓冲区溢出。
与 C 字符串不同,SDS 的空间分配策略完全避免了发生缓冲区溢出的可能性。当 SDS API 需要对 SDS 进行修改时,会先检查 SDS 的空间是否满足修改所需的要求。如果不满足的话,API 会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作。所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题。
3)减少修改字符串时带来的内存重分配次数
Redis 在修改 SDS 时,会面临「申请、释放」内存的开销,所以 Redis 做了如下优化。
优化一:当判断出缓冲区剩余大小(alloc - len)不够用时,Redis 会自动扩大 SDS 的空间大小,以满足修改所需的大小。当然准确的说,扩容的是 SDS 内部的 buf 数组,扩容规则是:当小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容。
并且在扩展 SDS 空间的时候,API 不仅会为 SDS 分配修改所需要的空间,还会给 SDS 分配额外的「未使用空间」。这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,从而有效地减少内存分配次数。
优化二:多余内存不释放,SDS 缩容的时候不释放多余的内存,下次可直接复用这些内存。
4)二进制安全
C 字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含 \0。否则最先被程序读入的 \0 将被误认为是字符串结尾,这些限制使得 C 字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。
但 SDS 不需要用 \0 字符来标识字符串结尾,而是有个专门的 len 成员变量来记录长度,所以可存储包含 \0 的数据。但是 SDS 为了兼容部分 C 语言标准库的函数,还是会在结尾加上 \0 字符。注意:我们说 alloc 成员维护的是 buf[] 数组的长度,但是这个长度不包括结尾的 \0,比如 alloc 为 10,但 buf[] 的长度其实是 11。
因此 SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候是什么样的,它被读取时就是什么样的。通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。
5)节省内存空间
SDS 结构中有个 flags 成员变量,表示的是 SDS 类型,Redis 一共设计了 5 种 SDS。而这 5 种的主要区别就在于,它们的 len 和 alloc 成员变量的数据类型不同。以 sdshdr16 和 sdshdr32 为例:
可以看到 sdshdr16 的 len 和 alloc 的数据类型都是 uint16_t,表示字符数组长度和分配空间大小不能超过 2 的 16 次方;sdshdr32 则都是 uint32_t,表示字符数组长度和分配空间大小不能超过 2 的 32 次方。
之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如在保存小字符串时,len、alloc 这些元数据的占用空间也会比较少。像我们保存一个 5 字节的字符串,完全没有必要使用 uint64_t,否则 len 和 alloc 加起来就 16 字节了,这显然是得不偿失的。
除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在定义 struct 时声明了 __attribute__ ((packed)),它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。
内存对齐是为了减少数据存储和读取的工作量,现在的 64 位处理器默认按照 8 字节进行对齐。所以相同的结构体,如果字段顺序不同,那么占用的大小也不一样,我们举个例子:
#include <stdio.h> typedef struct { int a; long b; char c; } S1; typedef struct { long a; int b; char c; } S2; int main() { printf("%u %u\n", sizeof(S1), sizeof(S2)); // 24 16 }
两个结构体的内部都是 3 个成员,类型为 int, long, char,但因为顺序不同导致整个结构体的大小不同,这就是内存对齐导致的。
关于内存对齐的具体细节这里不再赘述,总之它的核心就是:虽然现代计算机的内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问都可以从任意地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们默认会要求这些数据的首地址的值是 8 的倍数(64 位机器),这就是所谓的内存对齐。
我们在 C 中可以指定对齐的字节数,比如 #pragma pack(4) 表示按照 4 字节对齐。当然啦,还可以像 Redis 那样在声明结构体的时候指定 __attribute__ ((packed)) 来禁止内存对齐,此时结构体中的字段都是紧密排列的,不会出现空洞。
#include <stdio.h> typedef struct { int a; long b; char c; } S1; typedef struct { long a; int b; char c; } S2; typedef struct __attribute__ ((packed)) { long a; int b; char c; } S3; int main() { printf("%u %u %u\n", sizeof(S1), sizeof(S2), sizeof(S3) ); // 24 16 13 }
我们看到在禁止内存对齐之后,结构体占 13 个字节,就是每个成员的大小相加。
小结
以上我们就从命令操作、应用场景、实现原理三个角度介绍了 Redis 字符串,命令操作比较简单,网上资料一大堆,应用场景也很简单,重点是它的实现原理。
我们要清楚为什么 Redis 自己定义 SDS 来表示字符串,而不使用 C 的字符数组,原因是 C 的字符数组有很大的不足。关于 SDS 和 C 字符数组之间的差别,再总结一下:
以上就是 Redis 字符串的内容,因为介绍了相关命令操作(加上代码演示),所以内容稍微有点多。如果你对 Redis 的命令操作已经很熟悉了,那么这部分也可以不用看。
最后,SDS 字符串在 Redis 内部也被大量使用,比如 :
- Redis 的所有 key 都是字符串,可以查看 src/db.c 的 dbAdd 函数;
- Redis 服务端在读取 Client 发来的请求时,会先读到一个缓冲区中,这个缓冲区也是字符串,可以查看 src/server.h 中 struct client 的 querybuf 字段;
- 写操作追加到 AOF 时,会先写到 AOF 缓冲区,这个缓冲区也是字符串,可以查看 src/server.h 中 struct client 的 aof_buf 字段;
本文参考自:
- 极客时间蒋德钧:《Redis 源码剖析与实战》
- 微信读书:《Redis 设计与实现》
- 小林 coding:《图解 Redis》
- 课代表 kaito 在极客时间《Redis 源码剖析与实战》评论区的精彩回答