前言

今天来整理学习下Redis有哪些常用数据结构都是怎么使用的呢?首先看下全局存储结构。

全局存储结构

基础你们肯定都知道,redis支持的基础数据结构如下: String(字符串)、List(链表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合),那我来给你整个的画一画redis全局存储结构模型。( redis版本不同,代码也不尽相同,但是看原理够用了),从redis源码开始分析:

  • 首先是redis启动会初始化redisServer,默认创建16个数据库redisDb,默认我们用的都是第一个编号0的数据库。
struct redisServer {
 // …
 // redis数据库数组
 redisDb *db;
    // 数据库的数量 默认16
    int dbnum;
 //...
}

struct redisServer {
 // …
 // redis数据库数组
 redisDb *db;
    // 数据库的数量 默认16
    int dbnum;
 //...
}
  • 每个redisDb数据库用dict(字典)保存着数据库中的所有键值对
struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
 dict *dict; 
 //...
}

 struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
 dict *dict; 
 //...
}
  • dict(字典)使用一对hashtable哈希表实现,跟Java中的HashMap很像
typedef struct dict {
    // 包含2个hashtable
    dictht ht[2];
    // ...
} 

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // ...
} 

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    void *val;
    // 指向下个哈希表节点,形成链表
    dictEntry *next;

}

typedef struct dict {
    // 包含2个hashtable
    dictht ht[2];
    // ...
} 

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // ...
} 

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    void *val;
    // 指向下个哈希表节点,形成链表
    dictEntry *next;

}
  • 字典中实际存储的Redis对象
typedef struct redisObject {   
    // 类型    
    unsigned type:4;// string,list,set,zset,hash等
    // 编码    
    unsigned encoding:4;  // int,raw,embstr,ziplist,intset,quicklist,skiplist等  
    // 对象最后一次被访问的时间    
    unsigned lru:REDIS_LRU_BITS;  
    // 引用计数    
    int refcount;   
    // 指向实际值的指针   
    void *ptr;
} 

typedef struct redisObject {   
    // 类型    
    unsigned type:4;// string,list,set,zset,hash等
    // 编码    
    unsigned encoding:4;  // int,raw,embstr,ziplist,intset,quicklist,skiplist等  
    // 对象最后一次被访问的时间    
    unsigned lru:REDIS_LRU_BITS;  
    // 引用计数    
    int refcount;   
    // 指向实际值的指针   
    void *ptr;
}

从上面分析可得Redis全局存储结构如下:

redis 强制null redis long_链表

在这里插入图片描述

(这个图直接把我画裂开了,如有错误欢迎指正)

下面我们用"3w"方法来一一介绍下,每个数据类型,底层所用到了哪些数据结构(编码 )。

String 字符串

是什么

内部其实就是一个带长度信息的字节数组,原理类似Java中的ArrayList,可以动态扩容,所以很多特性都类似了,原理是相通的。内容是以二进制的形式存储的,所以 SDS(Simple Dynamic  String) 可以存储任何类型的二进制数据,同时也不需要担心数据格式转换的问题。

struct SDS {
    // ...
  T capacity; // 数组容量
    T len; // 数组长度
    byte[] content; // 数组内容
}   

struct SDS {
    // ...
  T capacity; // 数组容量
    T len; // 数组长度
    byte[] content; // 数组内容
}



redis 强制null redis long_redis 强制null_02

在这里插入图片描述

为什么

1.为什么申请空间比实际占用空间大,冗余了很多空位?

字符串支持append修改操作,如果没有冗余空间,那么追加操作必会引起频繁的数组扩容,而扩容是个耗时操作,所以通过空间预分配的方式来解决,即用冗余空间换时间。

2.实际使用长度len字段存在的意义是什么?

我们来用反证法证明,如果没有len来记录字符串长度,那么每次获取字符串长度时,就要调用默认的strlen函数来获取,而这个函数的时间复杂度是O(n),如果有了len,每次获取长度可以直接访问它,时间复杂度立马降至为O(1)。查询效率迎来质的飞跃,这块跟Arraylist的size原理一样。

