常用数据结构
简单动态字符串(SDS)
-
结构:
struct sdshdr{ // 记录buf中已使用的字节数量,也是字符串的长度 int len; // 记录buf数组中未使用字节的数量 int free; // 字节数组,用于保存字符串 char buf[]; };
-
相较于C字符串的优势:
- 常数复杂度获取字符串的长度
- 杜绝缓存区溢出:如两个C字符串连接时,可能由于空间不够会出现内存溢出,但是SDS会自动检查free是否足够,并自动扩展空间
- 减少修改字符串时带来的内存重分配次数:无需每次修改字符串都重新修改内存大小,通过空间预分配和惰性空间释放两种策略进行优化
- 空间预分配:当大小小于1M时,free空间大小分配为len长度大小,当大小大于1M时,free空间分配为1M,只有free无法满足扩展的大小时会进行重新分配大小
- 惰性空间释放:缩短字符串后,不会释放空间,除非调用API,进行释放
- 二进制安全:buf中存储的是通过二进制转换的数据
- 兼容部分C字符串函数:由于buf最后以空字符串结尾,所以能够复用C字符串的函数
链表
-
结构
-
链表节点:
typedef struct listNode { // 前置节点 struct listNode * prev; // 后置节点 struct listNode * next; // 值 void * value; }listNode;
-
链表:
typedef struct list { // 表头节点 struct listNode * prev; // 表尾节点 struct listNode * next; // 链表所包含的节点数量 unsigned long len; // 节点值复制函数 void *(*dup)(void *ptr); // 节点值释放函数 void (*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr,void *key); }list;
-
-
特性:
- 双端:链表节点往前和往后遍历都为O(1)
- 无环:表头的prev和表尾的tail都指向null
- 带表头表尾:表头表尾获取为O(1)
- 链表长度计数:长度获取为O(1)
- 多态:值可以存储为任意类型
字典
-
结构:
-
字典:
typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表,有两个是为了进行rehash操作 dictht ht[2]; // rehash 索引 //当rehash,不在进行时,值为-1 in trehashidx; /* rehashing not in progress if rehashidx == -1 */ } dict;
-
哈希表:
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值,总是等于size-1,key的hash值与该值进行与操作,从而计算出索引位置 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
-
哈希表节点:
typedef struct dictEntry { // 键 void *key; // 值 union{ // 存指针 void *val; // uint64整数 uint64_tu64; // int64整数 int64_ts64; } v; // 指向下个哈希表节点,形成链表,当有hash冲突时,新节点总是加入到表头的位置,从而保证插入的复杂度为O(1) struct dictEntry *next; } dictEntry;
-
-
特点:
-
每个字典有两个哈希表,一个用来存储数据,一个用来rehash使用
-
解决hash冲突的方法使用单向链表解决
-
hash表扩展和收缩时使用渐进式的方式进行rehash
-
rehash触发条件:
-
服务器没有执行BGSAVE或者BGREWREITEAOF命令时,负载因子大于等于1
-
服务器在执行BGSAVE或者BGREWREITEAOF命令时,负载因子大于等于5
-
当哈希表负载因素小于0.1时,会执行搜索操作
注:1. 负载因子 = 哈希表已保存数量 / 哈希表大小
2. 是否在执行BGSAVE或者BGREWREITEAOF命令时,负载因子的大小不一样的原因是,在执行BGSAVE或者BGREWREITEAOF命令时,扩大负载因子,从而减少rehash,从而为执行命令节约内存
-
-
渐进式rehash:
- 将ht[0]拷贝ht[1]的动作均摊到每一次对hash表的添加、删除、查找或者更新操作上,从而避免集中式的庞大计算量,每拷贝ht[0]的一个索引,都会在rehashidx上进行记录拷贝的位置
- 添加只会往ht[1]添加,删除、查找或更新则会先在ht[0]操作,若没有再到ht[1]操作
- rehash完成后,ht[0]和ht[1]的引用对调,rehash索引变为-1
-
-
跳跃表
-
结构:
-
跳跃表结构:
typedef struct zskiplist { // 表头节点和表尾节点 structz skiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
-
跳跃表节点结构:
typedef struct zskiplistNode { // 层 每次都会随机生成一个1-32之间的值作为level数组的大小,也就是层的高度,生成的算法是志越大生成的概率越小 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[]; // 后退指针 struct zskiplistNode *backward; // 分值 double score; // 成员对象 robj *obj; } zskiplistNode;
-
-
特点:
- 相较于红黑树的缺点:占用更多的内存,每个节点的大小取决于节点的层数
- 相较于红黑树的优点:
- 实现比红黑树简单
- 比红黑树更容易扩展
- 高并发时,红黑树需要旋转附近节点,需要加锁,跳跃表不需要考虑
-
操作:
- 根据分值查询:从最上层节点进行查询,当发现分值小于后续节点时,返回前一节点由次之的层级再次进行查询
- 根据排名查询:从最上层节点进行查询,当跨度大于排名时,返回前一节点由次之的层级再次查询,直至跨度累计的和等于排名
- 插入:
- 先根据分值查询插入的位置,查询时将每一层级到该节点的跨度rank[]和每一层级上一节点的引用update[]进行记录
- 随机生成该节点的层数,并创建节点
- 根据update[]插入节点,并对跨度信息进行更新
- 删除:
- 先根据分值查询插入的位置,查询时将每一层级到该节点上一节点的引用update[]进行记录
- 将引用引向后续节点,跳过要删除的节点
- 释放删除节点的内存
- 更新:
- 若没有更新排名:修改节点的数据
- 若更新排名:先删除,再插入
整数集合
-
结构:
typedef struct intset { // 编码方式,数组中长度最大的元素决定编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组,不重复,有顺序地存储在数组中 int8_t contents[]; } intset;
-
特点:
- 支持升级:
- 好处:提高灵活性,节约空间
- 升级步骤:
- 内存空间先扩大
- 原有元素从大到小依次分配到新的空间位置
- 新元素分配到数组位置中(新元素最后加入的原因是,新元素导致升级,所以长度最长,所以新元素是最值)
- 不支持降级
- 支持升级:
压缩列表
- 结构:
- 压缩列表结构:
- zlbytes:记录压缩列表占用的内存大小。内存分配和计算zlend时使用
- zltail:记录表尾节点距离压缩列表起始位置的字节数量,从而能够直接找到表尾节点
- zlen:压缩列表的节点数量,当大于65535时,只能遍历求出数量
- entryx:长度不定,会存在多个元素,记录节点数据,数据为字节数组或整数值
- zlend:特殊值0xFF,用于标记是压缩列表的表尾位置
- 压缩列表节点结构:
- previous_entry_length:前一个节点的字节长度,用于从表尾向前遍历,找到指针
- encoding:保存数据的类型和长度
- content:保存数据,可以是整数或字节数组
- 压缩列表结构:
- 特点:
- 压缩列表相较于链表,能够更好地节约内存空间
- 遍历效率相对较低
- 添加和删除节点会出现连锁更新的情况,但概率较低
对象
对象概述
-
结构:
typedef struct redisObject { // 类型,主要分为字符串对象、列表对象、哈希对象、集合对象和有序集合对象 unsigned type:4; // 编码,主要分为 INT、EMBSTR、RAW、HT、LINKEDLIST、ZIPLIST、INTSET、SKIPLIST unsigned encoding:4; // 指向底层实现数据结构的指针 void *ptr; // 对象的空转时长,用于计算对象多久没有被访问,当开启相关配置时,会先回收空转时长较长的对象 unsigned lru:22; } robj;
-
特点:
- redis的对象采用引用计数实现内存回收机制,当一个对象不被使用时,该对象的内存会自动释放
- redis会共享0-9999的字符串对象
字符串对象
encoding | 区别 | 转换 |
---|---|---|
int | 存储整数 | 数字操作时,直接操作,字符串相关操作时,先转换为raw,再进行操作 |
raw | 存储长度大于32字节的字符串 | 数字操作时,看是否能转换为数字进行操作,不能返回错误提示直接操作,字符串相关操作时,直接操作 |
embstr | 存储长度小于等于32字节的字符串,相对于raw,内存分配和释放相对于raw的两次只需一次,原因是redisObject和sdshdr存储在连续的内存中 | 数字操作时,看是否能转换为数字进行操作,不能返回错误提示直接操作,字符串相关操作时,转换为raw,再进行操作 |
列表对象
encoding | 区别 | 转换 |
---|---|---|
ziplist | 节约内存空间 | 同时满足以下条件,使用ziplist,不满足使用linkedlist:1.列表存储的元素数量小于5等于12个;2.每个字符串元素的长度小于等于64字节(上限值可修改) |
linkedlist | 插入、删除和遍历效率较高 | 同上 |
哈希对象
encoding | 区别 | 转换 |
---|---|---|
ziplist | 节约内存空间 | 同时满足以下条件,使用ziplist,不满足使用hashtable:1.哈希对象存储的键值对数量小于等于512个;2.每个键和值的字符串的长度小于等于64字节(上限值可修改) |
hashtable | 插入、删除和查询效率较高 | 同上 |
集合对象
encoding | 区别 | 转换 |
---|---|---|
intset | 存储整数,节约空间 | 同时满足以下条件,使用intset,不满足使用hashtable:1.哈希对象存储的键值对数量小于等于512个;2.存储元素都是整数值(上限值可修改) |
hashtable | 存储整数或字符串 | 同上 |
有序集合对象
encoding | 区别 | 转换 |
---|---|---|
ziplist | 每个集合元素使用两个紧挨着的压缩列表节点存储,第一个节点保存元素成员,第二个节点保存分值,节约空间 | 同时满足以下条件,使用ziplist,不满足使用skiplist:1.元素数量小于等于128个;2.存储元素的所有成员小于64字节(上限值可修改) |
skiplist | 实际使用zset结构保存,一个zset包含一个字典和一个跳跃表,且由于字典和跳跃表的元素引用指向共用同一地址,所以不会浪费额外内存 | 同上 |
-
zset结构:
typedef struct zset { // 跳表保证范围查询的效率,平均O(logN),最坏O(N) zskiplist *zsl; // 字典保证定位元素分值大小的效率为O(1) dict *dict; } zset;