Redis设计与实现笔记--数据结构

目录

简单动态字符串(SDS:simple dynamic string)

应用场景

SDS定义

SDS与C字符串的区别

链表

链表实现

字典

字典的实现

普通状态下的字典

哈希算法

Rehash

渐进式Rehash

跳跃表

使用场景

跳跃表实现

整数集合

整数集合的实现

压缩列表

压缩列表构成

连锁更新


简单动态字符串(SDS:simple dynamic string)

当Redis需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串时,Redis就会使用SDS来表示字符串值。

应用场景

  1. 包含字符串值的键值对在底层都是SDS实现的。
  2. AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。

SDS定义

struct sdshdr {
  // 记录buf数组中已使用字节的数量
  // 等于SDS所保存字符串的长度
  int len;
  //  记录buf数据中未使用字节的数量
  int free;
  //字节数组,用于保存字符串
  char buf[];
};

注意:SDS遵循C字符串以空字符结尾的惯例,保存空字符串的1字节空间不在len属性内。并且空字符串分配额外的1字节空间,以及添加空字符到字符串末尾等操作,是由SDS函数自动完成的。遵循该惯例好处是:SDS可以直接重用一部分C字符串函数库里面的函数

SDS与C字符串的区别

  1. 常数复杂度获取字符串长度。确保了获取字符串长度的工作不会成为Redis的性能瓶颈
  2. 杜绝缓冲区溢出。如:拼接操作。会先检查给定SDS空间是否足够,不够的话,先扩展空间,再执行拼接操作
  3. 减少修改字符串时带来的内存重分配次数。C字符串每次增长或缩短,都要进行一次内存重分配。会导致缓冲区溢出或内存泄漏。SDS实现了空间预分配和惰性空间释放两种优化策略。
    • 空间预分配:如果SDS长度小于1MB,那么将分配和len属性同样大小的未使用空间。如果长度将大于等于1MB,会分配1MB未使用空间。
    • 惰性空间释放:用于优化SDS的字符串缩短操作,当缩短SDS保存的字符串时,不立即使用内存重分配缩短多出来的字节,而是使用free将这些字节数量记录起来,等待将来使用。
  4. 二进制安全。C字符串除了字符串末尾之外,字符串里面不能包含空字符串。使得C字符串只能保存文本数据,不能保存二进制数据。buf属性为字节数组,可以保存一系列二进制数据。通过len属性值判断字符串是否结束。
  5. 兼容部分C字符串函数

链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度。

链表实现

typedef struct listNode {
// 前置节点
struct listNode * prev;
// 后置节点
struct listNode * next;
// 节点的值
void * value;
}listNode;
typedef struct list {
// 表头节点
listNode * head;
// 表尾节点
listNode * tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr,void *key);
}list;

字典

字典,又称为符号表、关联数组或映射,是一种保存键值对的抽象数据结构。

字典的实现

typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引
// 当rehash不在进行时,值为-1
int rehashidx;
} dict;
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于size-1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
}dictht;
typedef struct dictEntry {
// 键
void *key;
// 值
union{
    void *val;
    uint64_tu64;
    int64_ts64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;

普通状态下的字典

哈希算法

当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

Rehash

当哈希表保存的键值对数量太多或太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。可以通过中rehash(重新散列)操作来完成。步骤如下:

  1. 为字典的ht[1]分配空间,如果执行扩展操作,大小设置为第一个大于等于ht[0].user*2的2的n次幂。如果执行收缩操作,大小设置为第一个大于等于ht[0].user的2的n次幂
  2. 将保存在ht[0]的所有键值对rehash到ht[1]上面。
  3. 释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表。

当以下条件中的任意一个被满足时,程序会自动开始对哈希表进行扩展操作:

  1. 服务器目前没有在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
  2. 服务器目前正在进行BASAVE或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

    负载因子公式:负载因子=哈希表已保存节点数量/哈希表大小

当哈希表的负载因子小于0.1时,程序自动对哈希表执行收缩操作。

渐进式Rehash

Rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。这样做是为了避免庞大的数据量计算可能会导致的服务器在一段时间内停止服务。对服务器性能造成影响。步骤如下:

  1. 为ht[1]分配空间,让字段同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个索引计数器变量rehashidx,并将它设为0,表示rehash工作正式开始
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,除了执行指定操作外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值加1。
  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会rehash至ht[1],这是将rehashidx设为-1,表示rehash操作完成。

在进行渐进式rehash期间,字典的删除、查找、更新会在两个哈希表上进行。如:查找一个键,会先在ht[0]里面进行查找,没找到,就会继续在ht[1]里面进行查找。新添加的键值对一律会被保存到ht[1]里面,ht[0]不再进行任何添加操作。这一措施保证了ht[0]包含的键值对数量会只减不增。随着rehash操作的中而最终变成空表。

跳跃表

跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持平均O(logN),最坏O(N)复杂的的节点查找。

使用场景

  • 实现有序集合键
  • 集群节点中用作内部数据结构

跳跃表实现

typedef struct zskiplistNode {
// 层
struct zskiplistLevel {
    // 前进指针
    struct zskiplistNode *forward;
    // 跨度
    unsigned int span;
} level[];

//后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
} zskiplistNode;
typedef struct zskiplist {
// 表头节点和表尾节点
struct skiplistNode *header,*tail;
// 表中节点数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;

由多个跳跃节点组成的跳跃表

带有zskiplist结构的跳跃表

整数集合

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为结合键的底层实现。

整数集合的实现

typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;

contents数组按从小到大的顺序保存集合中的元素。

压缩列表

当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现。

压缩列表构成

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个接单,每个节点可以保存一个字节数组或者一个整数值。

压缩节点构成

previous_entry_length:记录压缩列表中前一个节点的长度,如果前一节点长度小于254字节,长度为1字节。如果大于等于254字节,长度为5字节,第一字节设置为0xFE(十进制254),之后的4字节保存前一节点的长度。

连锁更新

Redis将特殊情况下产生的连续多次空间扩展操作称之为连锁更新。连锁更新最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配最坏复杂度为O(N),所以连锁更新最坏复杂度为O(N^{2})

真正造成性能问题的几率是很低的:

  1. 压缩列表中恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能被引发,实际中,这种情况并不多见。
  2. 其次,即使出现连锁更新,只要被更新的节点数量不多,就不会对性能造成任何影响
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值