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内部编码类型
SDS
首先要知道c语言中不存在专门的字符串数据类型,字符串使用字符数组来存储的,其中通过'\0'来判断是否到达字符串的结尾。为了更方便的操作字符串,Redis内部自定义了一种SDS(Simple Dynamic String)这种数据类型,主要有三个字段——free、len、buf:
- free:剩余的空间
- len:判断是否到达字符串结尾
- buf:指向存储字符串的地址
而SDS又分为embstr和raw两种:
-
emstr:redisObject和SDS存储地址是连续的,也就是说只需要一次性申请内存即可。但由于紧凑的内存分配,导致SDS扩容时,需要对redisObject和SDS同时分配空间。
-
raw:redisObject和SDS存储地址是分离的,也就是说需要连续申请两次内存。
那么是什么时候使用embstr,什么时候又使用raw呢?
- redis 2.+版本 当字符串小于等于32字节时,使用embstr
- redis 3.0-4.0版本 当字符串小于等于39字节时,使用embstr
- redis 5.0版本后 当字符串小于等于44字节时,使用embstr
ziplist以及listpack
为什么内存利用率高?
- 使用该编码方式的数据类型,所有数据都将一个接一个的保存,这将保持内存的高度紧凑,尽可能地避免了内存碎片的出现,故提高了内存利用率。
- 除此之外,保存数据时会对每个数据再编码一次,使得保存的数据进一步地压缩,从而利用更少的内存空间保存更多的有效数据,故提高了内存利用率。
为什么会出现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
当list的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值的字节都小于list-max-ziplist-value配置时(默认64字节),使用的是ziplist。
由于ziplist的级联更新问题,在元素个数多的时候,性能将会急速下降,故切换为双向链表的形式。但是这也带来了内存不够紧凑的问题,而根据局部性原则可知,这将会降低访问效率,除此之外也可能导致出现大量的内存碎片。
在Redis 3.2版本后,Redis使用了quicklist作为唯一的一种list内部编码方式
linkedlist是一个个节点相连起来的,内存不够紧凑连续。而quicklist的每个节点的数据结构是ziplist,当超出元素数量阈值时就创建新ziplist节点,这既避免了内存碎片,又提高了访问效率,还可以避免级联更新现象。
hashtable
hashtable发生节点冲突时,采用的是拉链法来解决。
hashtable比较重要的就是扩容rehash的过程。而Redis采用的是渐进式rehash,之所以要采用渐进式rehash就是为了在rehash的同时保证可以继续进行hash表的写入,而不是阻塞hash表写入操作直到rehash完毕。
- hashtable中实际上有两个hash表,当进行rehash时,就会将新数据写入另一个空的hash表中。
- 除此之外每个写请求还会将原hash表的一个索引位置上所有entry都迁移到新的hash表中。例如第一次写请求会将hash表中第一个索引位置上的entry都迁移到新hash表中,第二次写请求则会将hash表中第二个索引位置上的entry都迁移到新hash表中
这样就可以将rehash的耗时分摊到每次写请求中。
intset
skiplist
跳表相关的知识点可以看看这篇文章——从内存和磁盘的角度看待数据结构
redis如何存储K-V?
实际上每个Redis数据库都可以看作是一张内部编码为hashtable的hash表。
hashtable中的每个entry就是一个键值对,而这个KV实际上又属于redisObject数据结构。
- type表明该对象属于哪种数据类型,例如string、list、hash、set、zset等等
- encoding表明这个数据类型使用哪种编码方式,例如ziplist、linkedlist、hashtable等等
- prt就是指向具体存储数据的位置
- lruLRU模式下,存储键的最后访问时间;LFU模式下,存储访问频率和最近访问时间。
- ref用于内存回收,当引用计数为 0 时,对象会被自动释放。