redis没有直接使用数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。
1、字符串对象
1.1 字符串对象编码
int类型
如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示。那么字符串对象会将整数值保存字符串对象结构的ptr属性里面(将void *转换成long),并且将字符串对象的编码设置为int。
raw类型
如果一个字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并且将字符串对象的编码设置为raw。
embstr类型
如果一个字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于39字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。
long double 类型表示的浮点数在redis中也是作为字符串值来保存的。保存时,程序会先将这个浮点数转换成字符串值,然后再保存转换得到的字符串值。使用时,程序会将保存在字符串对象里面的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得到的浮点数值转换回字符串值,并继续保存在字符串对象里面。
1.2 编码转换
int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。
因为redis没有为embstr编码的字符串对象编写任何相应的修改程序,所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换为raw,然后再执行修改指令。因为这个原因,embstr编码的字符串对象,在执行修改命令之后,总会变成一个raw编码的字符串对象。
1.3 字符串类型命令
命令 | int 编码的实现方法 | embstr 编码的实现方法 | raw 编码的实现方法 |
set | 使用 int 编码保存值。 | 使用 embstr 编码保存值。 | 使用 raw 编码保存值。 |
get | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值。 | 直接向客户端返回字符串值。 | 直接向客户端返回字符串值。 |
append | 将对象转换成 raw 编码,然后按 raw编码的方式执行此操作。 | 将对象转换成 raw 编码,然后按 raw编码的方式执行此操作。 | 调用 sdscatlen 函数,将给定字符串追加到现有字符串的末尾。 |
incrbyfloat | 取出整数值并将其转换成 longdouble 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。 | 取出字符串值并尝试将其转换成long double 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。如果字符串值不能被转换成浮点数,那么向客户端返回一个错误。 | 取出字符串值并尝试将其转换成 longdouble 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数,那么向客户端返回一个错误。 |
incrby | 对整数值进行加法计算,得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令,向客户端返回一个错误。 | raw 编码不能执行此命令,向客户端返回一个错误。 |
decrby | 对整数值进行减法计算,得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令,向客户端返回一个错误。 | raw 编码不能执行此命令,向客户端返回一个错误。 |
strlen | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,计算并返回这个字符串值的长度。 | 调用 sdslen 函数,返回字符串的长度。 | 调用 sdslen 函数,返回字符串的长度。 |
setrange | 将对象转换成 raw 编码,然后按 raw编码的方式执行此命令。 | 将对象转换成 raw 编码,然后按 raw编码的方式执行此命令。 | 将字符串特定索引上的值设置为给定的字符。 |
getrange | 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 |
2、列表对象
2.1 列表对象编码
ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点都保存了一个字符串对象,而每个字符串对象保存了一个列表元素。
2.2 编码转换
当列表对象可以同时满足一下两个条件时,列表对象使用ziplist编码(不能满足这两个条件的列表对象使用linkedlist对象编码):
列表对象保存的所有字符串元素的长度都小于64字节;
列表对象保存的元素数量小于512个;
2.3列表类型命令
命令 | ziplist 编码的实现方法 | linkedlist 编码的实现方法 |
lpush | 调用 ziplistPush 函数,将新元素推入到压缩列表的表头。 | 调用 listAddNodeHead 函数,将新元素推入到双端链表的表头。 |
rpush | 调用 ziplistPush 函数,将新元素推入到压缩列表的表尾。 | 调用 listAddNodeTail 函数,将新元素推入到双端链表的表尾。 |
lpop | 调用 ziplistIndex 函数定位压缩列表的表头节点,在向用户返回节点所保存的元素之后,调用 ziplistDelete 函数删除表头节点。 | 调用 listFirst 函数定位双端链表的表头节点,在向用户返回节点所保存的元素之后,调用 listDelNode 函数删除表头节点。 |
rpop | 调用 ziplistIndex 函数定位压缩列表的表尾节点,在向用户返回节点所保存的元素之后,调用 ziplistDelete 函数删除表尾节点。 | 调用 listLast 函数定位双端链表的表尾节点,在向用户返回节点所保存的元素之后,调用 listDelNode 函数删除表尾节点。 |
lindex | 调用 ziplistIndex 函数定位压缩列表中的指定节点,然后返回节点所保存的元素。 | 调用 listIndex 函数定位双端链表中的指定节点,然后返回节点所保存的元素。 |
llen | 调用 ziplistLen 函数返回压缩列表的长度。 | 调用 listLength 函数返回双端链表的长度。 |
linsert | 插入新节点到压缩列表的表头或者表尾时,使用 ziplistPush 函数; 插入新节点到压缩列表的其他位置时,使用 ziplistInsert 函数。 | 调用 listInsertNode 函数,将新节点插入到双端链表的指定位置。 |
lrem | 遍历压缩列表节点,并调用 ziplistDelete 函数删除包含了给定元素的节点。 | 遍历双端链表节点,并调用 listDelNode 函数删除包含了给定元素的节点。 |
ltrim | 调用 ziplistDeleteRange 函数,删除压缩列表中所有不在指定索引范围内的节点。 | 遍历双端链表节点,并调用 listDelNode 函数删除链表中所有不在指定索引范围内的节点。 |
lset | 调用 ziplistDelete 函数,先删除压缩列表指定索引上的现有节点,然后调用 ziplistInsert 函数,将一个包含给定元素的新节点插入到相同索引上面。 | 调用 listIndex 函数,定位到双端链表指定索引上的节点,然后通过赋值操作更新节点的值。 |
3、哈希对象
3.1 编码类型
ziplist类型
ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:
保存了同一个键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象的键值对会被放在压缩列表的表尾方向。
hashtable类型
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:
字典的每隔键都是一个字符串对象,对象中保存了键值对的键;
字典的每个值都是一个字符串对象,对象中保存了键值对的值。
3.2 类型转换
当哈希对象同时满足以下两个条件时,哈希对象使用ziplist编码(不能满足这两个条件的哈希对象使用hashtable编码):
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
哈希对象保存的键值对数量小于512个;
该两个条件的上限值可以通过配置文件redis.conf中hash-max-ziplist-value选项和hash-max-ziplist-entries选项修改。
3.3 哈希对象命令
命令 | ziplist 编码实现方法 | hashtable 编码的实现方法 |
hset | 首先调用 ziplistPush 函数,将键推入到压缩列表的表尾,然后再次调用 ziplistPush 函数,将值推入到压缩列表的表尾。 | 调用 dictAdd 函数,将新节点添加到字典里面。 |
hget | 首先调用 ziplistFind 函数,在压缩列表中查找指定键所对应的节点,然后调用 ziplistNext 函数,将指针移动到键节点旁边的值节点,最后返回值节点。 | 调用 dictFind 函数,在字典中查找给定键,然后调用 dictGetVal 函数,返回该键所对应的值。 |
hexists | 调用 ziplistFind 函数,在压缩列表中查找指定键所对应的节点,如果找到的话说明键值对存在,没找到的话就说明键值对不存在。 | 调用 dictFind 函数,在字典中查找给定键,如果找到的话说明键值对存在,没找到的话就说明键值对不存在。 |
hdel | 调用 ziplistFind 函数,在压缩列表中查找指定键所对应的节点,然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 | 调用 dictDelete 函数,将指定键所对应的键值对从字典中删除掉。 |
hlen | 调用 ziplistLen 函数,取得压缩列表包含节点的总数量,将这个数量除以 2 ,得出的结果就是压缩列表保存的键值对的数量。 | 调用 dictSize 函数,返回字典包含的键值对数量,这个数量就是哈希对象包含的键值对数量。 |
hgetall | 遍历整个压缩列表,用 ziplistGet 函数返回所有键和值(都是节点)。 | 遍历整个字典,用 dictGetKey 函数返回字典的键,用 dictGetVal 函数返回字典的值。 |
4、集合对象
4.1 编码类型
intset类型
intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有对象都被保存在整数集合里面。
hashtable类型
hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值全部被设置为NULL。
4.2 类型转换
当集合对象同时满足一下两个条件时,对象使用intset编码(不能满足这两个条件的集合对象需要使用hashtable编码):
集合对象保存的所有元素都是整数值;
集合对象保存的元素数量不超过512个。
该两个条件的上限值可以通过配置文件redis.conf中set-max-intset-entries选项修改。
4.3 哈希类型命令
命令 | intset 编码的实现方法 | hashtable 编码的实现方法 |
sadd | 调用 intsetAdd 函数,将所有新元素添加到整数集合里面。 | 调用 dictAdd ,以新元素为键,NULL 为值,将键值对添加到字典里面。 |
scard | 调用 intsetLen 函数,返回整数集合所包含的元素数量,这个数量就是集合对象所包含的元素数量。 | 调用 dictSize 函数,返回字典所包含的键值对数量,这个数量就是集合对象所包含的元素数量。 |
sismember | 调用 intsetFind 函数,在整数集合中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明元素不存在于集合。 | 调用 dictFind 函数,在字典的键中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明元素不存在于集合。 |
smembers | 遍历整个整数集合,使用 intsetGet 函数返回集合元素。 | 遍历整个字典,使用 dictGetKey 函数返回字典的键作为集合元素。 |
srandmember | 调用 intsetRandom 函数,从整数集合中随机返回一个元素。 | 调用 dictGetRandomKey 函数,从字典中随机返回一个字典键。 |
spop | 调用 intsetRandom 函数,从整数集合中随机取出一个元素,在将这个随机元素返回给客户端之后,调用 intsetRemove 函数,将随机元素从整数集合中删除掉。 | 调用 dictGetRandomKey 函数,从字典中随机取出一个字典键,在将这个随机字典键的值返回给客户端之后,调用 dictDelete 函数,从字典中删除随机字典键所对应的键值对。 |
srem | 调用 intsetRemove 函数,从整数集合中删除所有给定的元素。 | 调用 dictDelete 函数,从字典中删除所有键为给定元素的键值对。 |
5、有序集合对象
5.1 编码类型
ziplist类型
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(scope)。压缩列表内的集合元素按分值从小到大进行排列。
skiplist类型
skiplist编码的有序集合对象使用zset结果作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
zset结构中的zsl跳跃表按照分支从小到大保存了所有的集合元素。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等。
除此之外,zset结构中的dict字典,为有序集合创建了一个从成员到分值的映射,字典的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值保存了元素的分值。通过这个字典,程序可以用O(1)的复杂度查找给定成员的分值,比如ZSCORE。
5.2 编码转换
当有序集合对象可以同时满足一下两个条件时,对象使用ziplist编码(否则使用skiplist编码):
有序集合保存的元素数量小于128个;
有序集合保存的所有元素成员的长度都小于64字节。
5.3 有序集合对象命令
命令 | ziplist 编码的实现方法 | zset 编码的实现方法 |
zadd | 调用 ziplistInsert 函数,将成员和分值作为两个节点分别插入到压缩列表。 | 先调用 zslInsert 函数,将新元素添加到跳跃表,然后调用 dictAdd 函数,将新元素关联到字典。 |
zcard | 调用 ziplistLen 函数,获得压缩列表包含节点的数量,将这个数量除以 2 得出集合元素的数量。 | 访问跳跃表数据结构的 length 属性,直接返回集合元素的数量。 |
zcount | 遍历压缩列表,统计分值在给定范围内的节点的数量。 | 遍历跳跃表,统计分值在给定范围内的节点的数量。 |
zrange | 从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素。 | 从表头向表尾遍历跳跃表,返回给定索引范围内的所有元素。 |
zrevrange | 从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素。 | 从表尾向表头遍历跳跃表,返回给定索引范围内的所有元素。 |
zrank | 从表头向表尾遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名。 | 从表头向表尾遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名。 |
zrevrank | 从表尾向表头遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名。 | 从表尾向表头遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名。 |
zrem | 遍历压缩列表,删除所有包含给定成员的节点,以及被删除成员节点旁边的分值节点。 | 遍历跳跃表,删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。 |
zscore | 遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分值节点保存的元素分值。 | 直接从字典中取出给定成员的分值。 |