###Redis版本:Redis5.0.14
###人生建议,一定要按照对应的版本阅读,否则会相当难受
一、redis的简介、特性、部署
· 简介
Redis是一个开源的,内存中的数据结构存储系统,它可以用作数据库、缓存、消息中间件。
需要注意他和levelDB的区别。
· 特性
1. 性能高
2. 丰富的数据类型
3. 支持事务
4. 内建replication和集群 (主从复制、Redis Cluster)
5. 支持持久化 (RDB设置阈值存储、AOF保存每一条指令)
6. 单线程、原子性操作
· 压测
官方自带性能测试工具
redis-banchmark [-h] [-p] [-c] [n] [-k]
二、简单动态字符串(SDS)
· 数据结构
redis为了节省内存,针对不同长度的数据采用不同的数据结构。
#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
但是SDS_TYPE_5并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放。
typedef char *sds;
// __attribute__ ((__packed__))用来表示取消结构在编译过程中的优化对齐
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 表示数据长度 */
uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度 */
unsigned char flags; /* 3 lsb of type, 5 unused bits ,小端*/
// 变长数据
char buf[];
};
· 空间扩容
1. 当前有效长度>=新增长度,直接返回 sdsavail(s)=(当总长度alloc-当前长度len) >= 新增长度addlen
2. 如果newlen < SDS_MAX_PREALLOC,翻倍增; (SDS_MAX_PREALLOC=1024*1024)
否则增到newlen;
更新类型为newlen对应的SDS_TYPE
3. 更新之后,判断新旧类型是否一致:
一致使用remalloc:重新分配内存
如果不是相同类型:使用malloc+free
重新分配新内存,然后把老数据copy到新数据中,再把老数据free释放。
· 空间缩容
在sdstrim操作时,采用的是惰性空间释放;即,不会立即使用内存重分配来回收缩短的字节,只是进行移动和标记,并修改数据长。
真正的删除:sdsRemoveFreeSpace()
· 优点:
1. 常量获取字符长度 (直接查len字段)
2. 避免缓冲区溢出 (alloc和len对比)
3. 减少字符串修改带来的内存频繁重分配次数 (成倍扩容)
4. 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据 (视频流数据)
5. 以'\0'结尾,兼容C字符串函数
· 从typedef char *sds可以发现,sds时char* 的别名,可以理解为分配的是一块连续内存,
根据局部性原理可以提高访问速度。
· 数据存储不使用 SDS_TYPE_5 ,是因为这个类型每次更新数据时都要进行扩容。
· 利用C语言内存布局,既可以保证变长,又能保证内存连续,
在sds的函数操作中,可以发现频繁使用s[-1],是因为C语言这样表示前一位(buf位)
三、链表(adlist)
· 数据结构
redis的链表就是普通的双向链表
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
}listNode;
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
· 迭代器
这里链表的迭代和数据时分开的,采用了类似迭代器模式,这个思想被用到很多场景(如 leveldb)
typedef struct listIter {
listNode *next;
int direction; //方向
} listIter;
listIter *listGetIterator(list *list, int direction)
{
listIter *iter;
if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
if (direction == AL_START_HEAD)
iter->next = list->head;
else
iter->next = list->tail;
iter->direction = direction;
return iter;
}
四、字典(dict)
· 数据结构:
非常像Golang中的map
typedef struct dictEntry {
void *key;
union { // union节省内存
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 采用链地址法,针对新节点采用头插法
void *metadata[]; // 对齐
} dictEntry;
值得注意的是7.0.0中把原有的dictht整合到了dict中,把定位和size加入了defind
#define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
struct dict {
dictType *type; //记录一些回调函数
dictEntry **ht_table[2]; // 两个表,表里存dictEntry,一般只会使用0,ht[1]哈希表只会对ht[0]哈希表进行rehash操作
unsigned long ht_used[2]; // 记录使用情况
long rehashidx; // 记录rehash的进度,为-1表示不会进行rehash
int16_t pauserehash; // 记录rehash状态
signed char ht_size_exp[2]; //两个表的大小
};
· size的设置每次都是2的幂或者扩到最大,和Golang一样
· rehash==-1代表没有在rehash
· 定位用的是取模的方法,和Golang一样
#define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
· 扩容,缩容,除了细节其他和GOlang一样
扩容总结:
1. 先判断是否正在持久化;
2. 校验合法性,并容量size分配为2的幂(或者是虽大限额)
3. 迁移完成,释放ht[0],将ht[1]指向ht[0],并初始化ht[1],等待下一次使用
缩容总结:
1. 当超过了初始值且填充率小于10%,代表字典中空洞很多;或者心跳函数触发
2. 判断是否需要缩容;
3. 确保内存尽量==1:1
· 渐进式rehash:
指rehash不是一次性、集中式完成的;
1. redis是单线程的,如果hash的key太多,rehash可能会阻塞服务器过长,
所以redis本身将rehash操作分散在了后续的每次增删改查中
2. 定时rehash,每次1ms
· rehash注意点
1. rehash过程中添加新的字典,只会插入到ht[1]中,确保ht[0]只减不增;
2. 持久化过程中,服务器rehash的负载因子会变高,默认是5 (Golang语言,时刻复习Golang ahha)
· 迭代器
1. 安全模式:
支持一边遍历一边增删改查,但是不支持rehash
2. 非安全模式
只支持只读模式,但是支持rehash;使用增删改查操作会造成不可预期的问题
· 跳表
src找不到了hahaha找不到,算法栏目中有一个python的跳表
五、整数集合(intset)
· 数据结构:
整数集合(intset)是redis用于保存整数值的集合抽象数据结构,他也可以保存类型为int16、int32、int64的值;
且保证集合中不会出现重复元素,数据也是从小到大存储。
typedef struct intset {
uint32_t encoding; // 表示元素类型,默认是INTSET_ENC_INT16
uint32_t length; // 表示元素个数,按需分配元素
int8_t contents[]; // 动态数组,按需分配空间,按照从小到大的顺序保存元素
} intset;
· 数据操作
1. 查找:采用的是折半查找
2. 插入和升级:
当新插入的元素类型大于当前intset类型时,为防止溢出,会对其进行升级;
1. 计算输入值编码类型
2. 判断编码是否大于当前intset编码
如果不是:
3. 查找插入最佳位置
4. remalloc新的内存,更新intset的长度信息
如果是:
3. 更新当前intset编码,并添加元素
4. 分别计算输入值和当前intset编码类型
5. remalloc新的内存,更新intset的长度信息
6. 按照新编码类型,从后向前重新把元素放到正确的位置
7. 插入元素
intset会有一个优化,先进行首尾比较再二分
· 特点:
1. 灵活性
可以通过自动升级底层数组来适应新元素,所以可以将任意类型的整数添加至集合,而不用担心类型错误
2. 节约内存
不同类型采用不同类型的空间对其存储,从而最小的避免空间浪费
3. 不支持降级
4. 添加和删除都要进行remalloc操作,会引起性能波动,慎用
六、压缩列表
· 数据结构:
压缩列表是有一系列特殊编码的连续内存组成的顺序性数据结构,一个压缩列表可以包含任意多个节点;
每个节点可以保存一个字节数组或者一个整数值。适合存储小对象和长度有限的数据。
ziplist数据结构没有被真正的定义出来,而是通过宏定义提取出来;
存储在ziplist的entry并非按照zlentry定义的,他也是通过宏来总结出来的
// 只用于接受,并不是实际存储的
typedef struct zlentry {
unsigned int prevrawlensize;
unsigned int prevrawlen;
unsigned int lensize;
unsigned int len;
unsigned int headersize;
unsigned char encoding;
unsigned char *p;
} zlentry;
· 总的来说就是通过头位置和偏移量来找到内存中的位置
· prevrawlen 记录前一个节点的长度,以字节为单位。
当前前一个字节长度小于254时,prevrawlen只需要一个字节;
否则prevrawlen需要5个字节来进行存储,其中第一个固定为0xFE(254),后四位存储前一个字节的长度;
可以根据该属性快速定位到前一个节点的起始位置,支持反向遍历
· 注意点
1. 查找的时间复杂度为O(N)
2. 列表的长度超过了UINT16_MAX,此时,zllen不再表示节点的个数
3. 连锁更新
在entry会存储前一个节点的长度;
因此,当插入或删除操作,会打破ziplist具有的特性,此时需要进行节点的更新;
reids中只处理节点的扩张,即由1个字节变成5个字节,不进行收缩操作。
最差情况是:连接更新对ziplist执行N次空间分配操作,而每次空间分配的最坏复杂度为O(N),所以连锁更新最坏的情况是O(N^2)
Redis源码学习----Redis的基本数据结构
于 2022-05-12 22:37:59 首次发布
本文详细介绍了Redis中的核心数据结构,包括SDS、链表、字典和整数集合,强调了它们的特性、优化策略以及在实际操作中的注意事项。例如,SDS提供了常量时间获取字符串长度和避免缓冲区溢出的能力,链表采用了双向链表结构,字典则利用了哈希表和渐进式rehash策略。同时,文章也提到了压缩列表的存储方式和潜在的性能问题。
摘要由CSDN通过智能技术生成