redis 数据类型_Redis的基本数据类型

一、数据类型概述

大家都知道,Redis能够支持的基本数据类型有:String, List,  Hash, Set, SortedSet这五种。那么这些底层的数据结构都是什么样的呢,为了支持上面的这5种基本数据类型,redis底层采用了简单动态字符串、双向链表、压缩列表、字典、跳表、整数集合这6种数据结构来达到目标。

下面,我们给出这些数据类型之间的映射关系,如下图所示:

407ee70002264c1f5cd612fb26b0dd81.png

二、简单动态字符串-sds

我们先看看Redis中sds结构体的源码,如下:

struct __attribute__ ((__packed__)) sdshdr5 {

unsigned char flags; /* 低3位存储类型, 高5位存储长度 */    

char buf[]; /*柔性数组,存放实际数据*/

};

struct __attribute__ ((__packed__)) sdshdr8 {

uint8_t len; /* 1个字节存储已使用长度 */    

uint8_t alloc; /* 1个字节存储已分配长度 */    

unsigned char flags; /* 低3位存储类型, 高5位预留 */    

char buf[]; /*柔性数组,存放实际数据*/

};

struct __attribute__ ((__packed__)) sdshdr16 {

uint16_t len; /* 2个字节存储已使用长度 */    

uint16_t alloc; /* 2个字节存储已分配长度 */    

unsigned char flags; /* 低3位存储类型, 高5位预留 */    

char buf[]; /*柔性数组,存放实际数据*/

};

struct __attribute__ ((__packed__)) sdshdr32 {

uint32_t len; /* 4个字节存储已使用长度 */    

uint32_t alloc; /* 4个字节存储已分配长度 */    

unsigned char flags; /* 低3位存储类型, 高5位预留 */    

char buf[]; /*柔性数组,存放实际数据*/

};

struct __attribute__ ((__packed__)) sdshdr64 {

uint64_t len; /* 8个字节存储已使用长度 */    

uint64_t alloc; /* 8个字节存储已分配长度 */    

unsigned char flags; /* 低3位存储类型, 高5位预留 */    

char buf[]; /*柔性数组,存放实际数据*/

};

sdshdr5和sdshdr8、sdshdr16、sdshdr32、sdshdr64不太一样,它是专门用来存储长度小于32的字符串类型的key的,不会用于存储value,可能redis设计得觉得key不太需要变化,而sdshdr5只有1个字节的8位中的5位来表示长度,因此这个结构体并不具备扩容的潜力。

sdshdr8、sdshdr16、sdshdr32、sdshdr64这4种结构体,除了表示长度的数据类型不一样,其它的都一样。之所以设计成这4种结构体,主要是为了节省内存。redis会根据要保存的字符串的长度,选用不同的sds结构体类型来存储。

比如说,我们采用sdshdr8存储了"abcde"这样的字符串,如下图所示:

 73be14e0ab8cfc436f3369cea91da819.png

采用sds而非原生的C字符串,主要是为了以下好处:

(1)、以空间换时间,通过预分配内存的方式,来降低字符串变化时,频繁的内存申请开销;

(2)、sds对象中的bug是一个柔性数组,有上层调用的时候,直接只返回了buf。由于buf是直接指向内容的指针,故而它可以兼容C语言字符串的函数。另外,它通过len限制读取的长度,这样可以避免读到'\0'字符,从而可以保证字符串读取的二进制安全。

(3)、如果采用原生的C字符串存储,那么获取字符串长度的时间复杂度就是O(n),但采用sds的话,获取字符串长度的时间复杂度就是O(1)。

三、双向链表-quicklist

我们先看看这个双向链表的结点类型的结构体的源码:

typedef struct quicklistNode {

struct quicklistNode *prev; /* 前驱指针 */

struct quicklistNode *next; /* 后继指针 */

unsigned char *zl; /* 指向该节点对应的ziplist结构体的指针 */

unsigned int sz;             /* 当前节点对应的ziplist的字节数 */    

unsigned int count : 16;     /* 当前节点对应的ziplist的元素个数 */  

unsigned int encoding : 2;   /* RAW==1代表采用原生编码 或 LZF==2代表采用LZF进行压缩 */    

unsigned int container : 2;  /* NONE==1代表不需要容器存储 或 ZIPLIST==2代表采用ziplist存储 */    

unsigned int recompress : 1; /* 当前节点的前驱是否被压缩 */    

unsigned int attempted_compress : 1; /* 测试时使用:数据很小,不需要压缩 */    

unsigned int extra : 10; /* 预留字段 */

} quicklistNode;

下面我们给出双向链表在内存中的结构示意图:

d46e334d0ae3f349fba5860f51a0f201.png

双向链表这种结构,是专门为存储List类型而设计的,这里有两个问题:

问题1:为什么采用双向链表而非单链表来进行存储?

因为双向链表有一个好处就是可以逆序遍历,可以满足我们对List进行逆序操作的需求。

问题2:在第一节中,我们了解到,List还有可能采用压缩列表来进行存储,而并非所有的List都采用双向链表来进行存储呢?

这是因为当List里的元素个数比较较少且每个元素的数据也比较小的时候,采用压缩列表来进行可以节省存储空间。

双向链表常见操作时间复杂度:

查找、删除:O(n)

插入(头插和尾插):O(1)

在指定元素后插入:O(n)

四、压缩列表-ziplist

