Redis实现之对象(一)

Stella981
• 阅读 731

对象

前面我们介绍了Redis的主要数据结构,如:简单动态字符串SDS、双端链表、字典、压缩列表、整数集合等。Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们之前介绍的数据结构

通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以根据不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率

除此之外,Redis的对象系统还实现了基于引用计数的内存回收机制,当程序不再使用某个对象时,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享一个对象来节约内存

最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时间,在服务器启用了maxmemory功能的情况下,空转时间较大的那些键可能会优先被服务器删除

对象的类型编码

Redis使用对象来表示数据库中的键和值,每次当我们在Redis的数据库中新创建一个键值对时,我们至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值 对象)

举个栗子,以下SET命令在数据库将创建一个新的键值对,其中键值对的键是一个包含了字符串"msg"的对象,而键值对的值则是一个包含了字符串"hello world"的对象

127.0.0.1:6379> SET msg "hello world"
OK

Redis中的每个对象都由一个redisObject结构表示,该结构中保存数据相关的三个属性分别是:type、encoding、ptr

redis.h

typedef struct redisObject {
    //类型
    unsigned type:4;
    unsigned notused:2;  
    //编码
    unsigned encoding:4;
    unsigned lru:22;  
    //引用计数
    int refcount;
    //指向底层实现数据结构的指针
    void *ptr;
} robj;

类型

对象的type属性记录了对象的类型,这个属性的值可以是表1-1列出对的常量中的一个

表1-1   对象的类型

类型常量

对象的名称

REDIS_STRING

字符串对象

REDIS_LIST

列表对象

REDIS_HASH

哈希对象

REDIS_SET

集合对象

REDIS_ZSET

有序集合对象

对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种,因此:

  • 当我们称呼一个数据库键为“字符串键”时,我们指的是“这个数据库键所对应的值为字符串对象”
  • 当我们称呼一个数据库键为“列表键”时,我们指的是“这个数据库键所对应的值为列表对象”

TYPE命令的实现方式也与此类似,当我们对一个数据库键执行TYPE命令时,命令返回的结果为数据库键对应的值对象类型,而不是键对象类型:

# 键为字符串对象,值为字符串对象
127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> TYPE msg
string
# 键为字符串对象,值为列表对象
127.0.0.1:6379> RPUSH numbers 1 3 5
(integer) 3
127.0.0.1:6379> TYPE numbers
list
# 键为字符串对象,值为哈希对象
127.0.0.1:6379> HMSET profile name Tome age 25 career Programmer
OK
127.0.0.1:6379> TYPE profile
hash
# 键为字符串对象,值为集合对象
127.0.0.1:6379> SADD fruits apple banana cherry
(integer) 3
127.0.0.1:6379> TYPE fruits
set
# 键为字符串对象,值为有序集合对象
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> TYPE price
zset

表1-2列出了TYPE命令在面对不同类型的值对象时所产生的输出

表1-2   不同类型值对象的TYPE命令输出

对象

对象type属性的值

TYPE命令的输出

字符串对象

REDIS_STRING

string

列表对象

REDIS_LIST

list

哈希对象

REDIS_HASH

hash

集合对象

REDIS_SET

set

有序集合对象

REDIS_ZSET

zset

编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。encoding属性记录了对象使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值可以是表1-3列出的常量的其中一个

表1-3   对象的编码

编码常量

编码所对应的底层数据结构

REDIS_ENCODING_INT

long类型的整数

REDIS_ENCODING_EMBSTR

embstr编码的简单动态字符串

REDIS_ENCODING_RAW

简单动态字符串

REDIS_ENCODING_HT

字典

REDIS_ENCODING_LINKEDLIST

双端链表

REDIS_ENCODING_ZIPLIST

压缩列表

REDIS_ENCODING_INTSET

整数集合

REDIS_ENCODING_SKIPLIST

跳跃表和字典

每种类型的对象都至少使用了两种不同的编码,表1-4列出了每种类型的对象可以使用的编码

表1-4   不同类型和编码的对象

类型

编码

对象

REDIS_STRING

REDIS_ENCODING_INT

