为了账号安全,请及时绑定邮箱和手机立即绑定

redis实现之对象分析

2019.08.14 16:22 79浏览

对象

本篇文章着重介绍一下,redis中大家日常使用的对象实现原理,这里附上这一系列的数据结构讲解,有需要的请点击查看哟。

前面连续介绍了redis的数据结构,但是redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,每种对象系统包含至少一种之前介绍的数据结构来实现。

本节内容的提纲如下:

在这里插入图片描述

1、对象的类型与编码

redis使用对象来表示数据库中的键和值,每次当我们在redis的数据库中新建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),一个对象用作键值对的值(值对象)。对于redis来说,键总是一个字符串对象。

redis中的每一个对象都由一个redisObject结构表示,伪代码如下:

typedef struct redisObject{
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //指向底层实现数据结构的指针
    void *ptr;
    //...
}robj;

type属性的取值如下表所示:

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

不同类型和编码对象信息如下图:

在这里插入图片描述

1.1、字符串对象

字符串编码可以使int、raw和embstr。当键值是一个整数值得时候用long类型来表示;当键值是一个字符串,并且字符串的长度小于39字节使用SDS来表示;当键值是一个字符串,并且字符串的长度大于39字节,使用embstr方式来表示。

embstr编码字符串的优点:

  1. embstr编码将创建字符串对象所需的内存分配次数从raw编码(SDS)的两次变为一次。
  2. 释放字符串对象也只需要调用一次释放函数。
  3. embstr编码的字符串所有内容都保存在一块连续的内存中,所以能够更好的利用缓存。

1.1.1、字符串对象保存的各类值得编码方式:

编码
可以利用long类型保存的整数 int
可以用long double类型保存的浮点数 embstr或者raw
字符串值,或者无法用long double与long表示的数 embstr或者raw

因为redis没有为embstr对象编码的字符串对象编写任何响应的修改程序,所以embstr编码的字符串实际上是只读的,当我们对字符串进行任何修改时,程序会先将字符串编码从enbstr转换成raw,然后再执行修改命令。

1.1.2、字符串命令的实现

命令 int编码 embstr编码 raw编码
SET 使用int保存值 使用embstr保存值 使用raw保存值
GET 拷贝整数值,将拷贝转换为字符串,返回客户端这个字符串 直接返回字符串 直接返回字符串
APPEND 将对象转换成raw编码,按照raw编码方式执行此操作 将对象转换成raw编码,按照raw编码方式执行此操作 调用sdscatlen函数,将给定字符串追加到末尾。
INCRBYFLOAT 取出整数值转换为long double类型浮点数,进行加法运算,然后保存浮点数结果 取出字符串,尝试转换为long double类型浮点数,进行加法运算,保存浮点数结果。尝试失败,返回错误。 取出整数值转换为long double类型浮点数,进行加法运算,然后保存浮点数结果
INCRBY 对整数做加法运算,保存整数结果。 不执行,返回错误。 不执行,返回错误。
DECRBY 对整数做减法运算,保存整数结果。 不执行,返回错误。 不执行,返回错误。
STRLEN 拷贝整数值,将拷贝转换为字符串,计算字符串长度 调用sdslen函数 调用sdslen函数
STRANGE 将对象转换成raw编码,按照raw编码方式执行此操作 将对象转换成raw编码,按照raw编码方式执行此操作 将字符串特定索引上的值设置为给定字符串
GETRANGE 拷贝整数值,将拷贝装换为字符串,返回字符串指定索引上的字符 直接取出返回字符串指定索引上的字符 直接取出返回字符串指定索引上的字符
1.2、列表对象

列表对象的编码可以是压缩列表或者双端链表。这里需要注意的是双端链表底层是实现逻辑,链表节点中包含了字符串对象。

字符串对象是Redis五中类型的对象中唯一一个会被其他四种对象嵌套的对象。

1.2.1、编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用压缩列表(ziplist)编码

  • 列表对象保存的所有字符串元素的长度都小于64字节。
  • 列表对象保存的元素数量小于512个;

