小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
数据结构
简单动态字符串
struct sdshdr{
// 已使用字节数
int len;
// 未使用字节数
int free;
// char数组,用于保存字符串
char buff[];
}
- O(1)时间复杂度获取字符串长度
- 杜绝缓冲区溢出
- 通过空间预分配和惰性空间释放减少内存重分配的次数
- 二进制安全
链表
typedef struct listNode{
struct listNode* prev;
struct listNode* next;
void* value;
} listNode;
typedef struct list{
listNode *head;
listNode *tail;
unsigned long len;
} list;
字典
typedef struct dict{
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx; // 没有进行hash时为-1
}
typedef struct dictht{
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry{
void *key;
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
} dictEntry;
采用了MurmurHash2,即使输入是有规律的,算法仍能给出一个很好的随机分布性,计算速度也很快。
采用链表法解决hash冲突
rehash
为了让哈希表的负载因子维持在一个合理的范围,当哈希表保存的键值对太多或太少的时候,程序需要对哈希表进行扩展或者收缩。
步骤:
- 为字典的ht[1]哈希表分配空间。扩展时:new_size = 2^n >= ht[0].used * 2; 收缩时:new_size = 2^n >= ht[0].used
- 将保存在ht[0]中的所有键值对重新计算哈希值和索引值,并放置在ht[1]的对应位置上
- 当ht[0]中的所有键值对都迁移到ht[1]中后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]创建一个空白哈希表,为下一次rehash做准备。
触发机制:
- 负载因子>=1且没有执行bgsave或者bgrewriteaof命令,扩展
- 负载因子>=5且正在执行bgsave或者bgrewriteaof命令,扩展
- 负载因子<0.1,收缩
渐进式rehash
rehash动作并不是一次性、集中式地完成,而是分多次、渐进地完成,将rehash均摊到对字典的每个添加、删除、查找和更新操作上。这样可以避免服务器长时间的停止服务。
使用rehashidx记录rehash的进度,每次增删改查时顺带将rehashidx指向的键值对rehash到ht[1]上。删改查首先在ht[0]上进行查找,没有找到再到ht[1]进行查找;增只在ht[1]上进行,确保ht[0]只减不增,最终变成空表。
跳表
跳表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序操作来批量处理节点。
大部分情况下,跳表的效率可以和平衡术相媲美树。因为跳表的实现比平衡术更加简单,所以有不少程序都是用跳表来代替平衡树。
typedef struct zskiplist{
// header:名义头节点,不存储数据
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
}
typedef struct zskiplistNode{
// 层
struct zskiplistLevel{
struct zskiplistNode *forward;
// 两个节点之间的距离
unsigned int span;
} level[];
struct zskiplistNode *backward;
double score;
robj *obj;
} zskiplistNode;
每次创建一个跳表节点时,依据幂次定律随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。
整数集合
整数集合是集合键的底层实现之一,底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在需要时,会根据新添加元素的类型改变这个数组的类型,即升级操作,但不支持降级操作。
压缩列表
压缩列表是列表和哈希的底层实现之一,是为了节约内存而开发的,是由一系列的连续内存块组成的顺序型数据结构。
| zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
| previous_entry_length | encoding | content |
借助previous_entry_length可以实现从表尾向表头的遍历。
previous_entry_length可能为1一个字节也可能为5个字节,当有多个连续的、长度介于250-253字节的节点时,进行插入或者删除操作可能会导致连锁更新,但这种操作出现的几率并不高。
对象
字符串
int, row, embstr
embstr:是专门用于保存短字符串的一种优化编码方式,raw编码会调用两次内存分配来分别创建redisObject结构和sdshdr结构,而embstr编码则通过一次内存分配来分配一块连续空间。embstr是只读的
列表
ziplist, linkedlist
编码转换:
- 列表保存的所有字符串元素的长度都小于64字节
- 元素数量小于512个
满足上述两个元素时使用ziplist,否则使用linkedlsit
哈希
ziplist, hashtable
编码转换:
- 保存的所有键值对的长度都小于64字节
- 元素数量小于512个
满足上述两个元素时使用ziplist,否则使用hashtable
集合
intset, hashtable
编码转换:
- 保存的所有元素都是整数值
- 元素数量小于512个
满足上述两个元素时使用intset,否则使用hashtable
有序集合
ziplist, skiplist
性能优化
- 内存回收:引用计数实现内存回收机制,在使用的时候释放对象进行内存回收。
- 对象共享:将键指向一个现有的值对象,并将值对象的引用数+1。redis只对整数值(0-9999)的字符串对象进行共享。
- 对象空转时长:lru,记录对象最后一次被访问的时间。