参考:黄健宏 著. Redis设计与实现 (数据库技术丛书) . 机械工业出版社. Kindle 版本.




 

 

关于各种对象的实现方式:

 

字符串对象

列表对象

哈希对象

集合对象

有序集合对象

实现

整数值



简单动态字符串

压缩列表



双端链表

压缩列表



字典

整数集合



字典

压缩列表



跳跃表和字典

编码



REDIS_ENCODING_***

INT



EMBSTR



RAW

ZIPLIST



LINKEDLIST

ZIPLIST



HT

INTSET



HT

ZIPLIST



SKIPLIST

 


 



 



先来了解一下各种对象:



 



 



Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键( 键对象), 另一个对象用作键值对的值( 值对象)。




redis存储一对一关系型数据_redis存储一对一关系型数据


 


对于Redis数据库保存的 键值对 来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中 一种,


 


当我们对一个数据库键执行TYPE命令时, 命令返回的结果为数据库键对应的值对象的类型,


使用 OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码:


通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的 灵活性和效率, 因为 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象 在某一场景下的效率。


 


1》字符串对象


redis存储一对一关系型数据_Redis_02


redis存储一对一关系型数据_数据结构与算法_03


redis存储一对一关系型数据_数据结构与算法_04


 


这几种编码格式会在什么情况下相互转换?


 


2》列表对象


ziplist编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点( entry) 保存了一个列表元素。


redis存储一对一关系型数据_Redis_05


另一方面, linkedlist 编码的列表对象使用双端链表作为底层实现, 每个双端链表节点( node) 都保存 了一个字符串对象, 而每个字符串对象都保存了一个列表元素。


redis存储一对一关系型数据_redis存储一对一关系型数据_06


 


编码转换:


当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:


·列表对象保存的所有字符串元素的长度都小于 64 字节;


·列表对象保存的元素数量小于 512 个; 不能满足这两个条件的列表对象需要使用 linkedlist 编码。


 


注意


以上条件可以通过 list- max- ziplist- value 选项 和 list- max- ziplist- entries 调整。


 


3》哈希对象


哈希对象的编码可以是 ziplist 或者 hashtable。 ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存 了值的压缩列表节点推入到压缩列表表尾,


redis存储一对一关系型数据_redis存储一对一关系型数据_07


redis存储一对一关系型数据_Redis_08


 


·哈希对象保存的所有键值对的键和值的字符串长度都小于64 字节;


·哈希对象保存的键值对数量小于512 个; 不能满足这两个条件的哈希对象需要使用 hashtable 编码。


 


这两个条件的上限值是可以修改的, 具体请看配置文件中关于hash- max- ziplist- value 选项和 hash- max- ziplist- entries 选项的说明。


 


4》集合对象


集合对象的编码可以是 intset 或者 hashtable。 intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。


redis存储一对一关系型数据_字符串_09


redis存储一对一关系型数据_数据结构与算法_10


 


当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:


·集合对象保存的所有元素都是整数值;


·集合对象保存的元素数量不超过512 个。


 


5》有序集合对象


有序集合的编码可以是 ziplist 或者 skiplist。 ziplist 编码的压缩列表对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员( member), 而第二个元素 则保存元素的分值( score)。


redis存储一对一关系型数据_redis存储一对一关系型数据_11


redis存储一对一关系型数据_数据结构与算法_12


 


skiplist 编码的有序集合对象使用zset结构作为底层实现, 一个zset结构同时包含一个字典和一个跳跃表:


redis存储一对一关系型数据_数据库_13


 


为什么有序集合需要同时使用跳跃表和字典来实现?


 


 


当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码:


·有序集合保存的元素数量小于128 个;


·有序集合保存的所有元素成员的长度都小于64 字节;


不能满足以上 两个条件的有序集合对象将使用 skiplist 编码。


 




 


构成Redis对象的底层数据结构


 


一、简单动态字符串


 


在Redis 的数据库里面, 包含字符串值的键值对在底层都是由 SDS 实现的。


 


redis存储一对一关系型数据_字符串_14


SDS 遵循C字符串以空字符结尾的惯例


SDS可以直接重用一部分C字符串函数库里面的函数。 比如打印/比较


 


·比起C字符串, SDS具有以下优点:


1) 常数复杂度获取字符串长度。


2) 杜绝缓冲区溢出。 SDS拼接字符串前会计算内存空间是否不足


3) 减少修改字符串长度时所需的内存重分配次数。SDS有两种策略:空间预分配/用free属性记录回收的空间下次利用


4) 二进制安全。由len属性决定字符串是否结束


5) 兼容部分C字符串函数。


 


 


二、链表


 


·链表被广泛用于实现Redis的各种功能, 比如 列表键、 发布与订阅、 慢查询、监视器等。


redis存储一对一关系型数据_数据库_15


redis存储一对一关系型数据_redis存储一对一关系型数据_16


双端链表。


·每个链表使用一个list结构来表示, 这个结构带有表头节点指针、 表尾节点指针, 以及链表长度等信息。


无环链表。


·通过为 链表设置 不同的类型特定函数, Redis的链表可以用于保存各种不同类型的值。


 


 


三、字典


 


字典,又称为符号表(symbol table)、 关联数组( associative array) 或映射( map), 是一 种用于保存键值对( key- value pair)的抽象数据结构。 


Redis 的数据库就是使用字典来作为底层实现的, 对数据库 的增、删、查、改操作也是构建在对字典的 操作之上的。


字典还是哈希键的底层实现之一, 当一个哈希键包含的键值对比较多, 又或者键值对 中的元素都是比较长的字符串时, Redis就会使用字典作为哈希键的底层实现。


 


redis存储一对一关系型数据_数据库_17


 


·字典被广泛用于实现Redis的各种功能, 其中包括 数据库和 哈希键。


·Redis中的 字典使用 哈希表 作为底层实现, 每个字典带有 两个 哈希表, 一个平时使用, 另一个 仅在进行 rehash 时使用。


·当字典被用作 数据库的底层实现, 或者哈希键 的底层实现时, Redis 使用MurmurHash2 算法来计算键 的哈希值。


被分配到同一个索引 上的多个键值对 会连接成 一个单向链表。


redis存储一对一关系型数据_Redis_18


·在对哈希表进行扩展或者收缩操作时, 程序需要将现有哈希表包含 的所有键值对 rehash 到新哈希表里面, 并且这个 rehash 过程并不是一次性地完成的, 而是渐进式地完成的。  


redis存储一对一关系型数据_数据结构与算法_19


 


什么时候rehash? 


 


当以下条件中的任意 一个被满足时, 程序会自动开始对哈希表执行扩展操作:


1) 服务器目前没有在执行BGSAVE命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于1。


2) 服务器目前正在执行BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于5。


另一方面, 当哈希表的负载因子小于0. 1时,程序自动开始对哈希表执行收缩操作。


 


什么是hash table的负载因子? 


 


 


四、跳跃表


 


通过在每个节点中维持多个指向其他节点的指针, 从而达到快速访问节点的目的。


Redis 使用跳跃表作为有序集合键的底层实现之一, 如果一个有序集合包含的元素数量比较多, 又或者有序集合中元素的成员( member) 是比较长的字符串时, Redis就会使用跳跃表来作为有序集合键的底层实现。


redis存储一对一关系型数据_字符串_20


 


·跳跃表是有序集合 的底层实现之一。


·Redis的跳跃表实现 由zskiplist和zskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳跃表信息( 比如 表头节点、 表尾节点、 长度), 而 zskiplistNode 则用于表示跳跃表节点。


·每个跳跃表节点的层高 都是 1 至 32 之间的随机数。


 


 


五、整数集合


整数集合( intset) 是集合键的底层实现之一, 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis就会使用整数集合作为集合键的底层实现。


 ·整数集合的底层实现为数组, 这个数组以有序、 无重复的方式保存集合元素, 在有需要时, 程序会根据新添加元素的类型, 改变这个数组的类型。


 


 

redis存储一对一关系型数据_数据库_21


redis存储一对一关系型数据_数据库_22


整数集合的升级策略有两个好处, 一个是提升整数集合的灵活性, 另一个是尽可能地节约内存。


整数集合不支持降级操作  


 


 


 


六、压缩列表


  


 


