之前相关联的文章:
redis学习之一SDS
redis学习之二双端链表
redis学习之三字典
redis学习之四skiplist
redis学习之五ziplist

说明:本文是基于redis2.9版本

这段时间打算梳理下redis相关的知识点,先从redis底层的数据结构与redisObject对象开始吧。
在展开之前我们需要先对redis的设计原则有一个大概的认识:

  • 存储效率(memory efficiency),在追求存储效率这一原则的前提下,redis才会有上面这些数据结构,并且会在某些场景下会交替使用这些数据结构,可以想象的到redis会在数据压缩、避免内存碎片方面下功夫。
  • 快速响应(fast response),一般来说,单台redis可达到4W左右的QPS。因为数据是存在内存所以不会出现磁盘IO(这也是redis不必使用多线程的一个原因),而redis的性能瓶颈主要是内存大小网络带宽。正是有了底层这些特殊的数据结构支撑,才会有极快的响应。

我们都知道redis里我们经常使用的数据类型有:

  • string
  • list
  • hash
  • set
  • sorted set

这五种(其它数据类型可以参看:redis文档),那底层的数据结构是如何支撑这五种数据类型的呢?从源码里可以看出,底层分别是:

  • dict
  • sds
  • ziplist
  • quicklist
  • skiplist

这些结构数据,不过,redis并没有直接使用这些数据结构来实现键值对,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象、和有序集合对象,每种对象都会使用到至少一种上面提到的数据结构,具体是这样的:

  1. list:在同时满足下面两种情况下会使用ziplist:一是所有字符串长度小于64字节;二是元素数量小于512,不满足任一条件就会使用linkedlist(双端链表)。
  2. hash:在同时满足下面两种情况下会使用ziplist:一是所有键值对的键和值的字符串长度都小于64字节;二是键值对数量小于512个;不满足任意一个都使用hashtable编码。
  3. set:在同时满足下面两种情况下会使用intset:一是所有元素都是整数值;二是元素个数小于等于512个;不满足任意一条都将使用hashtable编码。
  4. zset:在同时满足下面两种情况下会使用ziplist:一是所有元素长度小于64字节;二是元素个数小于128个;不满足任意一条件将使用skiplist编码。

这样做的目的有:

  1. 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定命令。
  2. 可以针对不同的使用场景切换使用合适的数据结构。
  3. redisObject对象实现了基于引用计数的内存回收机制方便管理内存,同时在引用计数的基础上实现的对象共享机制也利于节约内存。

每次我们使用redis创建一个新的键值对时,至少会创建两个对象:一个是键对象一个是值对象。redisObject有三个属性分别为type属性、encoding属性、ptr属性:

typedef struct redisObject {
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //lru缓存淘汰机制信息,记录对象最后一次被访问的时间
    unsigned lru:LRU_BITS;
    //引用计数器
    int refcount;
    //指向底层实现数据结构的指针
    void *ptr;
    //...
} robj;

type属性记录了对象的类型,其对应的值如下:

类型常量对象的名称
REDIS_STRING字符串对象
REDIS_LIST列表对象
REDIS_HASH哈希对象
REDIS_SET集合对象
REDIS_ZSET有序集合对象

我们可以使用TYPE命令来查看值对象的类型。对于redis保存的键值来说,键总是一个字符串对象,而值则是上面提到的五种类型对象。

对象的ptr属性指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定,encoding记录着对象所使用的编码,可以是下面的值:

编码常量编码对应底层的数据结构
REDIS_ENCODING_INTlong类型的整数
REDIS_ENCODING_EMBSTRembstr编码的简单动态字符串
REDIS_ENCODING_RAW简单动态字符串
REDIS_ENCODING_HT字典
REDIS_ENCODING_LINKEDLIST双端链表
REDIS_ENCODING_ZIPLIST压缩列表
REDIS_ENCODING_INTSET整数集合
REDIS_ENCODING_SKIPLIST跳跃表

值对象的编码我们可以使用OBJECT命令进行查看。

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提升了灵活性与效率,redis可以根据不同的使用场景来选择特定对象不同的的编码,从而提升对应场景下的使用效率。

refcount&内存回收

因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
我们知道在java中使用可达性分析进行判决垃圾, 不使用引用计数, 是因为在java中对象之间存在互相引用, 但是在Redis中这是不存在的对象之间互相引用的, 并且如果是字符串对象其使用的也是对象共享机制,不会出现互相引用

对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。对象的引用计数信息会随着对象的使用状态而不断变化:

  • 在创建一个新对象时,引用计数的值会被初始化为1
  • 当对象被一个新程序使用时,它的引用计数值会被增一
  • 当对象不再被一个程序使用时,它的引用计数值会被减一
  • 当对象的引用计数值变为0时,对象所占用的内存会被释放

另,引用计数还有一个功能:在get大key的过程中因为数据过大而导致时间很长,而另一客户端在此过程中执行了del操作删数据,get仍在操作对应内存时会访问到非法的内存地址。而使用引用计数可以避免此类问题。详情见:redis使用基础(十一) ——Redis特殊情况处理机制

参考的文章有:

REDIS~对象(STRING、LIST、HASH、SET、ZSET等) 的对象检查、空转时长、内存回收与对象共享
黄健宏的《Redis设计与实现》一书
张铁蕾关于redis的文章


步履不停
38 声望13 粉丝

好走的都是下坡路