Redis:数据对象与底层实现

参考资料:

《Redis中的数据结构》

《Redis内部数据结构详解》

相关文章:

《Redis:底层数据结构》

        写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。

        

目录

一、redisObject

        1、redisObject的作用

        2、redisObject数据结构

        命令的类型检查和多态

二、redis对象与底层结构对应关系

        1、String

         三种实现方式

        编码的转换

        二、List

        三、 Hash

        两种实现

        编码转换

        四、Set

        实现方式

         编码转换

        五、Zset

       实现方式

      编码转换


        如果要了解redis的数据结构,可以从两个不同的层面来讨论它:

  • 第一个层面,是从使用者的角度,这一层面也是Redis暴露给外部的调用接口,比如:string,list,hash,set,sorted set。
  • 第二个层面,是从内部实现的角度,属于更底层的实现,比如:dict,sds,ziplist,quicklist,skiplist,intset。

        在之前的文章中我们介绍了Redis的几种底层数据结构,本文我们介绍下基础数据类型是如何关联到这些数据结构上的。

一、redisObject

        1、redisObject的作用

        在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。如: LPUSH 和 LLEN 只能用于列表键, 而 SADD 和 SRANDMEMBER 只能用于集合键, 等等; 另外一些命令, 比如 DEL、 TTL 和 TYPE, 可以用于任何类型的键;但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。

        以上的描述说明, Redis 必须让每个键都带有类型信息, 使得程序可以检查键的类型, 并为它选择合适的处理方式. 比如说, 集合类型就可以由字典和整数集合两种不同的数据结构实现, 但是, 当用户执行 ZADD 命令时, 他/她应该不必关心集合使用的是什么编码, 只要 Redis 能按照 ZADD 命令的指示, 将新元素添加到集合就可以了。

        这说明, 操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理.

为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:

  • redisObject 对象.
  • 基于 redisObject 对象的类型检查.
  • 基于 redisObject 对象的显式多态函数.
  • 对 redisObject 进行分配、共享和销毁的机制

        2、redisObject数据结构

/* 对象类型 */
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表


/* 对象编码 */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */


#define LRU_BITS 24

/* Redis 对象 */
typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码方式
    unsigned encoding:4;
    // LRU - 24位, 记录最末一次访问时间(相对于lru_clock)
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向底层数据结构实例
    void *ptr;
} robj;

        redisObject 几个最重要的属性如下:

        type:记录了对象所保存的值的类型,其值为下面几个常量之一

/* 对象类型 */
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表

       encoding:记录了对象所保存的值的编码其值为下面几个常量之一

/* 对象编码 */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

        ptr:一个指针,指向实际保存值的数据结构,这个数据结构由type和encoding属性决定。举个例子, 如果一个redisObject 的type 属性为OBJ_LIST , encoding 属性为OBJ_ENCODING_QUICKLIST ,那么这个对象就是一个Redis 列表(List),它的值保存在一个QuickList的数据结构内,而ptr 指针就指向quicklist的对象。

        在内存中的结构如下图:

         lru: 记录了对象最后一次被命令程序访问的时间。

        基于该属性,Redis引入了一个叫做空转时长的概念,当前时间减去键的值对象的lru时间,就是该键的空转时长。Object idletime命令可以打印出给定键的空转时长

        如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。

        refcount:记录当前对象被引用了多少次,对象被创建时记录为1,当对一个对象进行共享时,redis将这个对象的refcount加一。当使用完一个对象后,或者消除对一个对象的引用之后,程序将对象的refcount减一。当对象的refcount降至0 时,这个RedisObject结构,以及它引用的数据结构的内存都会被释放。

        关于引用计数,这里涉及到了对象共享,redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间,redis预分配的值对象如下:

    (1)各种命令的返回值,比如成功时返回的OK,错误时返回的ERROR,命令入队事务时返回的QUEUE,等等。(注意只有常用字符串,因为字符串的比较时间复杂度接近O(n))

    (2)包括0 在内,小于REDIS_SHARED_INTEGERS的所有整数(REDIS_SHARED_INTEGERS的默认值是10000)

        命令的类型检查和多态

        当执行一个处理数据类型命令的时候,redis执行以下步骤

  • 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
  • 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
  • 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
  • 返回数据结构的操作结果作为命令的返回值。

二、redis对象与底层结构对应关系

        经过上文和《Redis:底层数据结构》的介绍,我们对Redis的对象机制和底层数据结构有了一些了解,下面我们来介绍下Redis中基本数据类型的实现方式(即基于何种基础结构实现)。

        Redis提供的基础数据类型如下:

数据类型

可以存储的值

