大纲
基本结构
SDS字符串:结构与C语言字符串不同的是:redis字符串结构中有记录字符长度的字段,一是获取长度是复杂度只需要O(1),二是杜绝缓冲区溢出,在修改某个value时,先去校验分配的长度是否足够,不够的话自动扩展空间,这样不会覆盖到相邻空间的value值。三是减少修改时带来的内存空间重分配,C语言没有记录字符长度,每次修改都需要重新分配内存,避免缓冲区溢出或内存泄露
SDS是动态字符串,那么字符串一开始的长度其实就会预留出比实际字符串长度还要大的空间,如果字符串的长度是1024,Redis 会分配2048字节的存储空间,也就是 100% 的冗余空间。不过 Redis 考虑的更加周到,当字符串的长度超过 1M 时,它的冗余空间只有 1M,避免出现太大的浪费。Redis 还限制了字符串最大长度不得超过 512M。
注:我们日常使用的字符串都是只读的,一般只有拿字符串当位图使用时才会对字符串进行追加和修改操作
hashtable字典:字典结构:包括类型特定函数(计算哈希值的函数,复制键的函数等等),私有数据,哈希表(ht[X]),refresh索引(当refresh不在进行时,值为-1)
哈希表作为底层实现,哈希表结构体包括哈希表节点数组,哈希表大小,哈希表大小掩码(用于计算索引值,总是等于size-1),该哈希表已有节点的数量
哈希表节点结构:包括键,值,指针(指向下一个哈希表节点,形成链表)
哈希算法:计算哈希值计算索引值(数组下标),根据这两个去插入数据。当键冲突时,进行链地址法(头排法)解决
渐进式refresh:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
- 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
- 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
- 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
skiplist跳表:跳跃表支持平均 O(logN)、 最坏O(N)的复杂度的节点查找,还可以通过顺序性操作来批量处理节点,redis只有两个地方使用到了跳表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构
intset整数集合:整数集合(intset)是集合键的底层实现之一 ,当一个集合只包含整数值元素,并且这个集合的元素数量不多时, Redis就会使用整数集合作为集合键的底层实现。整数集合的底层实现为数组, 这个数组以有序、无重复的方式保存集合元素(根据二分法进行数据的查找后插入),每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade,申请内存),并且转换后的元素放在正确的位置上保持有序性质不变,然后才能将新元素添加到整数集合里面
linkedlist链表:双向,无环,带表头指针带表尾指针,带链表长度计数器,多态(通过为链表设置不同的类型特定画数, Redis 的链表可以用于保存各种不同类型的值)
ziplist压缩列表:压缩列表( ziplist) 是列表键和哈希键的底层实现之一。ziplist都是紧凑存储,没有冗余空间,意味着每次插入一个元素都需要重新分配内存,拷贝内容;有极小的概率会触发级联更新(所有的节点的长度都是 253 字节,插入一个大于 254 字节的节点时会出现级联更新)。所以ziplist不适合存储大小字符串,存储的元素也不宜过多
压缩列表类似一个数组,数组中每个元素都对应保存一个数据,与数组不同的是,压缩列表在表头有三个字段zlbytes,zltail,zllen,分别表示列表长度,列表尾偏移量(用来快速定位到最后一个元素,为了支持双向遍历),列表中entry个数(当前一个 entry 的长度在 254 字节以内的时候,这个属性用一个字节来记录。否则就会用 5 个字节来记录),表尾还有一个zlend,表示结束。
整数数组和压碎列表在时间复杂度上并不占优势,为什么redis还要把他们当做底层数据结构?
两方面原因:
- 内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。
- 数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。
quicklist快速列表: redis3.2版本以后使用quicklist替代ziplist 和 linkedlist 。
当list数据较大时,纯粹的使用 Linkedlist, 也就是普通链表来存储数据有两个弊端:
- 每个节点都有自己的前后指针,指针所占用的内存有点多,太浪费了。
- 每个节点单独的进行内存分配,当节点过多,造成的内存碎片太多了。影响内存管理的效率
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数 list-max-ziplist-size 决定。默认的压缩深度为 0, 也就是所有的节点都不压缩,如果将一个 ziplist 压缩,那么要从它里面读取值,必然要先解压,会造成性能变差,由于中间的数据访问频率比较低,可以压缩中间数据,两端数据不压缩
zset: skiplist作为zset的存储结构,包括一个dict对象和一个skiplist对象。dict保存key/value,key为元素,value为分值;skiplist保存的有序的元素列表,每个元素包括元素和分值。zskiplist保存有序的元素列表,便于执行range之类的命令。两种数据结构下的元素指向相同的位置。
/*
* 有序集合
*/
typedef struct zset {
// 字典,键为成员,值为分值
// 用于支持 O(1) 复杂度的按成员取分值操作
dict *dict;
// 跳跃表,按分值排序成员
// 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
// 以及范围操作
zskiplist *zsl;
} zset;
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
zset使用场景:
1. 延时队列
zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入zset集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。
起一个死循环线程不断地进行取第一个key值,如果当前时间戳大于等于该key值的socre就将它取出来进行消费删除,可以达到延时执行的目的。
2. 排行榜
经常浏览技术社区的话,应该对 “1小时最热门” 这类榜单不陌生。如何实现呢?如果记录在数据库中,不太容易对实时统计数据做区分。我们以当前小时的时间戳作为 zset 的 key,把贴子ID作为 member ,点击数评论数等作为 score,当 score 发生变化时更新 score。利用
ZREVRANGE或者ZRANGE查到对应数量的记录。3. 限流
滑动窗口是限流常见的一直策略。如果我们把一个用户的 ID 作为key 来定义一个 zset ,member 或者 score 都为访问时的时间戳。我们只需统计某个 key 下在指定时间戳区间内的个数,就能得到这个用户滑动窗口内访问频次,与最大通过次数比较,来决定是否允许通过
基本对象(常说的五种数据类型)
对象:Redis并没有直接使用前面那些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、 列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象, 每种对象都用到了至少一种我们前面所介绍的数据结构。通过这五种不同类型的对象, Redis可以在执行命令之前, 根据对象的类型来判断一个对象是否可以执行给定的命令。 使用对象的另一个好处是, 我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率(采用最合适的编码类型)。
string字符串对象编码:int,raw,embstr(专门用于保存短字符串的一种优化编码方式)
注:当字符串内容可以用 long 整数表达时,对象头的 ptr 指针将退化为一个 long 型的整数,如果这个整数太大,超出了 long 的表达范围,就会使用 sds 字符串表示。在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。当内存分配器分配了64的空间,字符串的redisObject 对象头,SDS对象头还需要大小,所以是44
对于 Redis 的字符串对象来说,我们需要先访问 redisObject 对象头,拿到 ptr 指针,然后再访问指向的 sds 字符串。如果对象头和 sds 字符串相距较远,就会存在缓存穿透现象,性能就会打折。所以 Redis 为了优化硬件的缓存命中,它为字符串设计了一种特殊的编码结构,这种结构就是 embstr 。它将 redisObject 对象头和 sds 字符串挤在一起连续存储,可以一次性放到缓存行里,这样就可以明显提升缓存命中率
list列表对象编码:ziplist,linkedlist
注:当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码:列表对象保存的所有字符串元素的长度都小于 64 字节;列表对象保存的元素数量小于 512 个;不能满足这两个条件的列表对象需要使用linkedlist 编码
hash哈希对象编码:ziplist,hashtable
注:当晗希对象可以同时满足以下两个条件时,哈希对象使用 ziplist 编码:哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;哈希对象保存的键值对数量小于 512 个;不能满足这两个条件的晗希对象需要使用hashtable
set集合对象编码:intset,hashtable
set 的结构为hashtable时,所有的 value 都是 NULL,其它的特性和字典一模一样。
注:当集合对象可以同时满足以下两个条件时,对象使用intset编码:集合对象保存的所有元素都是整数值;
集合对象保存的元素数量不超过512个。不能满足这两个条件的集合对象需要使用hashtable编码。
sortedSet有序对象编码:ziplist,skiplist
skiplist编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。使用两种结构结合,既保证了数据有序又能保证O(1)的时间复杂度
注:当有序集合对象可以同时满足以下两个条件时, 对象使用ziplist编码:有序集合保存的元素数量小于128个;有序集合保存的所有元素成员的长度都小于64字节;不能满足以上两个条件的有序集合对象将使用skiplist编码。
对象管理
类型检查:Redis中用于操作键的命令基本上可以分为两种类型。
其中一种命令可以对任何类型的键执行, 比如说 DEL 命令、EXPIRE 命令、 RENAME 命令、 TYPE命令、 OBJECT命令等,而另一种命令只能对特定类型的键执行,比如说SET、 GET等命令只能对字符串键执行,RPUSH、 LPOP 等命令只能对列表键执行。为了确保只有指定类型的键可以执行某些特定的命令,在执行一个类型特定的命令之前, Redis会先检查输入键的类型是否正确, 然后再决定是否执行给定的命令;类型特定命令所进行的类型检查是通过 redisObject 结构的 type 属性来实现的
命令多态:redis会根据值对象 的编码方式,选择正确的命令实现代码来执行命令。比如我们对一个键执行 LLEN命令,那么服务器除了要确保执行命令的是列表键之外,还需要根据键的值对象所使用的编码来选择正确的 LLEN 命令实现。如果列表对象的编码为ziplist,那么就要使用ziplistApi中的ziplistlen函数来返回列表的长度,如果是其他编码的需要使用对应的Api中的对应函数。由此我们可以认为LLEN命令是多态的
内存回收:Redis的对象系统实现了基于引用计数技术的内存回收机制,对象的引用计数信息会随着对象的使用状态而不断变化。 当程序不再使用某个对象的时候, 这个对象所占用的内存就会被自动释放
对象共享:Redis还通过引用计数技术实现了对象共享机制, 这一机制可以在适当的条件下, 通过让多个数据库键共事同一个对象来节约内存。
对象的空转时长:redisObject结构包含的最后一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间;数据库键的空转时长 = 当前时间 - lru时间。 在服务器启用了maxmemory功能的情况下,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru, 空转时长较大的那些键可能会优先被服务器删除。
本文深入解析Redis中的数据结构,如SDS字符串、hashtable字典、skiplist跳表、intset整数集合、linkedlist链表、ziplist压缩列表、quicklist快速列表和zset有序集合。探讨它们的特点、应用场景及优劣,揭示Redis如何高效存储和处理数据。

1644

被折叠的 条评论
为什么被折叠?



