本文内容摘自《redis设计与实现》黄建宏
1. 数据结构
redis主要基于以下数据结构实现:
- 简单动态字符串(SDS)
- 双端链表
- 字典
- 跳跃表
- 压缩列表
- 整数集合
2. 对象系统
redis基于上述数据结构构建了一个对象系统,针对不同的使用场景,为对象设置多种不同的数据结构实现,包括:
- 字符串对象 (string)
- 列表对象(list)
- 哈希对象(hash)
- 集合对象(set)
- 有序集合对象(zset)
redis使用键值对保存数据,键和值分别是对象,并且键总是字符串对象,值是上述5中对象之一,因此当称一个数据库键为列表键指的是其值为列表对象。
2.1 对象底层数据结构实现
对象的ptr指针指向底层的数据结构,由encoding属性决定使用哪种数据结构,每种类型的对象都至少使用了两种encoding,可以用object encoding key
查看;通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。
数据结构 | encoding | object encoding命令输出 |
---|---|---|
整数 | REDIS_ENCODING_INT | “int” |
emstr编码的简单动态字符串 | REDIS_ENCODING_EMBSTR | “embstr” |
简单动态字符串 | REDIS_ENCODING_RAW | “raw” |
字典 | REDIS_ENCODING_HT | “hashtable” |
双端链表 | REDIS_ENCODING_LINKEDLIST | "“linkedlist” |
压缩列表 | REDIS_ENCODING_ZIPLIST | “ziplist” |
整数集合 | REDIS_ENCODING_INTSET | "“intset” |
跳跃表和字典 | REDIS_ENCODING_SKIPLIST | “skiplist” |
2.2 字符串对象
2.2.1 底层实现
字符串对象的编码可以是int、raw、embstr。
- 如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。
- 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节(3.2之前的版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw
- 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度(包括最后的结束符)小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构
2.2.2 编码转换
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象:
- 对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw。
- 另外,因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序),所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象。
2.2.3 字符串键常用命令
命令 | 用途 |
---|---|
SET key value | 插入字符串键值 |
GET key | 获取key对应的值 |
APPEND key new | 将new追加到vaule |
INCRBY key number | 加法,只有int编码才能用 |
DECRBY key number | 减法,只有int编码才能用 |
STRLEN key | 返回字符串的长度 |
SETRANGE key startindex value | 将startindex 开始的值覆盖为给定的字符 |
GETRANGE key start end | 返回字符串索引上的值 |
2.3 列表对象(list)
列表对象的编码可以是ziplist或者linkedlist,当列表对象保存的所有字符串元素的长度都小于64字节且列表对象保存的元素数量小于512个时使用跳跃表作为底层实现。
命令 | 用途 |
---|---|
LPUSH | 将元素推入表头 |
RPUSH | 将元素推入表尾 |
LPOP | 将表头结点返回并移除 |
RPOP | 将表尾结点返回并移除 |
LINDEX | 返回指定位置元素 |
LINSERT | 将元素插入到指定位置 |
2.4 哈希对象(map)
哈希对象的编码可以是ziplist或者hashtable。ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
- 哈希对象保存的键值对数量小于512个
命令 | 用途 |
---|---|
HSET | 添加键值对 |
HGET | 根据key获取value |
HEXISTS | 判断key是否存在 |
HDEL | 删除key-value |
HLEN | 返回整个哈希对象包含的键值对数量 |
HGETALL | 返回整个哈希对象的键值对 |
2.5 集合对象(set)
集合对象的编码可以是intset或者hashtable。intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过512个。
命令 | 用途 |
---|---|
SADD | 添加元素 |
SCARD | 返回集合数量 |
SISMEMBER | 判断元素是否存在 |
SMEMBERS | 获取所有元素 |
SREM | 删除指定元素 |
SPOP | 随机pop一个元素 |
2.6 有序集合对象(sorted_set)
有序集合的编码可以是ziplist或者skiplist。有序集合键的值为哈希对象,每个值维护一个member和score,member是键,score是值,根据这个score排序。