2018-1-4 by Atlas

* 简述

  • redis并没有直接使用SDS、链表、字典、压缩列表、整数集合、跳跃表这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象这五种类型的对象,每种对象都至少用到了一种上面提到的数据结构。
  • 使用对象的一个好处是,可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
  • 对象系统还实现了基于引用计数技术的内存回收机制,释放不再使用的对象所占用的内存。
  • 引用计数技术实现了对象共享机制,通过多个数据库键共享同一个对象来节约内存。

* 定义

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

* 类型

对象的类型

TYPE命令,返回的结果为数据库键对应的值对象的类型,而不是键对象的类型。

redis> SET msg "hello"
ok
redis> TYPE msg
string

不同类型值对象的TYPE命令输出

* 编码

对象的编码

OBJECT ENCODING命令查看数据库键的值对象的编码。

redis> SET msg "hello"
ok
redis> OBJECT ENCODING msg
"embstr"

不同类型和编码的对象

OBJECT ENCODING对不同编码的输出

* 类型检查

类型检查是通过redisObject结构的type属性来实现的: 1)执行命令前,服务器先检查输入数据库键的值对象是否为执行命令所需的类型,如果是,服务器就对键执行命令; 2)否则,服务器拒绝执行命令,并向客户端返回一个类型错误。

LLEN命令执行时的类型检查过程 LLEN命令执行时的类型检查过程

* 多态实现

  • 基于编码的多态--一个命令可以同时用于处理多种不同的编码。
  • 基于类型的多态--一个命令可以同时用于处理多种不同类型的键。

LLEN命令执行过程 LLEN命令执行过程

* 空转时长

lru属性记录了对象最后一次被命令程序访问的时间。 OBJECT IDLETIME 命令可以打印给定键的空转时长,等于当前时间减去键的值对象的lru时间。 如果服务器打开了maxmemory选项,并且内存回收算法为volatile-lru或者allkeys-lru,那么服务器占用的内存数超过maxmemory选项设置的上限空转时长较高的部分键会优先被释放,回收内存。

* 内存回收

对象系统构建了一个引用计数技术实现的内存回收机制。 对象的引用计数信息会随着对象的使用状态而不断变化:

  • 创建一个新对象时,引用计数值初始化为1;
  • 当对象被一个新程序使用时,引用计数值增1;
  • 当对象不再被一个程序使用时,引用计数值减1;
  • 当对象的引用计数值变为0,对象所占的内存将会被释放。

* 对象共享

多个键共享同一个值对象需要执行以下步骤: 1)将数据库键的值指针指向一个现有的值对象; 2)将被共享的值对象的引用技术增一。

共享对象

* 字符串对象

编码可以是int、raw或者embstr。

  • 结构

如果字符串对象保存的是整数值,int编码实现。

int编码的字符串对象

如果字符串对象保存的是字符串值,并且长度小于等于39字节,embstr编码实现。

embstr编码的字符串对象

如果字符串对象保存的是字符串值,并且长度大于39字节,raw编码实现。

raw编码的字符串对象

  • 策略

int、embstr、raw三种不同编码实现字符串对象,就是根据值对象类型优化内存分配。 embstr编码的字符串对象较raw编码的字符串对象将内存分配次数从两次降低为一次。 embstr编码的字符串对象较raw编码的字符串对象内存释放也从两次减少为一次。 embstr编码的字符串对象数据保存在一块连续的内存能够更好地利用缓存的优势。 但是embstr编码的字符串对象实际上是只读的,int编码的字符串对象的值变更成字符串值,对象将转换成raw编码的字符串对象。

  • 命令

SET:保存值。 GET:取值。 APPEND:追加字符串。 STRLEN:取字符串长度。

* 列表对象

编码可以是ziplist或者linkedlist。

  • 结构

ziplist编码列表对象。

ziplist编码列表对象

linkedlist编码列表对象。

linkedlist编码列表对象

  • 策略

同时满足以下两个条件时,列表对象使用ziplist编码: 1)列表对象保存的所有字符串元素的长度都小于64字节; 2)列表对象保存的元素数量小于512个。 任意一个条件不满足,列表对象使用linkedlist编码。 配置:list-max-ziplist-value选项和list-max-ziplist-entries选项。

  • 命令

RPUSH:添加列表表头元素。 LPOP:弹出列表表头元素。 LINSERT:插入元素。 LLEN:取列表长度。

* 哈希对象

编码可以是ziplist或者hashtable。

  • 结构

ziplist编码哈希对象

ziplist编码哈希对象

hashtable编码哈希对象

hashtable编码哈希对象

  • 策略

同时满足以下两个条件,哈希对象使用ziplist编码: 1)哈希对象保存的所有键值对对象的键和值的字符串长度都小于64字节; 2)哈希对象保存的键值对数量小于512个。 任意一个条件不满足,哈希对象使用hashtable编码。 配置:hash-max-ziplist-value选项和hash-max-ziplist-entries选项。

  • 命令

HSET:添加元素。 HGET:获取元素。 HDEL:删除元素。 HLEN:取哈希元素数量

* 集合对象

编码可以是intset或者hashtable。

  • 结构

intset编码集合对象。

intset编码集合对象

hashtable编码集合对象。

hashtable编码集合对象

  • 策略

同时满足以下两个条件,集合对象使用intset编码: 1)集合对象保存的所有元素都是整数值; 2)集合对象保存的元素数量不超过512个。 任意一个条件不满足,集合对象使用hashtable编码。

  • 命令

SADD:添加元素。 SPOP:弹出元素。 SCARD:取集合元素数量。

* 有序集合对象

编码可以是ziplist或者skiplist。

  • 结构

ziplist编码有序集合对象。

ziplist编码有序集合对象

skiplist编码有序集合对象。

skiplist编码有序集合对象

  • 策略

同时满足以下两个条件,有序集合对象使用ziplist编码: 1)有序集合对象保存的所有成员的长度都小于64字节; 2)有序集合对象保存的元素数量小于128个。 任意一个条件不满足,序集合对象使用skiplist编码。

  • 命令

ZADD:添加元素。 ZCARD:取有序集合对象元素数量。 ZRANK:从表头向表尾遍历有序集合。 ZSCORE:取给定成员分值。

参考文献:《redis设计与实现》