Redis的内部编码

132 阅读5分钟

Redis的内部编码

五大基本数据类型对应的内部编码如下:

  • string
    • int
    • SDS(embstr、raw)
  • list
    • ziplist
    • linkedlist
    • quicklist
  • hash
    • ziplist/listpack
    • hashtable
  • set
    • intset
    • hashtable
  • zset
    • ziplist/listpack
    • skiplist

内部编码

int

当存储的字符串都是整数时,且可以使用long类型(4字节)表示,则采用int内部编码类型 image.png

SDS

首先要知道c语言中不存在专门的字符串数据类型,字符串使用字符数组来存储的,其中通过'\0'来判断是否到达字符串的结尾。为了更方便的操作字符串,Redis内部自定义了一种SDS(Simple Dynamic String)这种数据类型,主要有三个字段——free、len、buf:

  • free:剩余的空间
  • len:判断是否到达字符串结尾
  • buf:指向存储字符串的地址

而SDS又分为embstr和raw两种:

  • emstr:redisObject和SDS存储地址是连续的,也就是说只需要一次性申请内存即可。但由于紧凑的内存分配,导致SDS扩容时,需要对redisObject和SDS同时分配空间image.png

  • raw:redisObject和SDS存储地址是分离的,也就是说需要连续申请两次内存。 image.png

那么是什么时候使用embstr,什么时候又使用raw呢?

  • redis 2.+版本 当字符串小于等于32字节时,使用embstr
  • redis 3.0-4.0版本 当字符串小于等于39字节时,使用embstr
  • redis 5.0版本后 当字符串小于等于44字节时,使用embstr

ziplist以及listpack

image.png

image.png

为什么内存利用率高?

  • 使用该编码方式的数据类型,所有数据都将一个接一个的保存,这将保持内存的高度紧凑,尽可能地避免了内存碎片的出现,故提高了内存利用率。
  • 除此之外,保存数据时会对每个数据再编码一次,使得保存的数据进一步地压缩,从而利用更少的内存空间保存更多的有效数据,故提高了内存利用率。

为什么会出现listpack?

  • ziplist最大的问题是因为每个entry保存了上一个entry的长度,故会发生级联更新
  • 而listpack每个entry只保存自身的长度,从而避免了ziplist会发生的级联更新现象

还有一个比较有趣的问题是——为什么ziplist会记录前一个entry的长度?

  • 首先揭晓答案——为了可以双向遍历,我们必须得知下一个entry首地址上一个entry的首地址
  • 先来看看ziplist:
    • 假设每个entry第一部分记录的是自身的长度,那么从前向后定位下一个entry的首地址是可行的。但是逆向查找时,我们甚至无法定位最后一个entry的首地址
    • 如果entry记录了上一个entry的长度,但是这一部分记录在entry的最后面。实际上还是一样的,从前向后可以定位下一个entry的首地址,但是逆向时,甚至无法定位最后一个entry的首地址。
  • 再切换一下思路就成了listpack的编码格式:
    • 每个entry最后一部分记录的是自身的长度,那么从前向后定位下一个entry的首地址是可行的。逆向查找时,通过确定自身entry的首地址,也就得知了上一个entry的尾地址。同理上一个entry也可以得到自身entry的首地址。因此实现了双向遍历。

ziplist、linkedlist到quicklist

image.png

当list的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值的字节都小于list-max-ziplist-value配置时(默认64字节),使用的是ziplist。

由于ziplist的级联更新问题,在元素个数多的时候,性能将会急速下降,故切换为双向链表的形式。但是这也带来了内存不够紧凑的问题,而根据局部性原则可知,这将会降低访问效率,除此之外也可能导致出现大量的内存碎片。


在Redis 3.2版本后,Redis使用了quicklist作为唯一的一种list内部编码方式

image.png

linkedlist是一个个节点相连起来的,内存不够紧凑连续。而quicklist的每个节点的数据结构是ziplist,当超出元素数量阈值时就创建新ziplist节点,这既避免了内存碎片,又提高了访问效率,还可以避免级联更新现象。

hashtable

image.png hashtable发生节点冲突时,采用的是拉链法来解决。

hashtable比较重要的就是扩容rehash的过程。而Redis采用的是渐进式rehash,之所以要采用渐进式rehash就是为了在rehash的同时保证可以继续进行hash表的写入,而不是阻塞hash表写入操作直到rehash完毕。

  1. hashtable中实际上有两个hash表,当进行rehash时,就会将新数据写入另一个空的hash表中。
  2. 除此之外每个写请求还会将原hash表的一个索引位置上所有entry都迁移到新的hash表中。例如第一次写请求会将hash表中第一个索引位置上的entry都迁移到新hash表中,第二次写请求则会将hash表中第二个索引位置上的entry都迁移到新hash表中

这样就可以将rehash的耗时分摊到每次写请求中。

intset

image.png

skiplist

跳表相关的知识点可以看看这篇文章——从内存和磁盘的角度看待数据结构

redis如何存储K-V?

实际上每个Redis数据库都可以看作是一张内部编码为hashtable的hash表。

image.png

hashtable中的每个entry就是一个键值对,而这个KV实际上又属于redisObject数据结构。

  • type表明该对象属于哪种数据类型,例如string、list、hash、set、zset等等
  • encoding表明这个数据类型使用哪种编码方式,例如ziplist、linkedlist、hashtable等等
  • prt就是指向具体存储数据的位置
  • lruLRU模式下,存储键的最后访问时间;LFU模式下,存储访问频率和最近访问时间。
  • ref用于内存回收,当引用计数为 0 时,对象会被自动释放。

image.png