压缩列表是Redis 为了节约内存而开发的, 是由一系列特殊编码的连续内存块组成的顺序型( sequential)数据结构。 一个压缩列表可以包含任意多个节点( entry), 每个节点可以保存 一个字节数组或者一个整数值。


redis存储一对一关系型数据_数据库_23


 


压缩列表( ziplist) 是列表键和哈希键的底层实现之一。 当一个列表键只包含少量列表项, 并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。


另外, 当一个哈希键只包含少量键值对, 比且每个键值对的键和值要么就是小整 数值, 要么就是长度比较短的字符串, 那么Redis就会使用压缩列表来做哈希键的底层实现。


 


·添加新节点到压缩列表, 或者从压缩列表中删除节点,可能会引发连锁更新操作, 但这种操作出现的几率并不高。


redis存储一对一关系型数据_数据结构与算法_24


 


 

redis存储一对一关系型数据_字符串_25


 


 




服务器的其它特性


 


1》类型检查与命令多态


Redis 中用 于 操作 键 的 命令 基本上 可以 分为 两种 类型。 其中 一种 命令 可以 对 任何 类型 的 键 执行, 比如说 DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令 等。


而 另一种 命令 只能 对 特定 类型 的 键 执行, 比如说: ·SET、 GET、 APPEND、 STRLEN 等 命令 只能 对 字符串 键 执行;


redis存储一对一关系型数据_数据结构与算法_26


 


列表 对象 有 ziplist 和 linkedlist 两种编码可用, 如果我们对一个键执行 LLEN 命令, 会先检查类型,在根据编码选择不同的api 


DEL、 EXPIRE 等命令和 LLEN 等命令的区别在于, 前者是基于 类型的多 态—— 一个命令可以同时用于处理多种不同类型的键, 而后者是基于编码的多态—— 一个命令可以同时用于处理多种不同编码。


redis存储一对一关系型数据_Redis_27


 


2》内存回收


redis存储一对一关系型数据_字符串_28


初始化为1,被新程序引用时加1,不再使用减1,为0时回收


 


3》对象共享


 


除了 用于 实现 引用 计数 内存 回收 机制 之外, 对象 的 引用 计数 属性 还 带有 对象 共享 的 作用。


在 Redis 中, 让 多个 键 共享 同一个 值 对象 需要 执行 以下 以下 两个 步骤:


1) 将 数据库 键 的 值 指针 指向 一个 现 有的 值 对象;


2) 将被 共享 的 值 对象 的 引用 计数 增 一。


redis存储一对一关系型数据_数据库_29


 


目前来说, Redis会在初始化服务器时, 创建一 万个字符串对象, 这些对象包含了从 0 到 9999 的所有整数值, 当服务器需要用到值为 0 到 9999 的 字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。


注意


创建共享字符串对象的数量可以通过修改 redis. h/ REDIS_ SHARED_ INTEGERS 常量来修改。


 


为什么 Redis 不共享包含字符串的对象?


 


4》对象空转时长


除了前面介绍 过 的 type、 encoding、 ptr 和 refcount 四个属性之外, redisObject 结构包含的最后一个属性为 lru 属性, 该属性记录 了对象最后一次被命令程序访问的时间:


OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一 空转时长就是通过将当前时间减去键的值对象的 lru 时间计算得出的:


redis存储一对一关系型数据_数据库_30


如果服务器打开 了 maxmemory 选项, 并且服务器用于回收内存的算法 为 volatile- lru 或者 allkeys- lru, 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被 服务器释放, 从而回收内存。


 




重点回顾


·Redis 数据库中的每个键值对的键和值都是一个对象。


·Redis 共有字符串、列表、哈希、集合、有序集合五种类型的对象, 每种类型的对象至少都有两种或以上的编码方式, 不同的编码可以在不同的使用场景上优化对象的使用效率。


·服务器在执行某些命令之前,会先检查给定键的类型能否执行指定的命令,而检查一个键的类型就是检查键的值对象的类型。


·Redis 的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时, 该对象所占用的内存就会被自动释放。


·Redis 会共享值为0到9999的字符串对象。


·对象会记录自己的最后一次被访问的时间, 这个时间可以用于计算对象的空转时间。


 


参考:黄健宏 著. Redis设计与实现 (数据库技术丛书) . 机械工业出版社. Kindle 版本.