Redis 中有字符串、列表、哈希、集合和有序集合 5 种对象,每种对象都用到了至少一种之前介绍的数据结构。每个 Redis 对象都由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 type、encoding 和 ptr。
// redis.h |
redisObject 结构体使用了位域(Bit Field)类型,即在成员变量最后添加冒号并指定占用的位数,可以节约内存空间,用 sizeof robj
语句可以看到结构体占用的字节数是 16(64 位系统下指针占 8 字节),在 C 语言中默认的类型是 int,所以 unsigned
其实就是 unsigned int
。
type 属性即 Redis 的 5 种对象类型,TYPE 命令输出的就是这 5 种类型:
对象类型 | 对象 type 属性的值 | TYPE 命令的输出 |
---|---|---|
字符串 | REDIS_STRING |
string |
列表 | REDIS_LIST |
list |
哈希 | REDIS_HASH |
hash |
集合 | REDIS_SET |
set |
有序集合 | REDIS_ZSET |
zset |
ptr 指针对象对象的底层实现数据结构,而这些数据结构由 encoding 属性决定,OBJECT ENCODING 命令可以查看底层数据结构的类型:
底层数据结构类型 | 对象 encoding 属性的值 | OBJECT ENCODING |
---|---|---|
整数 | REDIS_ENCODING_INT |
int |
embstr 编码的 SDS | REDIS_ENCODING_EMBSTR |
emstr |
SDS | REDIS_ENCODING_RAW |
raw |
字典 | REDIS_ENCODING_HT |
hashtable |
链表 | REDIS_ENCODING_LINKEDLIST |
linkedlist |
压缩列表 | REDIS_ENCODING_ZIPLIST |
ziplist |
整数集合 | REDIS_ENCODING_INTSET |
intset |
跳跃表 | REDIS_ENCODING_SKIPLIST |
skiplist |
字符串对象
字符串对象的编码可以是 int、embstr 或者 raw 。如果一个字符串对象保存的是整数值,且这个整数值可以用 C 语言的 long 类型来表示,直接使用 ptr 指针属性存储这个整数值。如果保存的字符串很短(不大于 44 字节)则使用 embstr 编码,即通过调用一次内存分配函数申请一块连续的内存空间,依次存放 redisObject 和 sdshdr 两个结构,embstr 编码的字符串对象在修改之后总会变成一个 raw 编码的字符串对象。
// object.c |
列表对象
列表对象的编码可以是 ziplist 或者 linkedlist。ziplist 编码的列表对象使用压缩列表作为底层实现,每个节点(Entry)保存一个列表元素。linkedlist 编码的列表对象使用链表作为底层实现,每个节点(Node)都保存了一个字符串对象(redisOject),这个字符串对象保存了一个列表元素。
当列表对象可以同时满足以下两个条件时(数值可以配置),使用 ziplist 编码,否则使用 linkedlist 编码:
- 列表对象保存的所有字符串元素的长度都小于 64 字节;
- 列表对象保存的元素数量小于 512 个。
哈希对象
哈希对象的编码可以是 ziplist 或者 hashtable。ziplist 编码的哈希对象使用压缩列表作为底层实现,每当有心的键值对要加入到哈希对象时,先将保存了键的压缩列表节点推入表尾,然后再将保存了值的节点推入到表尾。hashtable 编码的哈希对象使用字典作为底层实现,字典的键值都是字符串对象,分别保存实际的键和值。
当哈希对象可以同时满足以下两个条件时,使用 ziplist 编码,否则使用 hashtable 编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
- 哈市对象保存的键值对数量小于 512 个。
集合对象
集合对象的编码可以是 intset 或者 hashtable。hashtable 编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含一个集合元素,而字典的值则全部被设置为 NULL。
当集合对象可以同时满足以下两个条件时,对象使用 intset 编码,否则使用 hashtable 编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过 512 个。
有序集合对象
有序集合对象的编码可以是 ziplist 或者 skiplist。ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨着的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值;压缩列表内的集合元素按分值从小到大排列。skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。
// redis.h |
zset 结构中的 zsl 跳跃表按照分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:object 属性保存了元素的成员(一个字符串对象),score 属性保存了元素的分值(一个 double 类型的浮点数),可以支持按照分值进行范围查询。dict 字典创建了一个从成员到分值的映射,支持 O(1) 复杂度查找给定成员的分值。值得一提的是,zsl 和 dict 这两种数据结构会通过指针来共享相同元素的成员和分值,所以并不会浪费额外的内存。
当有序集合对象可以同时满足以下两个条件时,对象使用 ziplist 编码,否则使用 skiplist 编码:
- 有序集合保存的所有元素成员的长度都小于 64 字节;
- 有序集合保存的元素数量小于 128 个。
数据库
Redis 服务器将所有数据库都保存在 redisServer 结构的 redisDb 数组中,每个 redisDb 结构代表一个数据库,redisDb 结构的 dict 字典保存了数据库中的所有键值对,这个字典被称为键空间(Key Space)。
// redis.h |
Redis 有 4 种不同的命令可以用于设置键的生存时间(Time To Live, TTL):EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT,最终前 3 个命令都会转换成 PEXPIREAT 命令,即将键的过期时间设置为指定的毫秒数时间戳。redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,这个字典称为过期字典:过期字典的键是一个指针,指向键空间中的某个键对象;过期字典的值是一个毫秒精度的 UNIX 时间戳(long long 类型)即过期时间。PERSISIT 命令可以移除一个键的过期时间,是 PEXPIREAT 的反操作,它会在过期字典中删除对应的键值对。
Redis 对过期键使用惰性删除策略,所有读写数据库的 Redis 命令在执行之前都会调用 db.c 文件中的 expireIfNeeded 函数判断键是否过期,如果过期则从数据库中删除。
Redis 也会定期清理过期键,redis.c 中的 serverCron 函数是服务器的周期性任务(每秒运行 hz 次),这个函数中会调用 activeExpireCycle 函数对过期键进行清理:它在规定时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。