Redis数据结构探究

1、与其他数据库的对比

最近系统中引入了Redis,在应用中发现Reids具有关系型数据库或其他缓存服务器所不具备的优点。
与关系型数据库如Mysql相比,Reids属于非关系型数据库,类似于Nosql,不同数据之间不需要有关联关系。
memcache也可以用来存储键值映射,同是对内存操作,和Redis性能差别不大,但是Redis具备以两种形式将数据写入硬盘的能力,并且除了存储普通的字符串键值外,还可以存储其他四种数据结构,而memcache只能存储字符串键。所以Redis可以用来解决更广泛的问题,而且不只可以用来当做缓存数据库,更可以用作主数据库使用。

2、Redis的五种数据类型

Redis可以存储五种键和数据类型间的映射,分别是STRING、LIST、SET、HASH、ZSET,其中STRING、LIST、SET、HASH在大多数编程语言中都存在,在实现和作用上也比较相似。另外ZSET是特有的一种数据结构,是一种有序集合。
以下是五种数据类型之间的对比和特点。

结构类型结构存储的值结构操作
STRING字符串、整数、浮点数可以对字符串或字符串一部分执行操作,对整数和浮点数进行++ –操作
LIST链表,链表的每个节点包含一个字符串可以在链表两端进行push pull(起到了栈的作用),根据偏移量进行trim,读取单个或多个元素,根据值查找或删除元素
SET无序集合添加/获取/移除元素,判断是否存在,交差并集合,随机获取元素
HASH无序散列表添加、移除、获取键值对,获取所有键值对
ZSET有序集合,由字符串和分数组成,元素排列顺序根据分值添加、获取、删除元素,根据分值获取元素

2.1 STRING

Redis的字符串映射的键和值都是字符串类型,可以存储字符串、整数或浮点数。和memcache的键值对作用相同。除了普通字符串类型,在存储一些复杂结构时,比如本次在系统中存储一个java对象,就是将对象序列化为字符串进行储存,另外,也可以将对象序列化为json或xml进行存储。
一般STRING类型有以下应用场景:

  • 缓存,存储基础数据信息(文章、用户、记录等)。仅当update/insert时访问数据库并更新缓存,其他时候只访问缓存,可以显著减轻数据库压力。
  • 用作session服务器,一般服务器集群中共享session会存储在数据库、session服务器、redis中,存取较快。
  • 计数器等操作频繁的数据请求,因为STRING类型可以存储整数或浮点数并可以自增,所以用作计数器、pingback等非常方便。

2.2 LIST

LIST就是我们经常用到的数据结构中的队列,Redis的LIST是双端队列,两端都可以进行Push和pull操作,所以也可以当做栈来使用,另外还可以获取一定范围内的列表。
LIST有以下应用场景:

  • 分布式系统中服务间调用可以使用LIST实现消息队列,实现异步调用,使应用间解耦。
  • 高并发场景下可以使用LIST实现限流。
  • 文章分页或者是有序的列表数据可以使用LIST,因为可以在某个范围内存取元素。

2.3 HASH

上文中提到了STRING可以用来存储序列化的对象,但是如果这个对象需要进行操作的话,需要先解析再修改再序列化保存,无法直接修改。而HASH类型可以解决这个问题,在Redis中,HASH结构可以解决这个问题,它的键也是一个键值对,用户可以直接修改HASH结构中某个属性的值。所以适合存储对象。
HASH有以下应用场景:

  • 存储用户数据,用户基础数据是请求频率较高的,但是属性较多且经常需要修改,所以可以存储在HASH,修改时update某个属性就可以。

2.4 SET

SET是数据结构中的集合,与列表不同的是它不保存重复数据,并且元素无序,所以不能获取一定范围内的元素,也不能根据索引获取。Redis支持对集合进行交并差集计算,在很多场景下都可以发挥作用。
SET有以下应用场景:

  • 数据排重,比如记录今天访问的用户,可以使用集合。
  • 标签操作,系统中有个功能是打标签,每打一个标签,可以将该数据保存在以标签id为key的set中,展示时直接展示该set。
  • 用户关系操作,比如计算用户的共同好友等。