操作

应用场景

STRING

字符串、整数或者浮点数

对整个字符串或者字符串的其中一部分执行操作

对整数和浮点数执行自增或者自减操作

做简单的键值对缓存

LIST

列表

从两端压入或者弹出元素

对单个或者多个元素进行修剪,

只保留一个范围内的元素

存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的数据

SET

无序集合

添加、获取、移除单个元素

检查一个元素是否存在于集合中

计算交集、并集、差集

从集合里面随机获取元素

交集、并集、差集的操作,比如交集,可以把两个人的粉丝列表整一个交集

HASH

包含键值对的无序散列表

添加、获取、移除单个键值对

获取所有键值对

检查某个键是否存在

结构化的数据,比如一个对象

ZSET

有序集合

添加、获取、删除元素

根据分值范围或者成员来获取元素

计算一个键的排名

去重但可以排序,如获取排名前几名的用户

        1、String

         字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。

        String类型可以存储数值和字符串,当存储字符串时,看过前文的朋友大概可以猜出时基于SDS实现的,而当存储的值是整数值时,String会判断其大小是否可以由long表示,可以的话直接保存,否则按字符串的模式保存。Redis通过3种不同的编码实现String的不同实现,这34种编码反方式分别为int,embstr和raw。

         三种实现方式

  • int 编码:保存的是可以用 long 类型表示的整数值(浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型)
  •  embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
  •  raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)   

        我们看到虽然同样是基于SDS结构实现的String字符串,却分为了embst和raw两种类型,从上图中我们可以看出embst存储的值比raw小,且embst的内存是直接和redisObject连接在一起的,而raw则是通过指针进行的连接这是因为embstr编码是专门用来保存短字符串的一种优化编码。

        embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

        编码的转换

        (1)数值转字符串:当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。

        (2)字符串修改:对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

       

        二、List

        list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。

        列表对象的编码目前有三种,quicklist、zipmap、linkedlist,不过后两种并没有被使用,因此list类型的编码其实被固定为了quicklist,quicklist在前文中已经做了详细介绍,这里不多赘述。

        三、 Hash

        哈希对象的编码可以是 ziplist 或者 hashtable;对应的底层实现有两种, 一种是ziplist, 一种是dict。

        两种实现

  • 编码为ziplist时的内存分布: 

         当使用ziplist作为底层实现时,新增的键值对是保存到压缩列表的表尾。

  • 编码为hashtable时的内存分布: 

        当使用dict作为底层实现时,哈希对象中的每个键值对都使用一个字典键值对(键与值均是以sds的形式存储的)

        其实使用dict作为Hash的实现已经足够了,就像我们java中的HashMap,但是Redis为了能够节省内存,将ziplist也提供为了hash的实现之一,就是为了能够在元素个数少、元素长度小的场景下尽量的压缩内存占用,达到节省空间的目的

        编码转换

        当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

        1、列表保存元素个数小于512个

        2、每个元素长度小于64字节

        不能满足这两个条件的时候使用 hashtable 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

        四、Set

        实现方式

         集合对象的底层结构有2种,分别是intset和dict。 显然当使用intset作为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数; 而当使用dict作为集合对象的底层实现时, 是将数据全部存储于dict的键中, 值字段闲置不用。

  • intset(注意只能存储整数):

  •  dict(注意这里和Hash种的区别,Hash中key和value都有值,这里只有key有值):

         编码转换

        当集合同时满足以下两个条件时,使用 intset 编码:

        1、集合对象中所有元素都是整数

        2、集合对象所有元素数量不超过512

        不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。

        五、Zset

        zset作为有序集合,使用score对元素进行排序。有序集合的底层实现依然有两种, 一种是使用ziplist作为底层实现, 另外一种比较特殊, 底层使用了两种数据结构: dict与zskiplist。

       实现方式

  •         使用ziplist时:

        ziplist可以有效的节省内存,当数据量不大的时候使用ziplist是个不错的选择,参考上文在hash中使用ziplist作为底层实现的设置,分别使用2个相邻的节点作为key和value,在有序集合中redis将他们调整为了data和score,同时做好去重和排序的工作。

        使用dict+zskiplist时:

         我们从上图中可以看出,这里将dict中的table数组作为了zskiplistNode节点的指针,将两种结构结合了起来。

        这么做的原因在于:

        (1)假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序。

        (2)假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作的复杂度变为了O(logN)。

        将两者结合起来后,技能利用到dict的快速查找能力,又能利用到zskiplist自身有序的特点。

      编码转换

        当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

        1、保存的元素数量小于128;

        2、保存的所有元素长度都小于64字节。

        不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值