数据结构
Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。不同类型的数据结构的差异就在于 value 的结构不一样。
String(字符串)
Redis 的字符串是动态字符串(SDS Simple Dynamic String),是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。
具体代码如下:
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte[] content; // 数组内容
}
数据结构如下:
内部编码:
- embstr:小于等于44个字节(版本不同字节数目不同)的字符串
- raw:大于44个字节(版本不同字节数目不同)的字符串
如下图所示,两种编码的根本区别在于:
- embstr只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。
- embstr编码情况下,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
为什么是44字节:
内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64。分配为64的时候,redisObject对象刚好占用了20字节只剩下44个字节来存储字符串。
List(列表)
Redis 的列表是quicklist(快速列表)的数据结构。 quicklist是由ziplist(压缩列表)和linkedlist (普通链表组成)。
LinkedList(普通链表)
普通链表是下面的数据结构,多个元素之间通过prov和next指针连接起来。
ziplist(压缩列表)
压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。相对于linkedlist节约了prev和next
指针空间。Entry就是存储的数据,不用通过指针连接。
quicklist (快速列表)
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。如下图所示:
内部编码:
- ziplist(压缩列表),当列表元素小于512个(默认512,可配置)同时列表的每个元素值都小于64字节(默认64,可配置),目的是为了节约内存。
- quicklist(快速列表),当列表无法满足ziplist的条件时使用quicklist
Hash(字典)
Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。hash 函数是 siphash。siphash 算法即使在输入 key 很小的情况下,也可以产生随机性特别好的输出,而且它的性能也非常突出。数据结构如下图所示:
内部编码:
- ziplist(压缩列表),当哈希类型元素小于512个(默认512,可配置)同时列表的每个元素值都小于64字节(默认64,可配置),目的是为了节约内存。
- hashtable(哈希表),当列表无法满足ziplist的条件时使用hashtable
Set(集合)
Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL
。
内部编码:
- intset(整数集合),当集合中的元素都是整数并且元素个数小于512个(默认512,可配置)
- hashtable(哈希表),当列表无法满足intset的条件时使用hashtable
zset (有序集合)
它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。下面看下跳跃列表的数据结构。
内部编码:
- ziplist(压缩列表),当有序集合元素小于128个(默认5128,可配置)同时列表的每个元素值都小于64字节(默认64,可配置),目的是为了节约内存
- skiplist(跳跃表),当列表无法满足ziplist的条件时使用skiplist
skiplist(跳跃列表)
下图就是跳跃列表的示意图,图中只画了三层,Redis 的跳跃表共有 64 层。
跳表具有如下性质:
- 由很多层结构组成
- 每一层都是一个有序的链表
- 最底层(Level 1)的链表包含所有元素
- 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
- 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
跳跃表查询步骤:
- 从顶层链表开始遍历,查询的元素x和当前节点的值y进行比较
- x < y:从当前查找位置,下降一层
- x > y:继续向前遍历
- 直到x=y,找到节点
比如我们要从上面三层的跳跃表找到节点33,步骤如下:
- 33 > 21,往前一个节点
- 33 < 55,下降一层
- 33 < 37,下降一层
- 33 = 33,找到节点
跳跃表插入操作:
- 查找到须要插入的位置
- 随机层数, Redis 标准源码中的晋升概率只有 25%,一层100%,二层25%,三层25%乘以25%
- 调整每一层的指针
比如我们要插入35这个节点,步骤如下:
- 找到插入节点的位置,在33和37之间
- 随机层数比如为2层
- 插入35到第一层和第二层
Redis缓存淘汰算法
LRU
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
LRU算法实现原理:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
redis近似LRU算法:
LRU算法需要消耗大量的额外的内存并且需要对现有的数据结构进行较大的改造,redis使用一个近似的LRU实现算法。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。
redis3.0新增淘汰池LRU算法:
淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余较旧的 key 列表放入淘汰池中留待下一个循环。
LFU
LFU(Least Frequently Used)算法根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
算法原理:
- 新加入数据插入到队列尾部(因为引用计数为1)
- 队列中的数据被访问后,引用计数增加,队列重新排序
- 当需要淘汰数据时,将已经排序的列表最后的数据块删除
redis LFU算法
redis LFU算法原理上面的LFU算法差不多,大概原理如下:
LFU把原来的key对象的内部时钟的24位分成两部分,前16位还代表时钟,后8位代表一个计数器counter。根据counter来表示访问热度。访问热度会随着访问增加,不访问会减少。当要淘汰的时候也是随机取5个,找到counter值最小的淘汰。