@(Redis)
一、数据结构与对象
1.SDS
SDS是Redis实现的一个字符串数据结构。
结构:
struct sdshdr {
int len; //字符串长度
int free; //未使用的字符串长度
char buf[] //保存字符
}
为什么不用c的字符数组
- SDS记录字符串长度,所以获取字符串长度的操作是O1
- SDS一次申请较长的内存,例如如果字符串长度是5,它会申请多于5的内存,而下次修改字符串,就不需要重新申请内存了。所以可以减少申请和释放内存的次数,提高效率
- 由于sds的底层还是c的字符数组,所以可以兼容c的字符串函数
2.链表
数据结构:
链表节点
typedef struct listNode {
struct
listNode *prev //前节点
struct listNode * next // 后节点
void *value; //节点的值
}
链表
typedef struct list {
listNode *head; // 头节点
listNode *tail ;// 尾节点
unsigned log len;// 节点长度
}
3.字典
哈希表节点
typedef struct dictEntry{
void *key; //键
union{
void *val;
uint64_t u64;
int64_t s64;
} v; // 节点的值,可以是指针(val),也可以是数值(u64,s64)
struct dictEntry *next ; //下一个节点,形成链表
}
哈希表
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned log size; //哈希表大小
unsigned log sizemask;// 大小掩码,总是等于size-1
unsigned log used;//已有节点数量
}
字典
typedef struct dict {
dictType *type; //类型特定函数
void *pricdata; // 私有数据
dictht ht[2]; // 哈希表
int trehashidx; // rehash索引,当rehash不在进行时,值为-1
}
一个字典有两张哈希表,0用于存储数据,1用于rehash。
当要存储k0=v0的时候,首先程序会算出k0的hash值,例如是8,假如sizemask是3,size是4,然后计算index=8&3=0(不知道&是什么操作,但是应该是8mode4=0,取余)。然后去ht[0].table数据中,找index等于0的哈希节点,然后是否有哈希节点,如果有,就看节点的next是否为空,直到找到next为空的节点,然后创建一个新的节点,使上一个节点的next指向新节点
如果是获取k0的值,计算index后,遍历节点连表,直到找到key等于k0的节点。
所以
- 如果一个index对应的链表很长,其实要花On的复杂度来遍历,然后获取节点的值的
- 当一个字典的节点数量(used值)增大到一定值时,字典会执行rehash操作,也就是增大size值,table的长度,这样就会减少节点链表的长度
rehash
rehash的过程就是
- 当触发rehash后,会修改字典的trehashidx值,表示正在执行rehash
- redis会启动一个新的进程,遍历dt[0]的所有键,然后重新计算index,放到dt[1]中,当然dt[1]的size会比dt[0]大
- 在rehash的过程中,字典的删除,查找,更新操作会在两个dt里面执行
- 插入,只在dt1中执行
- rehash完成后,把dt0执行dt1,把dt1置空。
负载因子= used / size
- 当负载因子小于1,执行收缩操作
- 当服务器没有执行BGSAVE或者BGREWRITEAOF操作,而且负载因子大于1,执行扩展操作
- 当服务器执行BGSAVE或者BGREWRITEAOF操作,而且负载因子大于5,执行扩展操作
4.跳跃表
跳跃表,主要用于加快遍历的速度。
例如有序列表,1,2,3,4,5,50
如果要查找50的节点在哪里,需要对前面的5个节点都遍历一次,比较慢。如果再第一个节点记录了50在哪里,或者5在哪里,程序就可以直接跳过前面的节点,直接到达,或者接近目标节点。
注意列表必须是有序的。
跳跃表结构
- header 指向头节点
- tail 指向尾节点
- level 表示最多层数的节点的层数
- length,表示节点的数量
跳跃表节点结构
typedef struct zskiplistNode {
struct zskiplistNode *backward;// 上一个节点的指针
double score;// 节点的分值
robj *obj; // 节点的存储对象
struct zkiplistLevel{// 节点的层,是一个列表
struct zskiplistNode *forward; //下一个节点的指针
unsigned int span; //前进的跨度
} level[];
}
一个列表有多少个元素,就会有多少个跳跃表节点
当创建一个新节点时,程序会根据幂次定律,随机生成1和32直接的值作为level数组的大小,这个大小就是这个节点的高度。
例如上面的元素1
的节点,会有一个level指向50
的节点,跨度是5。这样当程序需要找到50的时候,遍历1的节点,发现1节点的值小于50,就去遍历1节点的level,
- 如果第一个level就是指向50节点,那就立刻找到了。
- 如果第一个level是指向5,发现5不符合,就找下一个level
5.整数集合
当一个集合(Set)只包含整数元素,而且数量不多时,就会使用整数集合。
type struct intset {
uint32_t encoding;//编码方式,整数的大小,例如32位,16位
uint32_t length;//集合的长度
int8_t contents[];//集合的元素,类型取决于encoding属性的值
}
升级
如果集合的encoding是16位,当要插入一个32位的整数时,数据结构就会升级,例如升级为32位的。
但是不会进行降级操作。
6.压缩列表
当一个列表和字典的长度比较短,就会使用压缩列表。
属性:
- zlbyte,uint32_t 类型,记录压缩列表的字节数
- zltail uint32_t 记录尾元素节点距离起始地址有多少字节,通过这个可以快速定位到尾节点
- zllen 节点数
- entryX 节点元素
- zlend 压缩列表的末端
压缩列表不同上面的数据结构是redis里面的struct,压缩列表是一段内存数据。
1.节点的构成,也就是entryX里面的结构
- previous_entry_length 前一个节点的长度,单位是字节
- encoding,保存的数据类型
- content ,内容
如果要查找一个元素,
- 首先查看zltail,定位到尾节点
- 读取尾节点的encoding,确定content的长度
- 读取content的内容
- 如果内容不符合
- 根据previous_entry_length,,定位前一个节点的位置,重复上面的过程
如果新增元素,会在头部插入,然后更新前一个节点的previous_entry_length 。
如果删除元素,修改前一个节点的previous_entry_length为空。
压缩列表的优点
- 实现简单,可以节约内存
- 读写都比较简单,不需要改太多值
7.对象
Redis有五种对象
- 字符串
- 列表
- 哈希
- 集合
- 有序集合
redis中,通过type
命令,可以查看一个key的对象类型
这5中对象在不同的时候,会采用上面介绍的数据结构的其中一种。而且在操作的过程中,数据结构也会变化。
通过命令object encoding
可以查看key使用的数据结构
redis 127.0.0.1:6701> set test 'aaa'
OK
redis 127.0.0.1:6701> type test
string
redis 127.0.0.1:6701> object encoding test
"embstr"
redis 127.0.0.1:6701>
embstr就是上面的SDS。
redis使用的数据结构有
- 整数 int
- SDS enbstr,底层使用sds
- 简单动态字符串 raw ,底层使用sds
- 字典 hashtable
- 双端链表 linkedlist
- 压缩列表 ziplist
- 整数集合 intset
- 跳跃表和字典 skiplist
对象的数据结构
typedef struct redisObject {
unsigned type:4; //对象的类型,对应上面的五种对象
unsigned encoding:4; //使用的数据结构,对应上面的几种结构
void *ptr ;//底层的数据结构的指针
}
1.字符串对象
数据结构可以是int raw和embstr
如果字符串全是数字,就会使用int类型
如果字符串的大小大于39字节,使用raw来存储
如果大小小于39字节,使用embstr来存储
raw和embstr的区别是,raw需要申请两次内存,embstr只需要一次
2.列表对象
列表对象可以是ziplist或者linkedlist
如果列表元素的长度都小于64字节,而且元素的数量小于512个,就会使用ziplist,否则使用linkedlist
3.哈希对象
哈希对象可以是ziplist或者hashtable
如果键值对的长度都小于64字节,而且键值对的数量小于512个,就会使用ziplist,否则使用linkedlist
4.集合对象
可以是intset 或者hashtable
当元素都是整数值,元素不超过512,使用intset,否则使用hashtable
5.有序集合
数据结构可以是ziplist或者skiplist
当元素小于128个,而且元素长度都小于64字节,使用ziplist,否则使用skiplist。
6.内存回收
redis使用引用计数器来实现内存的回收
每个对象都有一个int refcount属性,来记录被引用的次数。
在业务端,
- 每次引用对象,需要调用对象的incrRefCount函数,使用对象的引用计算加一,
- 每次不引用对象的时候,都需要调用对象的decrRefCount函数,来令refcount属性减1,如果引用计数小于1,就会回收内存。
7.对象共享
数字0到9999,这些值的字符串对象,会在redis启动的时候就被创建,然后被共享。
为什么其他值不共享,因为如果要共享其他值,每次创建的时候,都有查看内存中有没有这个值的对象,这个查询的效率是很低的。
8.对象的空转时长
每个对象都有个属性 unsigned lru:22;,用于记录对象最后一次被访问的时间
通过object idletime
命令,可以查看一个key的空转时长,其实就是当前时间戳减去lru。
在redis回收内存的时候,而且当回收算法是volatile-lru或者allkeys-lru时,会优先回收空转时长较长的key。