不能满足这两个条件的列表对象需要使用双端列表(linkedlist)编码。以上两个条件是可以修改的。

1.2.2、命令实现

命令 ziplist编码 linkedlist编码
LPUSH 调用ziplistPush函数,将新元素推入到压缩列表的表头 调用listAddNodeHead函数,将新元素推入到双端链表的表头
RPUSH 调用ziplistPush函数,将新元素推入到压缩列表的表尾 调用listAddNodeTail函数,将新元素推入到双端链表的表尾
LPOP 调用ziplistIndex函数定位到压缩列表的表头节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表头节点 调用listFirst函数定位双端链表的表头节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表头节点
RPOP 调用ziplistIndex函数定位到压缩列表的表尾节点,在向用户返回节点所保存的元素之后,调用ziplistDelete函数删除表尾节点 调用listFirst函数定位双端链表的表尾节点,在向用户返回节点所保存的元素之后,调用listDelNode函数删除表尾节点
LINDEX 调用ziplistIndex函数定位压缩列表中的指定节点,然后返回节点保存的元素 调用listIndex函数定位双端链表中指定的节点,然后返回节点所保存的元素。
LIEN 调用ziplistLen函数返回压缩列表的长度 调用listLength函数返回双端链表的长度
LINSERT 插入新节点到压缩列表的表头或者表尾时,使用ziplistPush函数;插入到其他位置时,使用ziplistInsert函数 调用listInsertNode函数,将新节点插入到双端链表中的指定位置
LREM 遍历压缩列表,调用ziplistDelete函数删除包含了给定元素的节点 遍历链表,调用listDelNode函数删除包含了指定元素的节点
LTRIM 调用ziplistDeleteRange函数,删除压缩列表中所有不在指定索引范围内的节点 遍历链表,调用listDelNode函数删链表中所有不在指定索引范围内的节点
LSET 调用ziplistDelete函数,先删除压缩列表指定索引上现有的节点,然后调用ziplistInsert函数,将一个包含新元素的新节点插入到相同索引上 调用listIndex函数,定位到链表中指定的索引节点上,然后通过赋值操作更新节点的值
1.3、哈希对象

哈希对象的编码可以使压缩列表(ziplist)和哈希表(hashtable)。压缩列表作为底层实现时,每当有新的键值对要加入哈希对象时,程序会先将保存了键的节点推入列表表尾,然后将保存了值的节点推入到压缩列表表尾。

  • 保存了同一键值对的两个节点总是紧邻在一起,保存键的节点在前,保存值得节点在后。
  • 新加入的节点总是在表尾

1.3.1、编码装换

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:

  • 哈希对象保存的键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个;

1.3.2、命令实现

命令 ziplist编码 hashtable编码
HSET 首先调用ziplistPush函数吗,将键推入表尾;再将值推入表尾 调用dictAdd函数,将新节点添加到字典里
HGET 调用ziplistFind找到指定的节点,调用ziplistNext函数将指针移动到键节点旁边的值节点,返回值节点 调用dictFind函数在字典里查找给定的键,调用dictGetVal函数返回该键所对应的的值
HEXISTS 调用ziplistFind找到指定的节点,找到说明键存在,否则不存在 调用dictFind函数在字典中查找指定键,找到说明键存在,否则不存在
HDEL 调用ziplistFind找到指定键的节点,删除键以及值节点 调用dictDelete函数,将指定键的键值对从字典中删除
HLEN 调用ziplistLen函数,取得压缩列表节点的总数量,将这个数量除以2就是键值对的数量 调用dictSize函数返回字典中的键值对数量
HGETALL 遍历整个压缩列表,用ziplistGet函数返回所有键和值 遍历整个字典,用dictGetKey函数返回字典的键,用dictGetVal函数返回字典的值
1.4、集合对象

集合对象的编码可以是intset或者hashtabe。

1.4.1、编码转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码:

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512

1.4.2、命令的实现

