对象
本篇文章着重介绍一下,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编码字符串的优点:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码(SDS)的两次变为一次。
- 释放字符串对象也只需要调用一次释放函数。
- 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属性,记录了对象最后一次被访问的时间。空转时长较高的对象会被有限回收。