Redis对象(三) - 其它特性

类型检查和多态命令的实现

redis中用于键操作的命令基本上可以分为两类:

  1. 可以对任何类型的键执行, eg. del, expire, rename, type, object

  2. 只能对特定命令执行的键,

    eg. setgetappendstrlen 等命令只能对字符串键执行

    hdelhsethgethlen 等命令只能对hash键执行

    rpushlpoplinsertllen等只能对列表键执行

    saddspopsinterscard等命令只能对集合键执行

    zaddzcardzrankzcore 等命令只能对有序集合键执行

类型检查的实现

类型特定命令所进行的类型检查是通过redisObject结构的type属性 来实现的.

  1. 在执行一个类型特定命令之前, 服务器先检查输入数据库键的值对象是否为执行命令所需要的类型, 是、就执行
  2. 否则, server拒绝执行、并向client返回一个类型错误

eg. 对于llen命令:

在执行llen命令前、server会先检查输入数据库键的值对象是否为列表类型, 即: 检查redisObjecttype属性是否为redis_list, 是的话、执行 llen命令, 否则返回类型错误

多态命令的实现

Redis除了会根据值对象的类型来判断是否能执行特定命令外、还会根据值对象的编码方式、选择正确的命令实现代码来执行命令

eg. 对一个键执行 llen命令, 则服务器除了要确保执行命令的是列表键之外, 还要根据键的值对象所使用的编码来选择正确的llen命令实现

  1. 若列表对象的编码为 ziplist, 那么说明列表对象的实现为压缩列表, 程序将使用 ziplistLen 函数来返回列表的长度
  2. 若列表对象的编码为 linkedlist, 说明列表对象的实现为双端链表, 程序将使用 listLength 函数来返回列表的长度

借用面向对象的术语来说、可以认为llen命令的实现是多态的, 只要执行 llen 命令的是列表键、无论值对象是 ziplist 还是 linkedlist 编码、命令都可以正常执行

delexpire等命令和llen命令的区别在于、前者是基于类型的多态, 一个命令可以同时处理多种不同类型的键、而后者是基于编码的多态: 一个命令可以同时用于处理多种不同的编码

内存回收

因为C语言并不具备内存回收功能, redis 在自己的对象系统中构建了一个引用计数(reference counting) 技术来实现内存回收机制, 通过引用计数机制、程序可以通过跟踪对象的引用计数信息、在适当的时候自动释放对象并进行内存回收

每个对象的引用计数信息由 RedisObject 结构的 refcount属性记录:

typedef struct redisObject {
  // ...
  int refcount; // 引用计数
  // ...
} robj;

对象的引用技术信息会随着对象的使用状态不断变化

  1. 创建一个新的对象时、引用计数初始化为1
  2. 对象被一个新的程序引用时、引用计数值 +1
  3. 对象不再被一个程序引用时、引用计数值 -1
  4. 对象的引用计数值变为0时、对象所占用的内存会被释放

下边是修改对象引用计数的API

函数 作用
incrRefCount 将对象的引用计数值+1
decrRefCount 将对象的引用计数值-1, 当对象的引用计数值=0时、释放对象
resetRefCount 将对象的引用计数值设为0, 但不释放对象、需要重设对象引用值是使用

其它不同类型的对象也会经历类似的过程

共享对象

除了实现引用计数内存回收机制外、对象的引用计数属性还带有对象共享的作用.

eg. A键创建了一个包含整数值100的字符串对象作为值对象, 此时若B键也想要创建一个同样保存了整数值100的字符串对象作为值对象、那么Server有两种做法:

  1. 为键B创建一个包含整数值100的字符串对象
  2. 让键A和键B共享同一个字符串对象

明显, 第二种方式更节约内存, 在Redis中、多个键共享同一个值需要执行以下步骤:

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

**注意: **

创建共享字符串对象的数量可以通过修改 redis.h/redis_shared_integers 常量来修改

eg, 创建一个值为100的键a, 使用object refcount 命令查看a的引用计数, 会发现值为2

redis> set a 100
OK
redis> object refcount a
(integer) 2

引用这个值对象的两个程序分表是持有这个值对象的服务器程序, 及共享这个值对象的键A

另外: 这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的hash对象、hashtable编码的集合对象及zset编码的有序集合对象)等都可以使用这些共享对象

思考

为什么redis不共享包含字符串的对象?

当服务器考虑将一个共享对象设置为键的值对象时、程序需要检查给定的共享对象和键想创建的目标对象是否完全相同, 只有在共享对象和目标对象完全相同的情况下、程辉才会将共享对象的用作键的值对象、而一个共享对象保存的值越复杂、验证两者相同的复杂度就会越高, 消耗的CPU时间也会越多

  • 若共享对象保存整数值的字符串对象、那么验证操作的复杂度为 O(1)
  • 若共享对象是保存字符串值的字符串对象、那么验证操作的复杂度为 O(N)
  • 若共享对象是包含了多个值(或者对象)的对象, 比如列表对象或者hash对象、验证的复杂度将是O(N²)

因此、尽管共享更复杂的对象可以节约更多内存、但受到CPU时间的限制、redis只对包含整数值的字符串对象进行共享

对象的空转时长

除了前边介绍过的typeencodingptrrefcount 4个属性外, redisObject结构包含的最后一个属性为 lru属性, 它记录了对象最后一次被命令访问的时间

typedef struct redisObject {
  // ...
  unsigned lru:22;
  // ...
} robj;

object idletime 命令可以打印出给定键的空转时长, 就是通过当前时间 - 键的lru时间得到的

注意:

Object idletime的实现是特殊的, 它在访问键的时候、不会修改值对象的lru属性

除了使用 命令打印键的空转时长, lru属性还用于回收内存, 当设置了 maxmemory 选项, 且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru 时、当服务器占用内存超过了 maxmemory设置的上限值时, 空转时长较高的键会优先被服务器释放.

重点回顾

  1. redis数据库的中每个键值对的键和值都是一个对象
  2. redis共有字符串、列表、hash、结合、有序集合五种类型的对象, 每种类型的对象至少有2种或以上的编码方式, 不同的编码可以在不同的场景上优化对象的使用概率
  3. 服务器在执行某些命令之前、会先检查给定键的类型能否执行
  4. redis的对象系统带有引用计数实现的内存回收机制, 当一个对象不再被使用时、该对象占用的内存会被自动释放
  5. redis会共享值为 0 到 9999 的字符串对象
  6. 对象会记录自己最后一次被访问的时间, 这个时间还可以用于计算对象的空转时长

你可能感兴趣的:(Redis对象(三) - 其它特性)