使用整数值实现的字符串对象

REDIS_STRING

REDIS_ENCODING_EMBSTR

使用embstr编码的简单动态字符串实现的字符串对象

REDIS_STRING

REDIS_ENCODING_RAW

使用简单动态字符串实现的字符串对象

REDIS_LIST

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的列表对象

REDIS_LIST

REDIS_ENCODING_LINKEDLIST

使用双端链表实现的列表对象

REDIS_HASH

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的哈希对象

REDIS_HASH

REDIS_ENCODING_HT

使用字典实现的哈希对象

REDIS_SET

REDIS_ENCODING_INTSET

使用整数集合实现的集合对象

REDIS_SET

REDIS_ENCODING_HT

使用字典实现的集合对象

REDIS_ZSET

REDIS_ENCODING_ZIPLIST

使用压缩列表实现的有序集合对象

REDIS_ZSET

REDIS_ENCODING_SKIPLIST

使用跳跃表和字典实现的有序集合对象

使用OBJECT ENCODING命令可以查看一个数据库键的值对象的编码:

127.0.0.1:6379> SET msg "hello wrold"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
127.0.0.1:6379> SADD numbers "seven"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"

表1-5列出了不同编码的对象所对应的OBJECT ENCODING命令输出:

表1-5   OBJECT ENCODING对不同编码的输出

对象所使用的底层数据结构

编码常量

OBJECT ENCODING命令输出

整数

REDIS_ENCODING_INT

int

embstr编码的简单动态字符串(SDS)

REDIS_ENCODING_EMBSTR

embstr

简单动态字符串

REDIS_ENCODING_RAW

raw

字典

REDIS_ENCODING_HT

hashtable

双端链表

REDIS_ENCODING_LINKEDLIST

linkedlist

双端链表

REDIS_ENCODING_ZIPLIST

ziplist

整数集合

REDIS_ENCODING_INTSET

intset

跳跃表和字典

REDIS_ENCODING_SKIPLIST

skiplist

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。举个栗子,在列表对象包含的元素比较少时,Redis使用压缩列表作为列表对象的底层实现:

  • 因为压缩列表比双端链表更节约内存,并且在元素比较少时,在内存中以连续块方式保存的压缩列表比起双端链表可以更快被载入到缓存中
  • 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失时,对象就会将底层实现从压缩列表转换成功能更强、更适合保存大量元素的双端链表

其他类型的对象也会通过使用多种不同的编码来进行类似的优化,在接下来的内容中,我们将分别介绍Redis中的五种不同类型的对象,说明这些对象底层所使用的编码方式,列出对象从一种编码转换成另一种编码所需的条件,以及同一个命令在多种不同编码上的实现方法

字符串对象

字符串对象的编码可以是int、raw或者embstr。如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串结构的ptr属性中(将void *转换成long),并将字符串对象的编码设置为int

举个栗子,如果我们执行以下SET命令,那么服务器将创建一个如图1-1所示的int编码的字符串对象作为number键的值:

127.0.0.1:6379> SET number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"

 Redis实现之对象(一)

图1-1   int编码的字符串对象

如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。举个栗子,如果我们执行以下命令,那么服务器将创建一个如图1-2所示的raw编码的字符串作为store键的值 

127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived a king ..."
OK
127.0.0.1:6379> STRLEN story
(integer) 55
127.0.0.1:6379> OBJECT ENCODING story
"raw"

 Redis实现之对象(一)

图1-2   raw编码的字符串对象

如果字符串对象保存的是一个字符串,并且这个字符串长度小于等于44字节,那么字符串将使用embstr编码,看下面的示例

127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived"
OK
127.0.0.1:6379> STRLEN story
(integer) 44
127.0.0.1:6379> OBJECT ENCODING story
"embstr"
127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived "
OK
127.0.0.1:6379> STRLEN story
(integer) 45
127.0.0.1:6379> OBJECT ENCODING story
"raw"

embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码方式和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr结构,如图1-3所示

Redis实现之对象(一)

图1-3   embstr编码创建的内存块结构

embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串来保存短字符串值有以下好处:

  • embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
  • 释放embstr编码的字符串对象只需调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存中,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势

作为例子,以下命令创建一个embstr编码的字符串对象作为msg键的值,值对象的样子如图1-4所示

127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"

 Redis实现之对象(一)

图1-4   embstr编码的字符串对象

最后要说的是,可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的。如果我们要保存一个浮点数到字符串对象里面,那么程序先将这个浮点数转换成字符串值,然后将其保存。举个栗子,执行以下代码将创建一个包含3.14的字符串对象

127.0.0.1:6379> SET pi 3.14
OK
127.0.0.1:6379> OBJECT ENCODING pi
"embstr"

在有需要的时候,程序会将保存在字符串对象中的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得的浮点数转换回字符串值,并继续保存在字符串对象里面。举个栗子,我们执行以下代码:

127.0.0.1:6379> SET pi 3.14
OK
127.0.0.1:6379> INCRBYFLOAT pi 3.0
"6.14"
127.0.0.1:6379> OBJECT ENCODING pi
"embstr

 程序首先会取出字符串对象中保存的字符串值"3.14",将它转换回浮点数值3.14,然后把3.14和2.0相加得到5.14后在转换回字符串,并将字符串"5.14"保存到字符串对象中。表1-6总结并列出字符串对象保存各种不同类型的值所使用的编码方式

表1-6   字符串对象保存各类型值的编码方式

编码

可以用long类型保存的整数

int

可以用long double类型保存的浮点数

embstr或者raw

字符串值,或者因为长度太大而没办法用long类型表示的整数,又或者因为长度太大而没办法用long double类型表示的浮点数

embstr或者raw

编码的转换

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象。对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得对象保存的不再是整数值,而是一个字符串值,那么字符串对象将从int变为raw

下面的示例中,我们通过APPEND命令,向一个保存整数值的字符串追加一个字符串值,因为追加操作只能对字符串值执行,所以程序会将之前保存的整数值转换为字符串,然后再执行追加操作,操作的执行结果就是一个raw编码的、保存了字符串值的字符串对象

127.0.0.1:6379> SET number 10086
OK
127.0.0.1:6379> OBJECT ENCODING number
"int"
127.0.0.1:6379> APPEND number " is a good number!"
(integer) 23
127.0.0.1:6379> GET number
"10086 is a good number!"
127.0.0.1:6379> OBJECT ENCODING number
"raw"

另外,因为Redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有这些程序),所以embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序先将对象的编码从embstr转换成raw,然后再执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象

以下代码展示了一个embstr编码的字符串对象在执行APPEND命令之后,对象的编码从embstr变为raw的例子:

127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> OBJECT ENCODING msg
"embstr"
127.0.0.1:6379> APPEND msg " again!"
(integer) 18
127.0.0.1:6379> OBJECT ENCODING msg
"raw"

字符串命令的实现

因为字符串键的值为字符串对象,所以用于字符串键的所有命令都是针对字符串对象来构建的,表1-7例举了其中一部分字符串命令,以及这些命令在不同编码的字符串对象下的实现方法

表1-7   字符串命令的实现

命令

int编码的实现方法

embstr编码的实现方法

raw编码的实现方法

SET  

使用int编码保存值

使用embstr编码保存值

使用raw编码保存值

GET

拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值

直接向客户端返回字符串值

直接向客户端返回字符串值

APPEND

将对象转换成raw编码,然后按raw编码的方式执行此操作

将对象转换成raw编码,然后按raw编码的方式执行此操作

调用sdscatlen函数,将给定字符串追加到现有字符串的末尾

INCRBYFLOAT   

取出整数值并将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来

取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。

如果字符串值不能被转换成浮点数,那么向客户端返回一个错误

取出字符串值并尝试将其转换成long double类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。

如果字符串值不能被转换成浮点数,那么向客户端返回一个错误

INCRBY

对整数值进行加法计算,得出的计算结果会作为整数被保存起来

embstr编码不能执行此命令,向客户端返回一个错误

raw编码不能执行此命令,向客户端返回一个错误

DECRBY

对整数值进行减法计算,得出的计算结果会作为整数被保存起来