如何实现

我们来直接用redis自带的debug命令看下实际存储对象的底层编码encoding,来看下底层使用了什么数据结构。

本文实例用的是redis版本:6.0.6

int编码

> set key1 2000222222
OK
> debug object key1
Value at:0x7f21f2eadd20 refcount:1 encoding:int serializedlength:5 lru:13142802 lru_seconds_idle:25

> set key1 2000222222
OK
> debug object key1
Value at:0x7f21f2eadd20 refcount:1 encoding:int serializedlength:5 lru:13142802 lru_seconds_idle:25

embstr编码

> set key2 01234567890123456789012345678901234567890123  // 44个字符
OK
> debug object key2
Value at:0x7f21f2e15140 refcount:1 encoding:embstr serializedlength:21 lru:13145749 lru_seconds_idle:5

> set key2 01234567890123456789012345678901234567890123  // 44个字符
OK
> debug object key2
Value at:0x7f21f2e15140 refcount:1 encoding:embstr serializedlength:21 lru:13145749 lru_seconds_idle:5

raw编码

> set key2 012345678901234567890123456789012345678901234 // 45个字符
OK
> debug object key2
Value at:0x7f21f2eadd40 refcount:1 encoding:raw serializedlength:21 lru:13145765 lru_seconds_idle:2

> set key2 012345678901234567890123456789012345678901234 // 45个字符
OK
> debug object key2
Value at:0x7f21f2eadd40 refcount:1 encoding:raw serializedlength:21 lru:13145765 lru_seconds_idle:2

总结:

为了节省内存空间,会按照实际存储字符串长度类型来选用不同编码

  • 存储的字符串可以转为long型,则用long类型存储,编码为int
  • 存储的字符串长度不大于44个字节时,用embstr编码
  • 存储的字符串长度大于44个字节时,用raw编码

编码类型分这么细的原因?为了优先使用更紧凑的数据结构来解决问题,终极目标就是为了压缩内存、压缩内存、压缩内存。

raw和embstr的区别?embstr编码: RedisObject的元数据,指针和SDS是连续的,可以避免内存碎片

raw编码: Redis会给SDS分配独立的空间,并用指针指向SDS结构

扩容策略

  • 字符串长度小于1MB时,采用加倍策略,ArrayList1.5
  • 字符串长度大于1MB时,采用每次扩容只加固定1MB

这个扩容策略,就比ArrayList高明了,当字符串比较大时,比如200M,每次还是double的话,400M,那就太浪费空间了,为了避免这种过大的空间浪费,使用了这种阈值判断方式,针对原始数据的不同大小采用相应的有效策略。

Reids规定了字符串最大长度不能超过512MB

使用场景

常用于缓存用户信息、原子加减。

注意: 原子计数是有范围的(long的范围),超过了会报错异常

List 链表

是什么
  • 版本3.2之前在Redis中使用的是压缩列表ziplist+双向链表linkedlist.
  • 版本3.2之后快速链表quickList

3.2之前初始化的 List 使用的压缩列表ziplist,随着数据增多,转化为双向链表linkedlist。压缩列表转化成双向链表的条件:

  • 如果添加的字符串元素长度超过默认值64
  • zip包含的节点数超过默认值512

这两个条件是可以修改的,在redis.conf中

list-max-ziplist-value 64 
list-max-ziplist-entries 512  

list-max-ziplist-value 64 
list-max-ziplist-entries 512
linkedlist

原理类似Java中的LinkedList,增删时间复杂度O(1),查询O(n).

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
  // ...
}
typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}

typedef struct list{
     //表头节点
     listNode *head;
     //表尾节点
     listNode *tail;
     //链表所包含的节点数量
     unsigned long len;
  // ...
}
typedef  struct listNode{
       //前置节点
       struct listNode *prev;
       //后置节点
       struct listNode *next;
       //节点的值
       void *value;  
}



redis 强制null redis long_链表_03

在这里插入图片描述

ziplist

ziplist是什么

ziplist压缩列表是内存地址连续,元素之间紧凑存储,功能类似链表的一种数据结构。

