Redis 学习记录1——数据结构总结

116 阅读7分钟

基本数据结构

  • 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

    1. 分配ht[1]的大小,第一个大于ht[0]的2的n次方;
    1. rehashidx =0
    1. 每次对字典进行写操作时,顺便将ht[0]在rehashidx位置的所有键值对rehash到ht[1], rehashidx ++;
    1. 直至完成, rehashidx = -1
    1. ht[0] = ht[1], ht[1] = null;

渐进式rehash的好处?

避免一次性rehash对服务器性能造成影响

渐进式rehash 时,哈希表的操作?

  • 会同时使用两个哈希表

  • 读操作:先去ht[0],没有再去ht[1]

  • 新添加的键值对,一律去ht[1]

跳表

skipList

平均O(logn),最坏O(n)的查找

image.png

跳表数据结构

image.png

  • zskiplist:

  • header:头结点

  • tail:尾结点

  • level:最大的层数

  • length:跳表的节点数

  • 跳表右边的是4个zskiplistNode 结构:

  • level:表示每个节点在哪几个层,L1表示第一层,L2表示第二层,每层都有两个属性:

  • 前进指针 :指向下一个

  • 跨度:离下一个节点的距离,

  • 后退指针:bw标识后退指针,用于从后向前遍历使用。

  • 分值:1.0、2.0、3.0

  • 成员对象:

遍历?

    1. 访问表头
    1. 找跨度=1的,没有就去下一层找,
    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来计算的。