基本数据结构
-
String(字符串)
-
Hash(哈希)
-
List(列表)
-
Set(集合)
-
zset(有序集合)
它还有三种特殊的数据结构类型
-
Geospatial
-
Hyperloglog
-
Bitmap
String
- redis 没有使用C语言的字符串,而是自己实现了sds
数据结构
int len; // 已经使用的长度
int free; // 空闲的长度
char[] buf; // len + free + 1 的char数组,最后一个char 是\0
为啥最后一个char 一定是\0 ?
sds 可以直接重用一部分C语言字符串库里的函数,比如printf
sds 与 C 语言字符串的区别?
1.常数复杂度获取字符串长度
有len属性
2.避免缓冲区溢出
拼接操作时:
- 先判断sds 空间够不够用,如果不够用,进行扩展
3.降低修改字符串带来的内存重分配次数
原因?
因为 redis 作为数据库,对数据读取速度要求严格,如果频繁操作内存分配,会降低性能。
空间预分配:
-
如果 len < 1mb,分配和len一样的free空间
-
如果 len >= 1mb, 分配1mb 的free空间
惰性空间释放:当我们缩短sds 时,会暂时将这部分空间加入free空间,sds 也提供了api,释放sds 未使用的空间。
4.二进制安全
-
c字符串除了末尾的标识外,还不能包含空字符,否则会将读到的第一个空字符做饭字符串的结尾;因此c字符串不能保存图片、视频这些二进制数据。
-
sds 可以,因为sds 使用len属性的值而不是空字符来判断字符串是否结束
链表list
也没有使用C链表,而是自己实现
链表结构
list:
listNode head;
listNode tail;
long len;
dup() // 节点值复制函数
free() // 节点值释放函数
match() // 节点值比较函数
链表节点结构(双向)
listNode:
listNode prev;//前置
listNode next;//后置
object value; // 任一类型的value
特性总结
-
双向:获取前后节点的复杂度都是o(1),估计是为了方便范围查询
-
无环
-
带头尾指针
-
带链表长度
-
多态,没有指定value 的具体类型
字典dict
使用哈希表来实现
哈希表
类似hashMap
dictht:
dictEntry[] table; // 哈希表数组
long size; // 哈希表大小
long sizemask; // 哈希表大小掩码,=size-1,用于计算索引值
long used;//已有的节点数
哈希表节点
使用拉链法解决哈希冲突
dictEntry:
object key;
union v;
dictEntry next;
-
key :
-
v 可以是一个指针,或者int64整数,或者 uint64(uint 不带符号)
-
next 指向下一个 哈希值相同 的节点,解决哈希冲突
字典数据结构
dict:
type://类型指定
dictht ht[2];// 两个哈希表
int trehashidx;// rehash索引
-
type: 多态字典
-
ht[2]:一般只使用ht[0], ht[1] 只用来进行rehash
-
rehashidx:也是rehash使用,如果没有进行rehas,为-1
解决哈希冲突
-
拉链法
-
rehash
rehash
渐进式rehash
触发rehash的条件
-
没有执行bgsave、bgrewriteaof时,负载因子>=1;
-
执行bgsave、bgrewriteaof时,负载因子>=5;
-
负载因子:ht[0].used / ht[0].size;
-
负载因子<0.1 收缩操作
渐进式rehash
-
- 分配ht[1]的大小,第一个大于ht[0]的2的n次方;
-
- rehashidx =0
-
- 每次对字典进行写操作时,顺便将ht[0]在rehashidx位置的所有键值对rehash到ht[1], rehashidx ++;
-
- 直至完成, rehashidx = -1
-
- ht[0] = ht[1], ht[1] = null;
渐进式rehash的好处?
避免一次性rehash对服务器性能造成影响
渐进式rehash 时,哈希表的操作?
-
会同时使用两个哈希表
-
读操作:先去ht[0],没有再去ht[1]
-
新添加的键值对,一律去ht[1]
跳表
skipList
平均O(logn),最坏O(n)的查找
跳表数据结构
-
zskiplist:
-
header:头结点
-
tail:尾结点
-
level:最大的层数
-
length:跳表的节点数
-
跳表右边的是4个zskiplistNode 结构:
-
level:表示每个节点在哪几个层,L1表示第一层,L2表示第二层,每层都有两个属性:
-
前进指针 :指向下一个
-
跨度:离下一个节点的距离,
-
后退指针:bw标识后退指针,用于从后向前遍历使用。
-
分值:1.0、2.0、3.0
-
成员对象:
遍历?
-
- 访问表头
-
- 找跨度=1的,没有就去下一层找,
-
- 直到找到null
跨度的作用?
计算排位:就是当前节点的分值排第几?
在跳表里找每个节点是,将沿途访问过的所有层的跨度加起来,就是他的排位
后退指针?
方便倒序遍历
分值可以重复, 成员变量不可重复
重复的分值,按照 成员对象在字典 离得排序(hash值大小?)
整数集合
intset:
encoding;
length;
contents[];
压缩列表
为了节约内存:
-
zlbytes:4字节,记录整个压缩列表占用的内存字节数
-
zltail:4字节,记录 压缩列表队尾 的起始地址 有多少字节
-
zllen: 2字节,记录 压缩列表的节点数
-
entryX:不定字节,各个节点
-
zlend:1自己,特殊值0xFF(255)记录压缩列表的末端。
对象
*基于上述数据结构构造了一个对象系统,就是字符串、list、hash、set、zset
-
每种对象都至少用了一种数据结构,根据不同的使用场景,设置多种不同实现,优化对象在不同场景的使用效率
-
实现了基于引用计数的内存回收机制。
数据结构:
redisObject:
type // 类型
encoding
ptr //指针
int refcount; //引用计数
lru;//该对象最后一次被访问的时间
type类型
-
redis_string:字符串
-
redis_list:
-
redis_hash:
-
redis_set:
-
redis_zset:
type key 返回该key的type
encoding 编码实现
该对象使用了什么数据结构实现:
-
rdis_encoding_int :long整数
-
rdis_encoding_embstr:简单动态字符串
-
rdis_encoding_raw:动态字符串
-
rdis_encoding_ht:字典、哈希表
-
rdis_encoding_linkedlist:双向链表
-
rdis_encoding_ziplist:压缩lieba
-
rdis_encoding_intset:整数集合
-
rdis_encoding_skiplist:跳表
类型对应的编码对象
-
redis_string:
-
rdis_encoding_int
-
rdis_encoding_embstr
-
rdis_encoding_raw
-
-
redis_list:
-
rdis_encoding_ziplist
-
rdis_encoding_linkedlist
-
-
redis_hash:
-
rdis_encoding_ziplist
-
rdis_encoding_ht
-
-
redis_set:
-
rdis_encoding_intset
-
rdis_encoding_ht
-
-
redis_zset:
-
rdis_encoding_ziplist
-
rdis_encoding_skiplist
-
使用Object encoding key 命令查看
字符串对象:
- 如果保存的是整数,就用 int
- 如果保存的是字符串,而且长度大于39字节,那么使用sds,编码是raw
- 其他就是embstr,是一直专门保存短字符串的优化方式,和sds 一样,但是sds会调用两次内存分配函数来创建redisObject和sds对象,而embstr 只使用一次,分配一块连续空间,包括redisObject和sds对象
列表:
可以使用压缩列表或者双向列表。什么时候使用压缩列表?
- 所有字符串长度<64字节
- 元素数量<512,当然这两个参数都是可以修改的
哈希对象
- ziplist 或者hashtable
- ziplist保存时:key在前,value在后
- 什么时候使用zipList?
- 所有kv 的k和v长度<64;
- kv数量<512,,当然这两个参数都是可以修改的
集合对象:
- intset 或者hashtable
- 使用hashtable时,value 都是null,类似java hashset
- 什么时候使用intset?
- 元素都是整数
- 元素个数不超过512,当然这两个参数都是可以修改的
zset
- ziplist 或者skiplist
- 还有一个字典,key 是成员,value 是分值, 方便o(1)获取分值
- 会使用指针来共享成员和分值
- ziplist 也是分值在后,成员在前
- 什么时候使用ziplist?
- 元素个数<128
- 每个元素成员的长度<64字节
引用计数:
- 初始化:引用计数为1
- 被一个新程序使用,+1
- 不在被一个使用,-1
- 变为0时,释放内存
- 假设key a 100,key b 100, 他们会使用同一个对象,引用计数为2,可以节约内存
- redis 初始化服务器时,会创建一万个字符串对象,保存0-9999,类似java 中常量的缓存池。
- object refcount a 命令可以获取a的引用计数
对象空转时长lru
记录了对象最后一次被访问的时长
- object idletime a 命令可以打印空转时长,根据当前时间-lru得到
- 后续内存清理策略里的lru相关算法,都是根据这个lru来计算的。