struct ziplist {
  int32 zlbytes;   // 整个列表占用字节数
  int32 zltail_offset; // 达到尾部的偏移量
  int16 zllength; // 存储元素实体个数
  T[] entries; // 存储内容实体
  int8 zlend; // 尾部标识
}
struct entry {
  int prevlen;   // 前一个entry的字节长度int encoding;; // 元素类型编码
  optional byte[] content; // 元素内容
}
struct ziplist {
  int32 zlbytes;   // 整个列表占用字节数
  int32 zltail_offset; // 达到尾部的偏移量
  int16 zllength; // 存储元素实体个数
  T[] entries; // 存储内容实体
  int8 zlend; // 尾部标识
}
struct entry {
  int prevlen;   // 前一个entry的字节长度int encoding;; // 元素类型编码
  optional byte[] content; // 元素内容
}



redis 强制null redis long_long 转为string_04

在这里插入图片描述

为什么用ziplist?

因为普通的链表要附加prev、next前后指针浪费空间(64位操作系统每个指针占用8个字节),另外每个节点的内存是单独分配,会加剧内存的碎片化,影响内存管理效率。

如何实现

简单的来说就是用非指针连接的方式实现了双向链表的能力,能从头部和尾部(zltail)双向遍历没有维护双向指针prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度 更费内存。这是典型的“时间换空间”。只有字段、值比较小,才会用ziplist

优点:

  • 内存地址连续,省去了每个元素的头尾节点指针占用的内存,节省空间

缺点:

  • 插入数据、删除数据会导致连锁更新问题,有点儿类似Arraylist为保证内存连续性的数据移动的原理
quicklist

quicklist是什么

quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。

为什么

为什么用quicklist

结合了 zipListlinkedList 的优点设计出来的,ziplist会引入频繁的内存申请和释放,而linkedlist由于指针也会造成内存的浪费,而且每个节点是单独存在的,会造成很多内存碎片,所以结合两个结构的特点,设计了quickList。

如何实现

debug看下encoding: quicklist

> rpush key3 a b c
3
> debug object key3
Value at:0x7f21f2eaddb0 refcount:1 encoding:quicklist serializedlength:22 lru:13150287 lru_seconds_idle:17 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:20

> rpush key3 a b c
3
> debug object key3
Value at:0x7f21f2eaddb0 refcount:1 encoding:quicklist serializedlength:22 lru:13150287 lru_seconds_idle:17 ql_nodes:1 ql_avg_node:3.00 ql_ziplist_max:-2 ql_compressed:0 ql_uncompressed_size:20
struct quicklist {
  quicklistNode *head;   
  quicklistNode *tail; 
  long count; // 元素总数
  // ... 
}
struct quicklistNode  {
  quicklistNode *prev;  
  quicklistNode *next; 
  ziplist *zl;// 压缩列表


struct quicklist {
  quicklistNode *head;   
  quicklistNode *tail; 
  long count; // 元素总数
  // ... 
}
struct quicklistNode  {
  quicklistNode *prev;  
  quicklistNode *next; 
  ziplist *zl;// 压缩列表

quickList 的每个节点使用 ziplist 来保存数据,有headtail,每一个节点是一个quicklistNode,包含prev和next指针。每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

结构如下图:



redis 强制null redis long_字符串_05

在这里插入图片描述

每个quicklist节点上的ziplist大小可以配置

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。

-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。

-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。

-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)

-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

list-max0ziplist-size -2

-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。

-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。

-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。

-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(默认值)

-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

list-max0ziplist-size -2

中间节点压缩策略可配置

0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
以此类推
list-compress-depth 0

0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
以此类推
list-compress-depth 0

总结

  • 整个redis全局存储模型,是用字典完成的,类似Java中的HashMap原理
  • String类型,底层是动态字符串,会根据字符串类型和大小决定使用int编码raw编码或者embstr编码
  • List类型,3.2版本之前会根据数据大小判断用ziplist还是linkedlist,3.2版本之后优化为quicklist方式编码。