embstr编码不能执行此命令,向客户端返回一个错误

raw编码不能执行此命令,向客户端返回一个错误

STRLEN

拷贝对象所保存的整数值,将这个拷贝转换成字符串值,计算并返回这个字符串值的长度

调用sdslen函数,返回字符串的长度

调用sdslen函数,返回字符串的长度

SETRANGE

将对象转换成raw编码,然后按raw编码的方式执行此命令

将对象转换成raw编码,然后按raw编码的方式执行此命令

将字符串特定索引上的值设置为给定的字符

GETRANGE

拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后取出并返回字符串指定索引上的字符

直接取出并返回字符串指定索引上的字符

直接取出并返回字符串指定索引上的字符

点赞
收藏
评论区
推荐文章
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
redis数据结构底层实现
一.redis常用的数据结构有哪几种?1.简单字符串:String2.列表:List3.键值对:Hash4.唯一集合:Set5.有序唯一集合:SortedSet二.每种数据结构对应的底层实现1.首先需要知道
Irene181 Irene181
3年前
简述Python中常见的数据结构
「数仓宝贝库」,带你学数据!导读:Python中常见的数据结构有列表(list)、元组(tuple)、集合(set)、字典(dict)等,这些数据结构表示了自身在Python中的存在形式,在Python中可以输入type(对象)查看数据类型。1列表(1)创建列表列表是Python内置的一种数据类型,它是一种有序的数据集合,是用于存储一连串
深入理解跳表及其在Redis中的应用
跳表可以达到和红黑树一样的时间复杂度O(logN),且实现简单,Redis中的有序集合对象的底层数据结构就使用了跳表。其作者威廉·普评价:跳跃链表是在很多应用中有可能替代平衡树的一种数据结构。本篇文章将对跳表的实现及在Redis中的应用进行学习。
Stella981 Stella981
3年前
Redis 为什么这么快? Redis 的有序集合 zset 的底层实现原理是什么? —— 跳跃表 skiplist
Redis有序集合zset的底层实现——跳跃表skiplistRedis简介Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(Strings),散列(Hash),列表(List),集合(S
Stella981 Stella981
3年前
Redis 对象系统
Redis为了更好的实现键值对数据库,创建了一个对象系统,以下为Redis对象系统的相关知识简介。redisObjectRedis使用对象来表示数据库中的键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象。一个为键对象,一个为值对象。Redis对象的定义如下:typedef
Wesley13 Wesley13
3年前
unity将 -u4E00 这种 编码 转汉字 方法
 unity中直接使用 JsonMapper.ToJson(对象),取到的字符串,里面汉字可能是\\u4E00类似这种其实也不用转,服务器会通过类似fastjson发序列化的方式,将json转对象,获取对象的值就是中文但是有时服务器要求将传参中字符串中类似\\u4E00这种转汉字,就需要下面 publ
Stella981 Stella981
3年前
Redis 避不开的五种数据结构
Redis中有5种数据结构,分别是字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(SortedSet),因为使用Redis场景的开发中肯定是无法避开这些基础结构的,所以熟练掌握它们也就成了一项必不可少的能力。本文章精要地介绍了Redis的这几种数据结构,主要覆盖了它们各自的定义、基本用法与相关要点。字
Wesley13 Wesley13
3年前
.Net转Java自学之路—基础巩固篇十三(集合)
集合:集合是用于存储对象的一个工具。  集合与数组的特点    相同点:都是一个容器    不同点:      集合:可以存储对象,只能存储对象,集合长度可变。      数组:可以存储对象,也可以存储基本数据类型,数组长度固定。  容器对象有很多种,通过内部的数据结构来区分,数据结构就是一种数据存储方式。  在不断
3A网络 3A网络
2年前
Redis 存储对象信息是用 Hash 还是 String
Redis存储对象信息是用Hash还是StringRedis内部使用一个RedisObject对象来表示所有的key和value,RedisObject中的type,则是代表一个value对象具体是何种数据类型,它包含字符串(String)、链表(List)、哈希结构(Hash)、集合(Set)、有序集合(Sortedset)。