Redis对象
概述
- 对象包含: 字符串对象, 列表对象, 哈希对象, 集合对象, 有序集合对象
- Redis 用基于数据结构实现的对象系统来实现数据库, 而没有直接用数据结构
- 对于同一种对象, 可以在不同的使用场景下使用不同的数据结构来实现, 从而优化使用效率
- Redis通过引用计数实现内存回收机制和对象共享技术
- Redis通过对象的访问时间属性计算空转时间, 当内存不够时根据空转时间对内存进行回收。
- 在Redis数据库中每当我们创建一个键值对时, 都会创建至少创建两个对象,一个对象用作键, 一个对象用作值。
- 键总是字符串对象, 而值可以是五大对象中的一种, 因此称一个键为"XX键"时, 指这个数据库键的值是XX对象, 例如哈希键, 字符串键等等.
对象数据结构
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
type 类型
type属性记录了对象的类型
Redis 5大对象
类型常量 | 对象的名称 |
---|---|
REDIS_STRING | |
REDIS_LIST | 列表对象 |
REDIS_SET | 集合对象 |
REDIS_ZSET | 有序集合对象 |
REDIS_HASH | 哈希对象 |
redis.h 头文件中定义的对象常量的值
/* Object types */
// 对象类型
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
调用redis中的 TYPE 命令可以返回对象类型
encoding 编码
encoding属性记录了对象的底层实现数据结构
redis.h 头文件中定义可能的值
#define REDIS_ENCODING_RAW 0 /* 简单动态字符串 */
#define REDIS_ENCODING_INT 1 /* long类型的整数 */
#define REDIS_ENCODING_HT 2 /* 字典 */
#define REDIS_ENCODING_ZIPMAP 3 /* 压缩字典 */
#define REDIS_ENCODING_LINKEDLIST 4 /* 双向链表 */
#define REDIS_ENCODING_ZIPLIST 5 /* 压缩列表 */
#define REDIS_ENCODING_INTSET 6 /* 整数集合 */
#define REDIS_ENCODING_SKIPLIST 7 /* 跳跃列表 */
#define REDIS_ENCODING_EMBSTR 8 /* embstr */
使用 OBJECT ENCODING命令可以查看一个数据库键的值的对象编码
根据不同的使用场景, 为不同的对象设置不同的编码(数据结构), 可以优化对象的效率
字符串对象
字符串对象的编码可以是: int , raw(SDS简单动态字符串) 和 embstr。
编码转换
使用int编码:
- 字符串保存的是整数值
- 可以用long类型表示
直接使用整数值保存在字符串对象结构的ptr属性里(将void* 转换成 long类型)
并且将字符串编码设置为REDIS_ENCODING_INT
使用raw编码:
- 保存的是字符串
- 字符串的长度大于39字节
使用embstr编码:
- 保存的是字符串
- 字符串值的长度小于39字节
- embstr 一旦被执行任何修改命令, 会转换为 raw, 因为 Redis没有问 embstr 提供任何的修改程序。
embstr和sds的区别
embstr将redisObject对象结构和sdshdr结构通存放在连续的内存空间中, 只需要进行一次内存分配
优点
- 内存分配和释放都只需要调用一次相关函数
- 连续的内存存放对象和编码更好地利用缓存带来的优势
列表对象
列表对象的编码可以是ziplist和linkedlist
使用ziplist编码的列表对象
使用linkedlist编码的列表对象
注: linkedlist里存储的是字符串对象, 字符串对象是Redis五种对象中唯一一种会被其他四种对象嵌套的对象
编码转换
使用ziplist :
- 列表对象保存的所有字符串元素的长度都小于64字节
- 列表对象保存的元素数量小于512个
不能满足上述条件的列表对象使用linkedlist编码
哈希对象
哈希对象的编码可以是 ziplist 和 hashtable
ziplist编码的哈希对象使用压缩列表作为底层
- ziplist的哈希表中, 同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后.
- 先添加到哈希对象中的兼职对会放在压缩列表的表头方向
hashtable编码的哈希对象使用字典作为底层实现,
- 字典的键都是一个字符串对象, 保存了键值对的键
- 字段的值都是一个字符串对象, 保存了键值对的值
编码转换
哈希对象同时满足以下两个条件时, 哈希对象使用ziplist编码
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象保存的键值对数量小于512个
不满足上述条件的哈希对象使用hashtable编码
集合对象
集合对象编码可以是intset或者hashtable
intset使用整数集合作为底层实现
hsahtable底层使用字段实现, 每一个键都是字符串对象, 保存着集合的元素, 字典每个键的值都为NULL
编码转换
使用intset编码
- 所有元素都是整数值
- 元素数量不超过512个
不满足上述2个条件的集合对象需使用hashtable编码
有序集合对象
有序集合对象编码可以是ziplist或者skiplist
有序集合的每个元素成员都是一个字符串对象, 每个元素的分值都是一个double类型.
ziplist编码使用压缩列表, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员, 第二个节点保存元素的分值, 压缩列表集合内的元素按分值从小到大排序
如果对象使用skiplist编码, 则会使用zset结构作为底层实现
typedef struct zset {
// 字典,键为成员,值为分值
// 用于支持 O(1) 复杂度的按成员取分值操作
dict *dict;
// 跳跃表,按分值排序成员
// 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
// 以及范围操作
zskiplist *zsl;
} zset;
使用zskiplist跳跃表对集合进行范围型操作
使用dict字典堆可以时间复杂度O(1)对成员的分值进行查找操作
二者会通过共享指针来共享相同元素的成员和分值, 因此并不会产生任何重复的成员或分值
编码转换
使用ziplist
- 有序集合 保存的元素数量小于128个
- 有序集合所有成员的长度小于64字节
不满足上述条件的有序集合对象使用skiplist编码
类型检查与命令多态
Redis中的命令基本可以分为两类:
- 可以对任何类型执行的键执行, DEL命令, EXPIRE命令, RENAME命令, TYPE命令等,
- 只能对特定类型执行的命令, 如SET, GET, APPEND等只能对字符串执行
类型检查:
在执行一个命令之前, Redis 会通过检查 redisObject 结构的 type 属性对象是否符合
如果对执行的命令与键不符, Redis会返回一个类型错误
多态命令:
对与同一命令, Redis会根据同一对象的不同的编码来选择正确的命令实现, 借用面向对象的术语来说, 可以认为该命令是多态的
例如对同一LLEN命令
- 如果对象的编码为ziplis, 那么说明对象的实现为压缩列表, 程序将用ziplistLen函数来返回列表的长度
- 如果独享的编码为linkedlist, 那么程序将使用listLength函数来返回双向链表的长度
内存回收
C语言不具备内存回收功能, 因此Redis使用引用计数法来实现内存回收机制
每个对象的引用计数信息由 redisObject 结构的 refcount 属性记录
- 创建一个新对象时, refcount = 1
- 当一个对象被新程序使用时, refcount ++;
- 当一个对象不再被程序使用时, refcount --;
- 当对象的引用计数器值变为 0 时, 对象所占的内存会被回收
对象共享
Redis 利用引用计数属性实现对象共享
如果键A创建了一个保存100整数的字符串对象, 那么当键B也需要使用一个100整数的字符串对象时, 显然, 键A和键B共享一个对象比较节约内存
实现对象共享步骤
- 将数据库键的值指向一个现有的值对象
- 让该对象的引用计数增一
共享对象时, 要首判断给定的共享对象和目标对象是否相等
- 如果共享对象是保存整数的字符串, 那么该验证操作的时间复杂度为O(1)
- 如果共享对象 是保存字符串值的字符串, 那么该验证操作的时间复杂度为O(N)
- 如果共享对象 是保存了多个值或对象的对象, 那么该验证操作的时间复杂度为O(N^2)
因此为了避免验证对于CPU的消耗
redis仅对包含整数值的字符串对象进行共享
对象的空转时长
redisObject中的 lru属性记录了对象最后一次被命令程序访问的时间
OBJECT IDLETIME命令可以打印出给定键的空转时长, 该命令是通过lru时间计算出的
redis> SET msg "hello world"
redis> OBJECT IDLETIME msg
redis> 20
# 等一会
redis> OBJECT IDLETIME msg
redis> 350
# 访问一次msg
redis> GET msg
redis> "hello world"
redis> OBJECT IDLETIME msg
redis> 0
操作AIP
参考资料: 《Redis设计与实现》