命令 intset hashtable
SADD 调用intsetAdd函数,将所有新元素添加都整数集合中 调用dictAdd,以新元素为键,null为值,将键值对添加到字典里面
SCARD 调用intsetLen函数,返回整数集合所包含的元素数量 调用dictSize函数,返回字典所包含的键值对数量
SISMEMBER 调用intsetFind函数,在这个整数集合中查找给定的元素 调用dictFind函数,在字典的键中查找给定的元素
SMEMBERS 遍历整个整数集合,使用intsetGet函数返回集合元素 遍历整个字典,使用dictGetKey函数返回字典的键
SRANDMEMBER 调用intsetRandom函数,从整数集合中随机返回一个数 调用dictGetRandomKey函数,从字典中随机返回一个字典键
SPOP 调用intsetRandom函数,从集合中随机选取一个元素返回,然后调用intsetRomove函数删除该元素 用dictGetRandomKey函数,从字典中随机返回一个字典键,然后调用dictDelete函数,从字典总删除该键对应的键值对
SREM 调用intsetRomove函数,从集合中删除所有给定的元素 调用dictDelete函数,从字典中删除所有键为给定元素的键值对
1.5、有序集合对象

有序集合的编码可以是压缩列表和跳跃表。
使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的节点来保存,一个节点保存元素的成员。一个节点保存元素的分值。

这里用图片展示跳跃表的结构,语言显得说不清楚:

在这里插入图片描述

字典和跳跃表会共享元素的成员和分值,所以并不会造成任何数据重复。

1.5.1、编码转换
当有序集合对象可以同时满足以下两个条件时,使用压缩列表实现:

  • 有序集合保存的元素数量小于128个
  • 有虚假和保存的所有元素的长度都小于64字节

1.5.2、命令实现

命令 ziplist编码 zset编码
ZADD 调用ziplistInsert函数,将成员和分值作为两个节点分别插入压缩列表 先调用zslInsert函数,将新元素添加到跳跃表,然后调用dictAdd函数,将新元素关联到字典
ZCARD 调用ziplistLen函数获得列表节点数,除以2返回 访问跳跃表的length属性
ZCOUNT 遍历列表,统计分值在给定范围内的节点数量。 遍历跳跃表,统计分值在给定范围内的节点数量
ZRANGE 从表头向表尾遍历,返回给定索引范围内的所有元素 从表头向表尾遍历跳跃表,返回给定索引内的所有元素
ZREVRANGE 从表尾向表头遍历,返回给定索引范围内的所有元素 从表尾向表头遍历跳跃表,返回给定索引内的所有元素
ZRANK 遍历列表,记录经过的节点数,找到后返回排名 遍历跳跃表,记录经过的节点数,找到后返回排名
ZREVRANK 反向遍历列表,记录经过的节点数,找到后返回排名 反向遍历跳跃表,记录经过的节点数,找到后返回排名
ZREM 遍历列表,删除所有包含给定成员的节点,以及被删除节点旁边的分值节点 遍历跳跃表,删除所有包含给定成员的节点。并在字典中解除被删除元素成员和分值的关联
ZSCORE 遍历列表,查找包含给定成员的节点,然后取出分值节点的分值 直接从字典中取出给定成员的分值。
2、类型检查

除了以上列举的特定类型的命令,redis中还有可以操作任何类型的命令,DEL、EXPIRE、RENAME、TYPE、OBJECT等。redis会先检查给出的命令是否正确,才会决定是否执行具体的命令。

3、内存回收

redis在系统中构建了一个引用计数计数,实现了内存的回收机制。

计数信息状态变化:

  • 创建一个对象时,引用计数的值为1
  • 当对象被引用时,计数值加一
  • 当对象不再被引用时,计数值减一
  • 当计数值为0时,对象占用的内存被释放
4、对象共享

因为引用计数技术的引入,对象可以被共享。

  • 将数据库的值指针指向一个现有的值对象
  • 将被共享的值对象的引用计数加一

redis在初始化的时候,会创建一万个字符串对象,用于共享。

5、空转时长

每个对象还有一个lru属性,记录了对象最后一次被访问的时间。空转时长较高的对象会被有限回收。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

相关文章推荐

正在加载中
意见反馈 邀请有奖 帮助中心 APP下载
官方微信

举报

0/150
提交
取消