Redis为了节省空间,采用字节数组的方式来实现压缩链表,其存储结构如下:

f04748d55c03c9372120acd5691d5a42.png

zlbytes:表示整个压缩列表可以存储的最大字节数,4个字节,最多存储232-1个字节;

ztail:zlend相对起始位置的相对偏移量,4个字节。

zllen:压缩列表的元素个数,2个字节,最多只能支持216-1,也就是65536个元素,如果压缩列表中元素的个数超过了这个数值,那么要获取压缩列表的长度,必须遍历整个压缩列表,因此,采用压缩列表存储的时候,元素个数通常比较小。要不然,获取列表长度的时间复杂度将由O(1)直接攀升至O(n)。

zlend:占1个字节,恒定存储0xFF。

entry:实际数据中的条目,可以是字节数组,也可以是整数,长度没有限制,插入之前都会先进行压缩再存储,查询的时候,也会进行解压缩。

Redis通过压缩列表来存储数据,它就有如下的特点:

(1)、不适合存在大量的元素,因为存在元素多了之后,如果有数据插入、删除,那么将会涉及到整个字节数组的移动,会降低性能;

(2)、但是这种存储方式非常节省空间,无论存在多少数据,元数据所占用的空间都只有:4+4+2+1,仅仅只有11个字节的元数据存储开销。这一点跟sds不太一样,sds非常浪费存储空间的原因就在于每一个字符串,都有自己的元数据要浪费存储空间。这一点,我们在使用redis的时候需要格外的注意。

压缩列表获取长度的时间复杂度:O(1)

压缩列表插入、查询与删除的时间复杂度均为O(n)

五、字典-dict

Redis关于字典的实现,底层也是用的哈希表,我们先回顾一下哈希表的结构:

4cd303767391ca8b01c8d433e7c08c3b.png

哈希表其实就是一个数组,每一个key计算完哈希后,会命中数组中的一个位置,如果数组中已经存在该key,这就是哈希冲突,Redis解决哈希冲突采取的是常见的“拉链法”,也就是在数组的元素中拉出一个链表来。

下面,我们再看看Redis关于哈希表的源码:

/* 哈希表结构体 */

typedef struct dictht {

      dictEntry **table;  /* 指向数组的指针 */

      unsigned long size; /* table数组的容量 */

      unsigned long sizemask; /* 掩码用于快速计算key的哈希值位于哪个位置,按位与运算性能高于求模 */

      unsigned long used; /* table数组中已经存在的元素个数 */

} dictht;

/* 字典条目结构体 */

typedef struct dictEntry {

      void *key; /* 键 */

      union {

      void *val;

      uint64_t u64;

      int64_t s64;

      double d;

      } v; /* 值及过期时间 */

      struct dictEntry *next; /* 哈希冲突时指向下一个冲突的元素,形成链表 */

} dictEntry;

哈希表插入、查询、删除的时间复杂度都是O(1)

六、跳表-zskiplist

我们先看看Redis关于跳表的定义代码:

/* 跳表结构体 */

typedef struct zskiplist {

      struct zskiplistNode *header, *tail; /* 分别定义头尾指针*/

      unsigned long length;  /* 除头节点外的节点总数 */

      int level; /* 跳表的高度 */

} zskiplist;

/* 跳表节点类结构体 */

typedef struct zskiplistNode {

      sds ele; /* 用于存储字符串类的数据 */

      double score; /* 用于存储排序的分值 */

      struct zskiplistNode *backward; /* 后退指针 */

      struct zskiplistLevel {

      struct zskiplistNode *forward; /* 前进指针 */

      unsigned long span; /* 间距:forward指向的节点与当前节点之间的元素个数 */

      } level[]; /* 柔性数组,用于存储一个或多个前进指针 */

} zskiplistNode;

根据redis中关于跳表结构的定义,我们大致可以绘出下面的跳表结构图,如下:

4b003bd78046c5019fba6ee2bd652aa5.png

redis中跳表的实现,除了有前进指针之外,还提供了后退指针,后退指针的作用:比如说我们在Sorted Set里保存数据的时候,数据按升序进行存储,但是我们可以取TOP N条之类的降序范围查询。

跳表的查询、查找及删除的时间复杂度都是:O(logn)

另外,跳表支持正序以及逆序的范围查询,这也就是为什么Redis使用跳表而不使用红黑树的原因所在,尽管跳表在维护自平衡时所要付出的代价要大于红黑树,但跳表的优势就是支持范围查询。

七、整数集合-intset

redis还提供了一个整数集合的基础数据结构,源码如下:

/* 整数集合结构体 */

typedef struct intset {

uint32_t encoding; /* 编码类型 */

uint32_t length; /* 元素个数 */

int8_t contents[]; /* 柔性数组,根据encoding的类型决定几个字节存储一个元素 */

} intset;

其内存结构如下:

e4eba37eec68430464ce14f353d7b314.png

encoding:占4个字节,表示编码类型

length:占4个字节,表示元素个数

每一个元素的长度由encoding决定,encoding的选项有:

INTSET_ENC_INT16:2个字节

INTSET_ENC_INT32:4个字节

INTSET_ENC_INT64:8个字节

整数集合的底层用于存储具体数据的,其实就是一个整数数组。

整数集合一些常见操作的时间复杂度:

插入、删除:O(n)

查找:O(logn)

获取元素个数及随机查找:O(1)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值