Redis设计与实现(字符串对象)

Redis的键值对都是由对象来表示的,即每次创建一个新的键值对时,我们会至少创建两个对象,一个对象用作键(键对象),另一个用作值(值对象)。

Redis的每一个对象都是由一个 redisObject 结构表示,该结构中和保存数据有关的三个属性分别是 typeencodingptr 属性:

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

类型

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

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

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

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

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

所以当我们队一个数据库键执行 TYPE 命令时,命令返回的结果为数据库键对应的值对象的类型,而不是键对象的类型;

1
2
3
4
5
6
7
8
9
10
# 键为字符串对象,值为字符串对象
redis> SET msg "hello world"
OK
redis> TYPE msg
string
# 键为字符串对象,值为列表对象
redis> RPUSH numbers 1 3 5
(integer) 6
redis> TYPE numbers
list

下表为不同类型值对象的TYPE命令输出:

对象 对象 type 属性的值 TYPE命令的输出
字符串对象 REDIS_STRING “string”
列表对象 REDIS_LIST “list”
哈希对象 REDIS_HASH “hash”
集合对象 REDIS_SET “set“
有序集合对象 REDIS_ZSET “zset”

编码和底层实现

对象的 ptr 指针指向对象的底层实现数据结构,而这些数据结构由对象的 encoding 属性决定。

encoding 属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现,这个属性的值在下表中列出:

编码常量 编码所对应的底层数据结构
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 跳跃表和字典

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

类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象
REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象
REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_ENCODING_LINKEDLIST 使用双端链表实现的哈希对象
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象
REDIS_ENCODING_HT 使用字典实现的哈希对象
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_ENCODING_HT 使用字典实现的集合对象
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

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

1
2
3
4
5
6
7
8
redis> SET msg "hello wrold"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> SET story "long long long long long long ago ..."
OK
redis> OBJECT ENCODING story
"raw"

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使用压缩列表作为列表对象的底层实现:

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

字符串对象

字符串对象的编码可以是 intrawembstr

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型表示,那么字符串对象会将整数值保存在字符串对象结构的 ptr 属性里面(void* -> long),并将字符串对象的编码设置为 int
  • 如果字符串对象保存的是字符串值,并且这个字符串的长度大于 39 字节,那么字符串对象将会使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw
  • 如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于 39 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串值。

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

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

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

可以用 long double 类型表示的浮点数在Redis中也是作为字符串值来保存的:如果保存一个浮点数到字符串对象里,那么程序会先将这个浮点数转换成字符串值,然后再保存起转换所得的字符串值。

在有需要的时候,程序会把保存在字符串对象里的字符串值转换为浮点数,执行某些操作,执行之后把所得的浮点数值转换为字符串值,继续保存在字符串对象里。(主要是体现在某些运算命令上(+-*/))

下表为字符串对象保存各类型值的编码方式:

编码
long 类型保存的整数 int
long doube 类型保存的浮点数 embstrraw
字符串值,或因为长度太大而无法用 long 类型表示的整数,或太大无法用 long double 类型表示的浮点数。 embstr 或者 raw

编码的转换

int 编码的字符串对象和 embstr 编码的字符串对象在条件满足的情况下,会被转换为 raw 编码的字符串对象。

对于 int 编码的字符串对象,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从 int 变为 raw

示例:

1
2
3
4
5
6
7
8
9
10
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
redis> APPEND number " is a good number!"
(integer) 23
redis> GET number
"10086 is a good number!"
redis> OBJECT ENCODING number
"raw"

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

字符串命令的实现:

命令 int编码的实现方法 embstr编码的实现方法 raw编码的实现方法
SET 使用 int 编码保存值。 使用 embstr 编码保存值。 使用 raw 编码保存值。
GET 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后向客户端返回这个字符串值。 直接向客户端返回字符串值。 直接向客户端返回字符串值。
APPEND 将对象转换成 raw 编码,然后按 raw 编码的方式执行此操作。 将对象转换成 raw编码,然后按 raw 编码的方式执行此操作。 调用 sdscatlen 函数,将给定欺负穿追加到现有字符串的末尾。
INCRBYFLOAT 取出整数值,并将其转换成 long double 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。 取出字符串并尝试将其转换成 long double 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。如果字符串不能被转换成浮点数,那么向客户端返回一个错误。 取出字符串并尝试将其转换成 long double 类型的浮点数,对这个浮点数进行加法计算,然后将得出的浮点数结果保存起来。如果字符串不能被转换成浮点数,那么向客户端返回一个错误。
INCYBY 对整数值进行加法计算,得到的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令,向客户端返回一个错误。 raw 编码不能执行此命令,向客户端返回一个错误。
DECRBY 对整数值进行减法运算,得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令,向客户端返回一个错误。 raw 编码不能执行此命令,向客户端返回一个错误。
STRLEN 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,计算并返回这个字符串值的长度。 调用 sdslen 函数,返回字符串的长度。 调用 sdslen 函数,返回字符串的长度。
SETRANGE 将对象转换成 raw 编码,然后按 raw 编码的方式执行此命令。 将对象转换成 raw编码,然后按 raw 编码的方式执行此命令。 将字符串特定索引上的值设置为给定的字符。
GETRANGE 拷贝对象所保存的整数值,将这个拷贝转换成字符串值,然后取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。

本文标题:Redis设计与实现(字符串对象)

文章作者:Tokey

发布时间:2019年07月05日 - 09:07

最后更新:2021年06月29日 - 22:06

原始链接:http://TokeyRoad.github.io/2019/07/05/Redis设计与实现-字符串对象/

许可协议: 转载请保留原文链接及作者。

0%