Redis设计与实现 第 8 章 对象

第 8 章 对象

8.1 对象的类型与编码

Redis 使用对象来表示数据库中的键和值,即每次创建一个键值对时,至少创建了两个对象,一个是键对象,一个是值对象

Redis 每个对象都是由 redisObject 结构表示

redisObject

8.1.1 类型

type 记录了对象类型

对象类型

对于 Redis 键值对来说,键总是字符串对象,而值是不确定的

  • 字符串键:键对应的值也是字符串对象
  • 列表键:值是列表对象

TYPE 命令返回的也是值对象类型

127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> type msg
string
127.0.0.1:6379> rpush num 1 3 5
(integer) 3
127.0.0.1:6379> type num
list

Type命令输出

8.1.2 编码和底层实现

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

对象的编码

每种类型的对象至少使用了两种不同的编码,即至少拥有两种不同的数据结构

不同类型和编码的对象

OBJECT ENCODING 可以查询键值对的值对象编码

![object encoding 对不同编码的输出](https://cdn.jsdelivr.net/gh/ZephXu07/newimages@master/20210618/object encoding 对不同编码的输出.746naftj4j40.jpg)

使用 encoding 属性来设定对象使用的编码比为特定对象关联固定编码来说比较灵活,Redis 根据对象的元素数量、类型等因素改变底层结构时使用 encoding 编码即可灵活改变

8.2 字符串对象

字符串对象的编码有 int、raw、embstr

字符串对象如果是整数值,且可以用 long 类型表示,则 字符串对象会将整数只保存在字符串对象结构的 ptr 属性中,编码也会被设置为 int

127.0.0.1:6379> set number 10086
OK
127.0.0.1:6379> object encoding number
"int"

int编码的字符串对象

字符串对象如果是一个字符串值,且大于 32 字节,则使用 SDS 来报存,且对象的编码被设置为 raw

127.0.0.1:6379> set slogan "good good study, day day up,good good study, day day up "
OK
127.0.0.1:6379> strlen slogan
(integer) 56
127.0.0.1:6379> object encoding slogan
"raw"

raw编码的字符串对象

反之小于等于 32 字节则使用 embstr 编码方式

embstr 编码是专门用于报存短字符串的一种优化编码方式,和 raw 编码一样使用 redisObject 和 sdshdr 结构来表示字符串对象,但 raw 是调用两次内存分配来创建两个结构,embstr 则是一次内存分配一段连续空间来包含两个结构

emstr创建的内存块结构

embstr 和 raw 在执行命令时效果是一样的,但有以下好处

  • 内存分配次数变成一次
  • 释放对象只需要调用两次内存释放函数
  • 都分配在连续内存中,能有缓存上的优势
127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> object encoding msg
"embstr"

embstr编码的字符串对象

long double 类型的浮点数在 redis 中也是字符串保存的,程序会将其转换成字符串值再保存

127.0.0.1:6379> set pi 3.14
OK
127.0.0.1:6379> object encoding pi
"embstr"

值与编码对应

8.2.1 编码的转换

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

如 int 编码的字符串对象,执行了命令使其保存的不是整数值而是字符串值,int 编码会变成 raw 编码

127.0.0.1:6379> set num 7
OK
127.0.0.1:6379> object encoding num
"int"
127.0.0.1:6379> append num "is my love number"
(integer) 18
127.0.0.1:6379> get num
"7is my love number"
127.0.0.1:6379> object encoding num
"raw"

append 命令只能添加在字符串值后

embstr 编码实际上是只读的,当需要对 embstr 编码的字符串对象修改时,会转成 raw 编码再进行修改

127.0.0.1:6379> set msg "hello embstr"
OK
127.0.0.1:6379> object encoding msg
"embstr"
127.0.0.1:6379> append msg "again"
(integer) 17
127.0.0.1:6379> object encoding msg
"raw"

8.2.2 字符串命令的实现

字符串键的值为字符串对象,所以用于字符串键的所有命令都是针对字符串对象来构建的

字符串命令的实现

8.3 列表对象

列表对象的编码可以是 ziplist 或者 linkedlist

ziplist 编码底层是压缩列表实现的,每个压缩列表节点保存一个列表元素

127.0.0.1:6379> rpush num 1 "three" 5
(integer) 3
127.0.0.1:6379> object encoding num
"ziplist"

ziplist编码的numbers列表对象

linkedlist 编码的列表对象使用双端链表作为底层实现,每个节点保存了一个字符串对象,每个字符串对象则报存了一个列表元素

linkedlist编码的numbers列表对象

双端列表结构中包含了多个字符串对象

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

StringObject 实际上是简化的

简化与完整字符串对象

8.3.1 编码转换

使用 ziplist 编码

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

以上两个条件的上限值可以修改

  • list-max-ziplist-value
  • list-max-ziplist-entries

否则则是 linkedlist 编码

127.0.0.1:6379> rpush blah "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcabcabcabc"
(integer) 10
127.0.0.1:6379> object encoding blah
"ziplist"
127.0.0.1:6379> rpush blah "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcabcabcabca"
(integer) 11
127.0.0.1:6379> object encoding blah
"linkedlist"
127.0.0.1:6379>

在最后一个总共65个字符,65字节

127.0.0.1:6379> EVAL "for i = 1, 512 do redis.call('rpush',KEYS[1],i)end" 1 "integers"
(nil)
127.0.0.1:6379> llen integers
(integer) 512
127.0.0.1:6379>  object encoding integers
"ziplist"
127.0.0.1:6379> rpush integers 513
(integer) 513
127.0.0.1:6379> object encoding integers
"linkedlist"
127.0.0.1:6379>

8.2.3 列表命令的实现

列表命令实现

8.4 哈希对象

编码有 ziplist 或者 hashtable

ziplist 的哈希对象使用压缩列表作为底层实现,当新键值对加入哈希对象时,会先将保存了键的压缩列表节点推入压缩列表表尾,再将值的压缩列表节点也推入压缩列表表尾,于是

  • 报存了同一键值对的两个节点相邻,键在前,值在后

  • 先来的在压缩列表表头,后来的在表尾

127.0.0.1:6379> hset profile name "me"
(integer) 1
127.0.0.1:6379> hset profile age 21
(integer) 1
127.0.0.1:6379> hset profile career "student"
(integer) 1

压缩列表实现的哈希对象

hashtable 编码的哈希对象使用字典作为底层实现,每个键值对都使用字典键值对来保存

  • 字典的每个键都是字符串对象,保存键
  • 字典的每个值都是字符串对象,保存值

字典实现的哈希对象

8.4.1 编码转换

使用 ziplist 编码

  • 键值对键和值的长度都小于 64 字节
  • 键值对数量小于 512

同样可由参数设置上限值

  • hash-max-ziplist-value
  • hash-max-ziplist-entries
127.0.0.1:6379> hset book name "qweqweqweqweqweqwe"
(integer) 1
127.0.0.1:6379> object encoding book
"ziplist"
127.0.0.1:6379> hset book bite_bite_bite_bite_bite_bite_bite_bite_bite_bite_bite_bite_bite_ "content"
(integer) 1
127.0.0.1:6379> object encoding book
"hashtable"
127.0.0.1:6379> hset person age 25
(integer) 1
127.0.0.1:6379> object encoding person
"ziplist"
127.0.0.1:6379> hset person name "aabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbbaabbb"
(integer) 1
127.0.0.1:6379> object encoding person
"hashtable"
127.0.0.1:6379> EVAL "for i = 1,512 do redis.call('HSET', KEYS[1],i,i)end" 1 "numnum"
(nil)
127.0.0.1:6379> hlen numnum
(integer) 512
127.0.0.1:6379> object encoding numnum
"ziplist"
127.0.0.1:6379> hmset numnum "key" "value"
OK
127.0.0.1:6379> hlen numnum
(integer) 513
127.0.0.1:637

8.4.2 哈希命令的实现

哈希命令的实现

8.5 集合对象

集合对象的编码有 intset、hashtable

inset 编码的底层实现为 intset

hashtable 使用字典作为底层实现,键为字符串对象,而值全部为 NULL

intset与hashtable编码

8.5.1 编码的转换

intset 编码

  • 所有元素都是整数值
  • 元素数量不超过 512 个

反之则为 hashtable 编码

同理上限值可修改,set-max-intset-entries

127.0.0.1:6379> sadd n 1 3 5 7
(integer) 4
127.0.0.1:6379> object encoding n
"intset"
127.0.0.1:6379> sadd n "num"
(integer) 1
127.0.0.1:6379> object encoding n
"hashtable"
127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('SADD',KEYS[1],i) end" 1 nn
(nil)
127.0.0.1:6379> scard nn
(integer) 512
127.0.0.1:6379> object encoding nn
"intset"
127.0.0.1:6379> sadd nn 882
(integer) 1
127.0.0.1:6379> scard nn
(integer) 513
127.0.0.1:63

8.5.2 集合命令的实现

集合命令的实现

8.6 有序集合对象

编码有 ziplist、skiplist

ziplist 使用压缩列表,每个集合元素使用两个相邻的压缩列表节点保存,第一个是元素的成员,第二个是元素的分值,按照分值从小到大排序,小的在表头附近,大的在表尾

127.0.0.1:6379> zadd age 18 a 19 b 21 me
(integer) 3
127.0.0.1:6379> object encoding age
"ziplist"

压缩列表实现有序集合

skiplist 编码使用 zset 实现,一个 zset 结构同时包含一个字典和跳跃表

typedef struct zset {
    zskiplist *zsl;
    dict *dict;
}zset;

zsl 跳跃表按分值从小到大保存了所有的集合元素,每个跳跃表节点即保存了一个集合元素:object 属性保存了元素的成员,score 属性保存了元素的分值

通过跳跃表可以对有序集合进行范围型操作,如 ZRANK、ZRANGE

zset 的 dict 字典为有序集合创建了一个从成员到分值的映射,键保存了元素的成员,值保存了元素的分值,通过这样即可用 O(1) 的复杂度来查找成员的分值,如 ZSCORE 命令

有序集合的元素成员是字符串对象,分值是 double 类型的浮点数

跳跃表和字典通过指针来共享相同元素的成员和分值,不会造成浪费

使用跳跃表和字典来实现有序集合:

保持有序的同时使查找速度尽量地快

字典和跳跃表实现有序集合

图展示时元素的成员和分值重复了,但实际中是共享的

8.6.1 编码的转换

使用 ziplist 编码

  • 元素数量小于 128 个
  • 元素成员长度都小于 64 字节

同理以下参数可修改

  • zset-max-ziplist-entries
  • zset-max-ziplist-value

反之则是 skiplist 编码的字典和跳跃表实现

127.0.0.1:6379> EVAL "for i=1,128 do redis.call('zadd',KEYS[1],i,i) end" 1 nnnn
(nil)
127.0.0.1:6379>  zcard nnnn
(integer) 128
127.0.0.1:6379>  object encoding nnnn
"ziplist"
127.0.0.1:6379>  zadd nnnn 3.14 pi
(integer) 1
127.0.0.1:6379>  object encoding nnn
"skiplist"

8.6.2 有序集合命令的实现

有序集合命令的实现

8.7 类型检查与命令多态

两种类型的命令:

  • 对任何类型的键执行

    • DEL、EXPIRE、RENAME、TYPE、OBJECT
  • 对特定类型的键执行

    • SET、GET、APPEND、STRLEN 对字符串键执行

    • HDEL、HEST、HGET、HLEN 对哈希键执行

    • RPUSH、LPOP、LINSERT、LLEN 对列表键执行

    • SADD、SPOP、SINTER、SCARD 对集合键执行

    • ZADD、ZCARD、ZRANK、ZSCORRE 对有序集合使用

8.7.1 类型检查的实现

在执行类型特定的命令之前,Redis 会检查输入键的类型是否正确,在决定是否执行给定命令

类型检查通过 redisObject 的 type 属性实现

  • 执行之前,先检查输入数据的键的值对象是否为执行命令所需要的类型,是则对键执行命令
  • 否则拒绝执行,返回错误

LLEN 命令例子:

LLEN命令类型检查

8.7.2 多态命令的实现

Redis 会根据值对象的编码方式,选择正确的命令执行

如 LLEN 命令

  • 如果列表对象的编码是 ziplist,则底层实现为 压缩列表,使用 ziplistLen 函数
  • 如果是 linkedlist,则底层为双端链表,使用 listLen 函数

LLEN 命令是多态,其他特定类型的命令也是多态的,类型的多态

实际上对所有对象都能执行的 DEL 等命令也称为多态命令,对于编码的多态

LLEN命令执行过程

8.8 内存回收

Redis 在自己的对象系统中构建了一个引用计数器技术实现的内存回收机制,通过跟踪对象的引用计数信息,在适当的时候释放对象并进行内存回收

每个对象的引用计数信息由 redisObject 的 refcount 属性记录

refcount

对象的引用计数信息会随着对象的使用状态不断变化:

  • 创建的新对象的引用计数的值被初始化为 1
  • 当对象被新程序引用时,引用计数值加 1
  • 当对象不被新程序引用时,引用计数值减 1
  • 当对象的引用计数值为 0 时,对象占用的内存被释放

修改引用计数的API

对象的生命周期为创建对象、操作对象、释放对象三个阶段

生命周期

8.9 对象共享

对象的引用计数属性还有对象共享的作用

假如键 A 创建了整数值为 100 的字符串对象作为值对象,此时键 B 也要创建整数值为 100 的字符串对象为值对象,此时便可以使用对象共享

  • 将键的值指针指向一个现有的值对象
  • 将被共享的值对象的引用计数增 1

共享与否

Redis 会在初始化服务器时,创建 10000 个字符串对象,即 [0, 9999],当需要使用到值为 [0, 9999] 字符串对象时,则会使用这些共享对象

共享字符串对象的数量通过 redis.h/REDIS_SHARED_INTEGERS 常量来修改

127.0.0.1:6379> set A 100
OK
127.0.0.1:6379> object refcount A
(integer) 10
127.0.0.1:6379> set B 100
OK
127.0.0.1:6379> object refcount A
(integer) 11
127.0.0.1:6379> object refcount B
(integer) 11
127.0.0.1:6379>

共享对象

共享对象不只有字符串键可以使用,在数据结构中嵌套了字符串对象的对象(Linkedlist 编码的列表对象、hashtable 编码的哈希对象、集合对象,zset 编码的有序集合对象)都可以使用共享对象

考虑一个共享对象为键的值对象时,需要先检查共享对象和键想创建的目标对象是否完全相同,完全相同情况下才会使用共享对象,而共享对象保存的值越复杂,验证时间则越长

  • 整数值字符串对象,O(1)
  • 字符串值字符串对象,O(n)
  • 对象的对象,O(n^2)

因此只使用整数值的字符串对象

8.10 对象的空转时长

redisObject 结构中 lru 属性为记录对象最后一次被命令访问的时间:

lru

OBJECT IDLETIME 命令可以打印键的空转时长,等于当前时间减去键的值对象的 lru 时间

127.0.0.1:6379> set uuu "aaa"
OK
127.0.0.1:6379> object idletime uuu
(integer) 15
127.0.0.1:6379> object idletime uuu
(integer) 23
127.0.0.1:6379> object idletime uuu
(integer) 25
127.0.0.1:6379> get uuu
"aaa"
127.0.0.1:6379> get uuu
"aaa"
127.0.0.1:6379> object idletime uuu
(integer) 2
127.0.0.1:6379>

OBJECT IDLETIME 命令实现是特殊的,访问键的值对象时不会修改 lru

键的空转时长的另外作用:

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

posted @ 2021-06-20 20:57  zephxu  阅读(74)  评论(0)    收藏  举报