Redis 是一个由 C 语言编写的高性能 key-value 存储系统。Redis 与其他 key-value 缓存产品(如 memcache)有以下几个特点。

  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
  • Redis 支持数据的备份,即 master-slave 模式的数据备份。

本文涉及源码为 Redis 3.0.0 版本号第 2 位为奇数:为非稳定版本(2.7、2.9、3.1)
版本号第 2 位为偶数:为稳定版本(2.6、2.8、3.0) http://download.redis.io/releases/

1. Redis 基础数据结构

1.1 Redis 的对象类型与编码类型

Redis 的数据类型:字符串、哈希、列表、集合、有序集合,这个在 redis 源码中能找到准确的定义:

  1. /* Object types */
  2. #define REDIS_STRING 0
  3. #define REDIS_LIST 1
  4. #define REDIS_SET 2
  5. #define REDIS_ZSET 3
  6. #define REDIS_HASH 4

其实这只是redis对外暴露的抽象结构,其底层实现要看其编码类型来决定使用该编码类型对应的数据结构。
如果一个对象类型只有一种底层数据结构的实现方式,那么这个编码类型就完全多余了,早期的 redis 的确没有这个概念。但后来为了优化性能,一种对象类型可能对应多种不同的编码实现。编码类型在 redis 源码中也有准确定义:

  1. /* Objects encoding. Some kind of objects like Strings and Hashes can be
  2. * internally represented in multiple ways. The 'encoding' field of the object
  3. * is set to one of this fields for this object. */
  4. #define OBJ_ENCODING_RAW 0 /* Raw representation */
  5. #define OBJ_ENCODING_INT 1 /* Encoded as integer */
  6. #define OBJ_ENCODING_HT 2 /* Encoded as hash table */
  7. #define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
  8. #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
  9. #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
  10. #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
  11. #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
  12. #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
  13. #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */

同一个对象类型,可以有不同的编码类型作为底层实现。而同一种编码类型,也可以支持上层的多种对象类型。他们的关系如下:
image.png
为什么一种对象类型要对应多种编码类型?为了提升性能。
object encoding xxx 这个 redis 命令来查看某一个 key 其 value 对象所使用的编码类型

  1. 127.0.0.1:6379> set number 100
  2. OK
  3. 127.0.0.1:6379> object encoding number
  4. "int"
  5. 127.0.0.1:6379> set number "100"
  6. OK
  7. 127.0.0.1:6379> object encoding number
  8. "int"
  9. 127.0.0.1:6379> set number abc
  10. OK
  11. 127.0.0.1:6379> object encoding number
  12. "embstr"
  13. 127.0.0.1:6379> set number aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  14. OK
  15. 127.0.0.1:6379> object encoding number
  16. "raw"
  17. 127.0.0.1:6379> set number 9999999999999999999999999
  18. OK
  19. 127.0.0.1:6379> object encoding number
  20. "embstr"
  21. 127.0.0.1:6379> set number 99999999999999999999999999999999999999999999999999999999999999
  22. OK
  23. 127.0.0.1:6379> object encoding number
  24. "raw"

用我们最常使用的字符串做了测试,观察到其编码类型随着我设置的value值不同而改变,我整理出来表格来对应上面的测试结果

value 编码类型
100 int
“100” int
abc embstr
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa raw
9999999999999999999999999 embstr
999999999999999999999999999999999999999999999 raw

1.2 Sting字符串

Redis中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的。Redis的字符串叫 SDS,也是Simple Dynamic String。它的结构是一个带长度信息的字节数组。

  1. set name zcq
  2. get name
  3. > exists name
  4. (integer) 1
  5. > del name
  6. (integer) 1
  7. > get name
  8. (nil)

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

  1. > set name1 zcq
  2. OK
  3. > set name2 Tom
  4. OK
  5. > mget name1 name2 name3 # 返回一个列表
  6. 1) "zcq"
  7. 2) "Tom"
  8. 3) (nil)
  9. > mset name1 boy name2 girl name3 unknown
  10. > mget name1 name2 name3
  11. 1) "boy"
  12. 2) "girl"
  13. 3) "unknown"

过期和 set 命令扩展
可以对Key设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。

  1. > expire name 5 #5s 后过期
  2. ... # wait for 5s
  3. > get name
  4. (nil)
  5. > setex name 5 zcq # 5s 后过期,等价于 set+expire
  6. > get name
  7. "zcq"
  8. ... # wait for 5s
  9. > get name
  10. (nil)
  11. > setnx name zcq # 如果 name 不存在就执行 set 创建
  12. (integer) 1
  13. > get name
  14. "zcq"
  15. > setnx name tom
  16. (integer) 0 # 因为 name 已经存在,所以 set 创建不成功
  17. > get name
  18. "zcq" # 没有改变

计数
如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值 (0~2^63-1),超过了这个值,Redis 会报错。

  1. > set age 30
  2. OK
  3. > incr age
  4. (integer) 31
  5. > incrby age 5
  6. (integer) 36
  7. > incrby age -5
  8. (integer) 31
  9. > set codehole 9223372036854775807 # Long.Max
  10. OK
  11. > incr codehole
  12. (error) ERR increment or decrement would overflow

image.pngcontent 里面存储了真正的字符串内容,那 capacitylen 表示什么意思呢?它有点类似于 Java 语言的 ArrayList 结构,需要比实际的内容长度多分配一些冗余空间。capacity 表示所分配数组的容量,len 表示字符串的实际长度。

前面我们提到字符串是可以修改的字符串,它要支持 append 操作。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的长度非常长,这样的内存分配和复制开销就会非常大。
sds.c

  1. /* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
  2. * end of the specified sds string 's'.
  3. *
  4. * After the call, the passed sds string is no longer valid and all the
  5. * references must be substituted with the new pointer returned by the call. */
  6. sds sdscatlen(sds s, const void *t, size_t len) {
  7. size_t curlen = sdslen(s); // 原字符串长度
  8. // 按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中
  9. s = sdsMakeRoomFor(s,len);
  10. if (s == NULL) return NULL; // 内存不足
  11. memcpy(s+curlen, t, len); // 追加目标字符串的内容到字节数组中
  12. sdssetlen(s, curlen+len); // 设置追加后的长度值
  13. s[curlen+len] = '\0'; // 让字符串以\0 结尾,便于调试打印,还可以直接使用 glibc 的字符串函数进行操作
  14. return s;
  15. }

Redis 规定字符串的长度不得超过 512M 字节

  1. static int checkStringLength(redisClient *c, long long size) {
  2. if (size > 512*1024*1024) {
  3. addReplyError(c,"string exceeds maximum allowed size (512MB)");
  4. return REDIS_ERR;
  5. }
  6. return REDIS_OK;
  7. }