redis中的SDS
Simple Dynamic String 简单动态字符串
定义了一个sds结构体,使用不同的结构体来保存不同长度大小的字符串
sdshdr5/8/16/32 分别对应 长度为2^5-1等
除了一个char buf[]
成员变量之外还有
len; /* 字符串长度,buf已用的长度 */
alloc; /* 为buf分配的总长度,alloc-len就是sds结构体剩余的空间
unsigned char flags; /* flags共8位,低三位保存类型标志,高5位保存字符串长度/
具体使用哪一个结构体,sds底层是通过flags属性与SDS_TYPE_MASK
做与运算得出具体的类型
通过这样的设计,使得sds相与C中的字符串有了以下特点:
-
常数复杂度获取字符串长度
-
减少缓冲区溢出
在修改SDS之前都会判断他是否有足够的空间完成接下来的操作。在执行memcpy修改sds的buf[]之前都会调用
sdsMakeRoomFor
函数去检查sds字符串s是否有足够的空间,如果没有足够空间,就为其分配足够的空间,从而杜绝了缓冲区溢出。如果修改后的长度小于1M,则分配的空间是原来的2倍,否则增加1MB的空间 -
减少了内存重新分配次数
惰性空间释放:在操作减少了字符串大小的时候,对应的
sdstrim
函数其实只是修改了len的值,并没有第一时间把多余空间释放掉,下次需要增加sds字符串大小的时候,就不想要再为其分配空间了。 -
二进制安全
dict
当集合对象的元素数量小于512个时,且所有元素为整数,会使用intset作为底层实现。
dict结构体里其实有两个哈希表,通过定义dictht ht[2]
存储,实际使用的是ht[0]
,ht[1]
是在重哈希的时候使用的。
dictht 哈希表
/* 哈希表结构 */
typedef struct dictht {
dictEntry **table; /* 哈希表节点数组 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 哈希表大小掩码,用于计算哈希表的索引值,大小总是dictht.size - 1 */
unsigned long used; /* 哈希表已经使用的节点数量 */
} dictht;
哈希表节点
/* 哈希表节点 */
typedef struct dictEntry {
void *key; /* 键名 */
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; /* 值 */
struct dictEntry *next; /* 指向下一个节点, 将多个哈希值相同的键值对连接起来*/
} dictEntry;
https://www.hoohack.me/assets/images/2018/01/dict.png
重哈希
不仅仅当哈希表元素过多时,会进行重哈希,元素表过少的时候,也会进行重哈希,因为redis要保证负载因子处于一个相对合理的状态
渐进式重哈希
在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
以下是哈希表渐进式 rehash 的详细步骤:
- 为
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 操作已完成。
链表
当列表对象中的元素数量大小于512个,使用压缩链表实现,否则使用链表
在redis中,例如列表键的底层实现之一就是链表。而且,在redis中的链表结构被实现成为双向链表,因此,在头部和尾部进行的操作就会非常快。
跳跃表
若一个有序集合包含的元素数量比较多,或者有序集合中的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
压缩列表
当列表对象中的元素数量大小于512个,使用压缩链表实现,否则使用链表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度较短的字符串时;或者当哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么是长度较短的字符串时,那么Redis就会使用压缩列表来做为列表键或哈希键的底层实现。
每个节点由三部分组成:prevlength、encoding、data
- prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist
- encoding: 当前节点的编码规则,下文会详细说
- data: 当前节点的值,可以是数字或字符串
为了节省内存,根据上一个节点的长度prevlength 可以将ziplist节点分为两类:
图2 entry布局
- entry的前8位小于254,则这8位就表示上一个节点的长度
- entry的前8位等于254,则意味着上一个节点的长度无法用8位表示,后面32位才是真实的prevlength。用254 不用255(11111111)作为分界是因为255是zlend的值,它用于判断ziplist是否到达尾部。
连锁更新
由于压缩列表的'previous_entry_length'属性可能是1字节或5字节,若在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点,则添加新节点或删除节点都有可能会引发多个节点的连续多次空间扩展,这种现象称之为“连锁更新”。
因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作, 而每次空间重分配的最坏复杂度为O(N), 所以连锁更新的最坏复杂度为O(N^2)。但“连锁更新”发生的概率是很低的,所以不必担心其会影响压缩列表的性能。