2.5 ZSET

有序集合是一种特殊的集合,它既具备集合不重复元素的特点,又可以进行排序。但它和list不同的是,它不是根据下标排序,所以排序不固定,而是它的value中包含一个分值,分值可以是分数、访问量等等,根据自定义类别进行排序。
ZSET有以下应用场景:

  • 排行榜,排序操作最常用的应用就是排行榜,比如考试分数、访问量、点赞量。考虑的一个应用场景是在wiki中的精选文章中,对文章进行排序。因为精选文章相对固定但需要实时变动,应用ZSET比较合适。

3、底层数据结构的实现

我们知道Redis是使用ANSI C实现的,那它是怎么实现的集合等复杂数据结构,比较令人好奇,在阅读了《Redis设计与实现》这本书后,得到了比较准确的答案。首先我们需要了解一下Redis中使用到的数据结构。

3.1 简单动态字符串

最开始以为Redis是使用c++中的char数组来存储字符串类型,其实并不是这样,它定义了一个名为simple dynamic string(SDS)的结构体用来保存字符串类型,结构如下:

struct sdshdr{
    int len;
    int free;
    int buf[];
}

其中:

  • len:buf中已用长度
  • free:bug剩余可用长度
  • buf[] : 字符数组

我们存储的字符串就是存储在buf数组中,这样做的原因是char[]并不能满足Redis的操作需求,或是会带来较大的性能消耗,比如append,获取长度等等。像一些高级语言,也普遍有这种实现方式,像之前在阅读PHP数据结构源码时,string类型也是由数组和一个标志长度的int值实现。这样获取长度的复杂度就是O(1)。另外还有一些好处:

  • 防止缓冲区溢出
    字符串长度增加时,如果内存相邻地址已有内容,则会发生缓冲区溢出的现象,而Redis在扩展字符串时,会先检查free长度,如果不够时,会先拓展空间。
  • 减少内存分配次数
    SDS的扩展策略是小于1MB时,每次扩展到之前的二倍大小,大于1MB时,每次扩展1MB,所以不需要每次变化都重新分配内存。另外由于释放空间时采用惰性空间释放,减少了内存分配次数。
  • 惰性空间释放
    SDS释放空间时并不真正释放内存空间,而是修改free的值,既能避免内存泄露,又减少内存分配次数。
  • 二进制安全
    c字符串末尾默认是空字符,所以在首次读入空字符时会被认为字符串结束。而SDS记录了len长度,可以通过长度获取内容,有空字符也不影响,所以可以用来存储二进制数据。

简单动态字符串是Redis最重要的数据结构,键值等字符串类型是使用它,另外还有AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区也是由它实现。

3.2 链表

Redis中的LIST实现之一就是链表的。由链表和节点两种数据结构组成,节点用来存储数据和指针,链表结构封装了一些复制和删除节点的操作。
下面是节点的结构体:

typedef struct listNode{
    struct listNode *prev;
    struct listNode *next;
    void *value;
}listNode;

普通链表节点一般只有一个指针指向下一个节点,而这里面有pre和next两个指针,实现了一个双向链表。
下面是链表结构体:

typedef struct list{
    listNode *head;
    listNode *tail;
    unsigned long len;
    void *(*dup) (void *ptr)
    void *(*free)(void *ptr)
    int (*match)(void *ptr,void *key);
}list;

list结构体中除了包含head头结点,tail尾节点,len长度外,还封装了复制节点、删除节点、匹配节点的方法。

3.3 hashtable

Redis中HASH的实现方式之一是hashtable,由dict和dictht两种数据结构实现。
其中字典dict的数据结构:

typedef struct dict {  
    dictType *type;  
    void *privdata;  
    dictht ht[2];  
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */  
    int iterators; /* number of iterators currently running */  
} dict; 

type 属性 和privdata 属性是针对不同类型的键值对,为创建多态字典而设置的。ht 属性是一个包含两个项(两个哈希表)的数组。指向了两个哈希表dictht。

typedef struct dictht {  
    dictEntry **table;  
    unsigned long size;  
    unsigned long sizemask;  
    unsigned long used;  
} dictht; 

