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 源码中能找到准确的定义:
/* Object types */#define REDIS_STRING 0#define REDIS_LIST 1#define REDIS_SET 2#define REDIS_ZSET 3#define REDIS_HASH 4
其实这只是redis对外暴露的抽象结构,其底层实现要看其编码类型来决定使用该编码类型对应的数据结构。
如果一个对象类型只有一种底层数据结构的实现方式,那么这个编码类型就完全多余了,早期的 redis 的确没有这个概念。但后来为了优化性能,一种对象类型可能对应多种不同的编码实现。编码类型在 redis 源码中也有准确定义:
/* Objects encoding. Some kind of objects like Strings and Hashes can be* internally represented in multiple ways. The 'encoding' field of the object* is set to one of this fields for this object. */#define OBJ_ENCODING_RAW 0 /* Raw representation */#define OBJ_ENCODING_INT 1 /* Encoded as integer */#define OBJ_ENCODING_HT 2 /* Encoded as hash table */#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
同一个对象类型,可以有不同的编码类型作为底层实现。而同一种编码类型,也可以支持上层的多种对象类型。他们的关系如下:
为什么一种对象类型要对应多种编码类型?为了提升性能。object encoding xxx 这个 redis 命令来查看某一个 key 其 value 对象所使用的编码类型
127.0.0.1:6379> set number 100OK127.0.0.1:6379> object encoding number"int"127.0.0.1:6379> set number "100"OK127.0.0.1:6379> object encoding number"int"127.0.0.1:6379> set number abcOK127.0.0.1:6379> object encoding number"embstr"127.0.0.1:6379> set number aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaOK127.0.0.1:6379> object encoding number"raw"127.0.0.1:6379> set number 9999999999999999999999999OK127.0.0.1:6379> object encoding number"embstr"127.0.0.1:6379> set number 99999999999999999999999999999999999999999999999999999999999999OK127.0.0.1:6379> object encoding number"raw"
用我们最常使用的字符串做了测试,观察到其编码类型随着我设置的value值不同而改变,我整理出来表格来对应上面的测试结果
| value | 编码类型 |
|---|---|
| 100 | int |
| “100” | int |
| abc | embstr |
| aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | raw |
| 9999999999999999999999999 | embstr |
| 999999999999999999999999999999999999999999999 | raw |
1.2 Sting字符串
Redis中的字符串是可以修改的字符串,在内存中它是以字节数组的形式存在的。Redis的字符串叫 SDS,也是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
set name zcqget name> exists name(integer) 1> del name(integer) 1> get name(nil)
批量键值对
可以批量对多个字符串进行读写,节省网络耗时开销。
> set name1 zcqOK> set name2 TomOK> mget name1 name2 name3 # 返回一个列表1) "zcq"2) "Tom"3) (nil)> mset name1 boy name2 girl name3 unknown> mget name1 name2 name31) "boy"2) "girl"3) "unknown"
过期和 set 命令扩展
可以对Key设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。
> expire name 5 #5s 后过期... # wait for 5s> get name(nil)> setex name 5 zcq # 5s 后过期,等价于 set+expire> get name"zcq"... # wait for 5s> get name(nil)> setnx name zcq # 如果 name 不存在就执行 set 创建(integer) 1> get name"zcq"> setnx name tom(integer) 0 # 因为 name 已经存在,所以 set 创建不成功> get name"zcq" # 没有改变
计数
如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值 (0~2^63-1),超过了这个值,Redis 会报错。
> set age 30OK> incr age(integer) 31> incrby age 5(integer) 36> incrby age -5(integer) 31> set codehole 9223372036854775807 # Long.MaxOK> incr codehole(error) ERR increment or decrement would overflow

content 里面存储了真正的字符串内容,那 capacity 和 len 表示什么意思呢?它有点类似于 Java 语言的 ArrayList 结构,需要比实际的内容长度多分配一些冗余空间。capacity 表示所分配数组的容量,len 表示字符串的实际长度。
前面我们提到字符串是可以修改的字符串,它要支持 append 操作。如果数组没有冗余空间,那么追加操作必然涉及到分配新数组,然后将旧内容复制过来,再 append 新内容。如果字符串的长度非常长,这样的内存分配和复制开销就会非常大。
sds.c
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the* end of the specified sds string 's'.** After the call, the passed sds string is no longer valid and all the* references must be substituted with the new pointer returned by the call. */sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s); // 原字符串长度// 按需调整空间,如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL; // 内存不足memcpy(s+curlen, t, len); // 追加目标字符串的内容到字节数组中sdssetlen(s, curlen+len); // 设置追加后的长度值s[curlen+len] = '\0'; // 让字符串以\0 结尾,便于调试打印,还可以直接使用 glibc 的字符串函数进行操作return s;}
Redis 规定字符串的长度不得超过 512M 字节
static int checkStringLength(redisClient *c, long long size) {if (size > 512*1024*1024) {addReplyError(c,"string exceeds maximum allowed size (512MB)");return REDIS_ERR;}return REDIS_OK;}
