数据结构与对象
简单动态字符串(SDS)
SDS字符串只要用到字符数据结构就是用SDS实现
除此之外SDS还可以用来实现AOF缓冲区
SDS数据结构
//记录buf数组中已经使用的字节的数量
//等于SDS锁保存的字符串的长度
int len;
//记录buf数组中未使用的字节数量
int free;
//字节数组用于保存字符串
char buf[];
字符串依旧以'\0'为结尾所以该字符串可以复用很多c语言的函数
由于len的存在可以用常数时间获取到长度
依靠len和free可以解决缓存区溢出问题
依靠len和free,加空间预分配和惰性释放空间的技术可以解决修改增加删除内存空间重新分配的问题。
len解决了不依赖数组长度来计算字符串长度的问题,因此可以使用空间预分配技术
SDS内存分配策略
c语言增减字符串的时候会释放内存重新分配内存。SDS采用两种策略避免这个问题
1.空间预分配问题
字符需要长度小于1mb的时候
内存分配的时候会分配字符所需要的两倍+1,
一份给字符串,另一份给free空间,还有一个字节给'\0'
字符长度需要大于1mb的时候
内存会给字符串需要的长度大小并且分给了1mb未使用空间+1byte
当缩减字符串的时候不会立即释放多余内存而是留作free供以后使用
二进制安全
c语言字符串通过'\0'来判断字符是否结束,但是SDS通过len来判断字符串是否结束,
因此SDS可以保存任意格式的二进制数据
链表
list + listNode节点共同组成 redis链表结构
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值 void *表示是可以应用于任何值
void *value;
}listNode
value的类型为void*,这是一种通用指针,可以指向任何类型的数据。
使用void*类型的好处有以下几点:
增加了链表的通用性,可以存储不同类型的数据,比如整数,浮点数,字符串,结构体等。
降低了链表的耦合性,可以根据不同的数据类型,使用不同的函数来处理value比如比较,复制,释放等。
节省了内存空间,void*类型只占用一个指针的大小,而不是实际数据的大小。
当然,使用void*类型也有一些缺点,比如:
需要额外的空间来存储数据的类型,以便在使用value时进行类型转换。
需要额外的代码来管理value的内存分配和释放,以防止内存泄漏或野指针。
需要额外的代码来检查value的有效性,以防止空指针或错误的类型。
typedef struct list{
//表头结点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数
usigned long len;
//节点复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr)
//节点值对比函数
int (*match)(void *ptr,void *key);
}list
list的head指向链表节点的头结点
list的tail指向链表节点的尾结点
len表示链表节点的长度
字典
哈希表与哈希表节点
typedef struct dicentry{
//键
void *key
//值
unoin{
void *val
uint64_tu64
int64_ts64
}v
//指向下一个哈希表节点形成链表
struct dictEnty *next;
}dictEntry;
typeof struct dictht{
//哈希表节点的指针数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表掩码大小计算索引值 计算键值对存在数组哪个下标
undigned long sizemask
//哈希表已经有的节点数量
unsigned long used;
} dictht;
字典
typeof struct dict{
//类型特定函数
dictType *type;
//私有数据
void *private;
//哈希表
dictht ht[2];
//rehash 索引
// reshah不在进行的时候值为-1
int trehashid;
}dict;
为什么会有两个dictht后面渐进式hash会讲。
哈希算法
利用键值对的KEY值可以求出hash值,hash值与掩码进行&操作后对应的是dicEntry*数组中的值
rehash
rehash为了让负载因子维持在一个合理的范围内,hash表保存的键值对,数量太多或太少的时候需要对哈希表进行扩展或者收缩这时候需要rehash
rehash实际上是渐进进行的因为hash表中数据过多可能导致性能问题所以逐步进行
rehash步骤详情如下:
1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2) 字典中维持了一个索引rehashidx rehah是为0 没有rehash时为-1
3)在rehash期间所有操作会对两个哈希表一起做,并且期间把ht[0]的数据搬到ht[1]
4)当rehash结束所有数据转移完毕之后会将rehashidx的值设置为-1
5)渐进式rehash期间删改查等操作会在两个表上进行,现在ht0 里找后再ht1里找
6)增加操作只在ht1里执行不在ht0里执行确保ht0迅速清空
7) 结束后ht1变为ht0
跳跃表
跳跃表(zskiplist和zskiplistNode 跳跃表和跳跃表节点)
跳跃表辅助结点zskiplist
用来管理跳跃表链表
typedef stuct zskiplist{
//表头节点和表尾节点
structz skiplistNode *header,*tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
参考上图
level 层数 一共五层 所以level等于5
length 表示一共有几个数据
跳跃表节点
跳跃表头结点不用来存储数值方便进行操作
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}
内部数据结构解释
跳跃表节点level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
层数随机生成这样可以保证跳跃表的平衡性,使得查找、插入和删除的时间复杂度都是 O (log N)使用了幂次定律保证越大的数生成概率越小
前进指针指向同层的下一个节点,后退节点指向后面一个节点
跨度用来表示到下一个节点的距离
成员对象表示存储的对象
分数用来进行排序的数值大小
整数集合
typedef struct inset{
//编码方式
unit32_t encoding;
//集合包含的元素数量
unit32_t length;
//保存元素数组
int8_t contents[]
}
contents整数集合,且不包含任何重复元素,还按值得从小到大排列
length代表元素数组长度
encoding决定元素类型
升级
遇到加入元素类型大于之前的元素的时候要进行升级
所有元素都要升级到和加入元素一样大的类型,之后在加入数组
数组不支持降级操作
压缩列表
列表键:只包含较少列表项并且每个列表项的值是长度较短的字符串或整数
HASH:只包含较少的键值对,并且每个键值对是长度较短字符串或整数
简单来说只有有小又少又短才使用压缩列表
从左到右依次表示所占字节数,列表尾节点字节数(方便删除),记录压缩列表节点数,entry记录列表节点的内容,zlend表示压缩列表末端
entry
pre_entry_length 记录前一个节点的长度
前置长度的作用是为了实现双向链表的效果,可以从任意节点开始向前或向后遍历压缩列表。
如果没有前置长度,那么只能从头部开始向后遍历,无法从尾部开始向前遍历,
这样会降低压缩列表的灵活性和效率。
encoding:用来保存本节点长度 和编码类型
content用来保存节点的值值得类型和长度由节点的encoding属性来决定
可能会有连锁更新问题影响性能将性能降低为平方级别
连锁更新导致原因:前一个节点大小会影响previous_enty_length而previous_entry_length大小的变化会影响整个节点的大小变化,可能会引发连锁反应影响整个链表的变化。
对象篇章
前方的数据结构并不是直接在Redis中使用而是用数据结构构造了很多不同类型对象来进行使用
对象的类型与编码
Redis使用对象来表示数据中的键和值,另一个对象作键值对的值
typedef struct redisObject{
//类型
unsigned type:1;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
}robj;
TYPE : 字符串对象 列表对象 哈希对象 集合对象 有序集合对象
encoding:底层实现使用上文的数据结构有以下几种
long embstr 简单动态字符串 字典 双端链表 压缩列表 整数集合 跳跃表和字典
每种对象的实现
字符串对象
整数值实现(保存的是数值并且可以用long表示)
embstr编码的简单动态字符串实现(embst )
简单动态字符串实现(raw 并且这个子串长度大于32字节)
列表
使用压缩列表实现的列表对象(对象少的时候有优势对象,对象长度小于64,数量少于512)
使用双端链表实现的列表对象(对象多的时候有优势)
哈希
使用压缩列表实现的哈希对象(哈希对象所有键值对键和值得字符串长度都小于64字节,512个一下)
使用字典实现的哈希对象(不满足压缩列表)
集合对象
使用整数集合实现的集合对象(intset编码 所元素为整数值 且 < 512个)
使用字典实现的集合对象
有序集合对象
使用压缩列表实现的有序集合对象
使用跳跃表和字典实现的有序集合对象
单机数据库
键值相关
过期相关
一个数据库设置过期时间时,服务器会在数据库的过期字典(redisDb)中关联给定的数据库建和过期时间。
过期字典:过期字典的键是一个指针指向一个键值,过期字典的值是一个long类型的过期时间是所指向键对应的过期时间
是否过期判定 : 过期的话检查是否存在于过过期字典如果存在过,与过期时间对比当前时间大于过期时间则已经过期
过期键删除策略
定时删除
创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
优点:节约内存,到时就删除,快速释放掉不必要的内存占用
缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量
定期删除
周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间 (随机抽查,重点抽查)
惰性删除
数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;
发现已过期,删除,返回不存在。
优点:节约CPU性能,发现必须删除的时候才删除
缺点:内存压力很大,出现长期占用内存的数据
总结:用存储空间换取处理器性能(拿空间换时间)
场景:新跟那个要求高缓存数据小
caffine提供三种缓存驱逐策略
基于容量: 设置缓存数量上限
基于时间: 设置过期时间
caffine缓存是一个基于Java 8开发的高性能本地缓存组件,
它使用了W-TinyLFU算法来提供接近最佳的命中率,
并且支持多种淘汰策略和异步刷新机制。caffine缓存的底层实现主要包括以下几个方面:
读写缓冲:caffine缓存使用了环形队列和MPSC队列来缓存读写操作,从而减少锁的竞争和提高并发性能。
淘汰算法:caffine缓存使用了W-TinyLFU算法来决定哪些数据应该被淘汰,该算法利用了Count-Min Sketch数据结构来统计数据的访问频率,并且维护了一个PK机制来保证新上的热点数据能够缓存。
过期策略:caffine缓存支持多种过期策略,包括基于写入时间、访问时间、自定义时间等,它使用了时间轮算法来实现不同key的不同过期时间。
异步刷新:caffine缓存支持在数据过期后异步地刷新数据,从而避免阻塞读操作,它使用了CompletableFuture来实现异步回调