其中dictht中的table是用来存储kv元素的,每个dictEntry包含一对kv。
最后,哈希表节点dictEntry结构定义为:

typeof struct dictEntry{
   void *key;
   union{
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;
}

其中的next指针是为了解决hash冲突时,使用了链地址法组成链表。在此不对如何解决hash冲突和使用的散列算法进行深入讨论。
如图所示:
这里写图片描述
图片来自引用
HASH的另一种实现方式是ziplist。

3.4 intset

Redis中SET的实现方式有两种,其中一种是hashtable,在上文中已经有过了解。另外一种是intset,这是一个整数集合,里面存的为某种同一类型的整数,支持如下三种长度的整数:

#define INTSET_ENC_INT16 (sizeof(int16_t))  
#define INTSET_ENC_INT32 (sizeof(int32_t))  
#define INTSET_ENC_INT64 (sizeof(int64_t))  

intset结构体为:

typedef struct intset{
    //编码方式
    uint32_t enconding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
}intset;

其中里面的数据按照从大到小的元素排列。并且存储的类型由encoding决定。在集合中查找元素的复杂度为O(logN)。但插入时设计到从16位升级到32位或64位,所以复杂度不一定。并且升级后不能再降级。

3.5 skiplist

Redis的ZSET实现方式之一是跳跃表,另一种是ziplist。

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率和平衡树媲美 ——查找、删除、添加等操作都可以在对数期望时间下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多。

跳跃表主要由链表和节点组成,下面是链表zskiplist的数据结构。

typedef struct zskiplist {  
    struct zskiplistNode *header, *tail;
    unsigned long length;  
    //最大节点成熟
    int level;  
} zskiplist;  

可以看到,和普通链表不同的是,多了一个level用来记录表中层数最大的节点的层数。
下面是节点的结构体:

typedef struct zskiplistNode {  
    // member  
    robj *obj;  
    // 分值  
    double score;  
    // 后退指针  
    struct zskiplistNode *backward;  
    // 层  
    struct zskiplistLevel {  
        // 前进指针  
        struct zskiplistNode *forward;  
        // 这个层跨越的节点数量  
        unsigned int span;  
    } level[];  
} zskiplistNode;  

其中除了value和分数score外,还包含了一个level[]数组,这就是每个节点里的分层指针数组。
如图所示:
这里写图片描述
图片来自引用
其中每个节点的层数都是1到32的随机数,节点间是按照分值大小来进行排序。

3.6 ziplist

这是一种压缩列表,在LIST和HASH中使用,因为它保存在连续的内存空间中,所以比价节省内存空间,但在插入时每次都需要重新分配空间。
如图所示:
这里写图片描述
图片来自引用
其中,包含如下属性:

  • zlbytes:用于记录整个压缩列表占用的内存字节数
  • zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节
  • zllen:记录了压缩列表包含的节点数量。
  • entryX:要说列表包含的各个节点
  • zlend:用于标记压缩列表的末端

3.7 Redis对象的实现

上面讲解了Reids实现中使用到的数据结构,其中Redis中的每个对象都不是由固定的数据结构实现,而是会根据数据类型大小选择不同的实现方式,以下为对应表。

Redis对象实现方式
STRINGint 实现
embstr编码的简单动态字符串(SDS)实现
SDS实现
LISTziplist压缩列表实现
链表实现
HASHziplist实现
字典hashtable实现
SETintset整数集合实现
hashtable实现
ZSETziplist实现
跳表skiplist、hashtable实现

总结

通过阅读《Redis设计与实现》前两章,对Redis的底层实现有了初步的了解。后续会继续研究Redis两种持久化方式和线程架构方面。


参考来源:
1. 《Redis设计与实现》
2. 《Redis实战》
3. http://www.cnblogs.com/jaycekon/p/6227442.html
4. https://blog.csdn.net/u011531613/article/details/70193720?locationNum=7&fps=1
5. https://blog.csdn.net/wcf373722432/article/details/78678504
6. https://www.cnblogs.com/ysocean/p/9080942.html

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值