数据结构
与对象
概述本篇文章通过剖析Redis五种不同类型的对象所使用的底层数据结构,让我们更好的了解这些数据结构是如何深刻地影响对象的功能和性能的。
Redis 数据库里面的每个键值对(key-value) 都是由对象(object)组成的:
数据库的键总是一个字符串对象(sds);
数据库的值则可以是字符串对象(string)、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象这五种对象中的其中一种。
Redis用到的所有主要数据结构包括:简单动态字符串、双端链表、字典、跳跃表、整数集合、压缩列表。
Redis的对象系统包括:字符串对象、列表对象、哈希对象、集合对象、有序集合对象。
首先弄清楚Redis数据结构与对象之间的关系:对象是对底层数据结构的二次封装或者抽象,通过编码(每种类型的对象都至少使用了两种不同的编码),使得每种类型的对象都用到了至少一种数据结构。
Redis用到的所有主要数据结构包括:简单动态字符串、双端链表、字典、跳跃表、整数集合、压缩列表。
Redis 没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string SDS)的抽象类型,并将SDS用作Redis 的默认字符串表示。除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF模块中的AOF缓冲区。
typedef struct sdshdr {
int len; // 记录已经使用的空间长度
int free; // 记录未使用字节的数量
char buf[]; // 字节数组,用于保存字符串
} listNode;
1、获取一个SDS长度的复杂度为O(1);
C 语言里获取一个长度为字符串的长度,必须遍历整个字符串。
SDS 的数据结构中可以通过获取len 属性的值,直接知道字符串长度。
2、杜绝缓冲区溢出;
SDS空间分配策略完全杜绝了发生缓冲区溢出的可能性:
当我们需要对一个SDS 进行修改的时候,redis 会在执行拼接操作之前,预先检查给定SDS 空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作。
3、减少修改字符串带来的内存重分配次数;
SDS采用了空间预分配策略与惰性空间释放策略来避免内存分配问题。
空间预分配策略是指,每次 SDS 进行空间扩展时,程序不但为其分配所需的空间,还会为其分配额外的未使用空间,以减少内存再分配次数。额外分配的未使用空间大小取决于空间扩展后SDS 的 len 属性值。
惰性空间释放策略是指,SDS 字符串长度如果缩短,那么多出的未使用空间将暂时不释放,而是增加到 free 中。以使后期扩展 SDS 时减少内存 再分配次数。如果要释放 SDS 的未使用空间,则可通过 sdsRemoveFreeSpace()
函数来释放。
4、二进制安全;
不像C语言, 除了能保存文本数据,也能保存像图片,音频,视频,压缩文件这样的二进制数据。
5、兼容部分C函数。
可以使用一部分
C字符串 | SDS |
---|---|
获取字符串长度的复杂度为O(N) | 获取字符串长度的复杂度为O(1) |
API 是不安全的,可能会造成缓冲区溢出 | API 是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存重分配 | 修改字符串长度N次最多执行N次内存重分配 |
只能保存文本数据 | 可以保存二进制数据和文本文数据 |
可以使用所有 |
可以使用一部分 |
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
// 链表节点
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
// 链表【通过链表持有链表节点,操作起来更加方便】
typedef struct list {
listNode *head; //表头节点
listNode *tail; //表尾节点
void *(*dup)(void *ptr); //节点值复制函数
void (*free)(void *ptr); //节点值释放函数
int (*match)(void *ptr, void *key); //节点值对比函数
unsigned long len; //链表长度
} list;
// 由链表结构体可以看出链表特性: 双端(有前后指针)、无环、表头和表尾、长度计数器、多态(通过dup、free、match三个属性设置类型特定函数)。
字典是一种用于保存键值对的抽象数据结构。
在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。在C语言中,并没有这种数据结构,但是Redis 中构建了自己的字典实现。
// 字典
struct dict {
dictType *type; // 类型指定的哈希函数,用于计算哈希值
dictht *ht[2]; // 哈希表
long rehashidx; // rehash索引,用于判定当前是否在进行rehash
void *metadata[];
};
// 哈希表
typedef struct dictht {
dictEntry **table; //哈希表数组
unsigned long size; //哈希表大小
unsigned long sizemask; //哈希表大小掩码,用于计算索引值
unsigned long used; //该哈希表已有节点的数量
}
// 哈希表节点
struct dictEntry {
void *key; // 键
union { // 值
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 链地址法解决hash冲突
void *metadata[];
};
当要将一个新的键值对添加到字典里时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
第一步:先计算出哈希值;
hash=dict→type→hashFunction(key);
第二步:根据计算出来的哈希值 和 哈希表结构的sizemask属性,计算出索引值。
index=hash & dict→ht[x].sizemask;
如何计算哈希值?
当字典被用作数据库的底层实现,或者哈希建的底层实现时,redis使用MurmurHash2算法
来计算键的哈希值【MurmurHash2算法不仅计算速度快,而且即使键是有规律的,计算出来的哈希值也是随机的】
随着对哈希表的不断操作,哈希表保存的键值用作对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。这个rehash 操作并不是一次性、集中式完成的,而是分多次、渐进式地完成的。
哈希表空间分配规则:
如果执行的是拓展操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂。
如果执行的是收缩操作,那么ht[1] 的大小为第一个大于等于ht[0] 的2的n次幂。
渐进式rehash的详细步骤:
1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始。
3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一。【rehash过程中,新增节点操作始终在ht[1]中进行】
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束。
采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash带来的庞大计算量。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表是一种随机化的数据,跳跃表以有序的方式在层次化的链表中保存元素,效率可以和平衡树媲美【查找、删除、添加等操作都可以在对数期望时间下完成,并且比起平衡树来说,跳跃表的实现要简单直观得多】
Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构【slots-to-keys】。
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele; //成员对象
double score; //分值
struct zskiplistNode *backward; //后退指针
struct zskiplistLevel { //层
struct zskiplistNode *forward; //前进指针
unsigned long span; //跨度,用于记录两个节点之间的距离
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //表头节点和表尾节点
unsigned long length; //表中节点数量
int level; //表中层数最大的节点的层数
} zskiplist;
跳跃表在每次创建一个新跳跃表节点的时候,程序都根据幂次定律
(power law:越大的数出现的概率越小)随机生成一个介于1到32之间的值作为level数组的大小,这个大小就是层的高度。
跨度,则是用于记录两个节点之间的距离。
跳跃表是有序集合的底层实现之一。
主要有zskiplist 和zskiplistNode两个结构组成。
每个跳跃表节点的层高都是1至32之间的随机数。
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的对象必须是唯一的。
节点按照分值的大小从大到小排序,如果分值相同,则按成员对象大小排序。
整数集合是集合键Set的底层实现之一,当一个集合中只包含整数,且这个集合中的元素数量不多时,redis就会使用整数集合intset作为集合的底层实现。
typedef struct intset {
uint32_t encoding; // 编码方式——int16/int32/int64等
uint32_t length; // 长度
int8_t contents[]; // 用于保存元素的数组
} intset;
要将一个新元素添加到整数集合里,并且新元素的类型比整数集合 现有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里。
升级步骤:1、分配空间;2、转换类型,排序不变;3、添加新元素。
没有降级操作:一旦整数集合进行了升级,编码类型就会保持升级后的状态。
压缩列表是列表键和哈希键的底层实现之一。
压缩列表ziplist结构本身就是一个连续的内存块:由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串。
压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失。
zlbytes:用于记录整个压缩列表占用的内存字节数。
zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节。用于快速定位尾节点。
zllen:记录了压缩列表包含的节点数量。
entryX:压缩列表包含的各个节点。
zlend:用于标记压缩列表的末端。
value:也叫content,可以用来保存一个字节数组或者整数。
压缩列表是一种为了节约内存而开发的顺序型数据结构。
压缩列表被用作列表键和哈希键的底层实现之一。
压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
添加新节点到压缩列表,可能会引发连锁更新操作。连锁更新操作出现的几率不高。
previous_entry_length用于记录压缩列表前一个节点的长度。
如果前一节点长度<254字节,那么previous_entry_length属性的长度为1字节;如果前一节点长度>=254字节,那么previous_entry_length属性的长度为5字节。
previous_entry_length的作用:从表尾向表头遍历[回溯]。通过指针计算,根据当前节点的起始地址来计算出前一个节点的起始地址。
Redis在某些特殊情况下会产生连续多次的空间扩展操作,这称之为连锁更新。
比如:大多数entry节点的长度都是250~253字节的,如果新增了一个长度超过254字节的节点,则新增节点的后一个节点previous_entry_length要扩展为5字节的,这会导致连锁更新。同理删除也可能会触发连锁更新。
连锁更新的时间复杂度为O(N^2),这会比较占用CPU性能。然而我们不需要过分担心,因为连锁更新真正造成性能问题的几率非常小:连锁更新的前提条件是——恰好有多个连续的介于250~253字节之间的节点。
quicklist
结构是在redis 3.2
版本中新加的数据结构,用在列表的底层实现。
quicklist
与ziplist
的关系:quicklist
是由ziplist
组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据。
// 快表
typedef struct quicklist {
quicklistNode *head; //指向头部(最左边)quicklist节点的指针
quicklistNode *tail; //指向尾部(最右边)quicklist节点的指针
unsigned long count; //ziplist中的entry节点计数器
unsigned long len; //quicklistNode节点计数器
// ... // 一些压缩参数
} quicklist;
// 快表节点
typedef struct quicklistNode {
struct quicklistNode *prev; //前驱节点指针
struct quicklistNode *next; //后驱节点指针
unsigned char *entry; //压缩列表ziplist的总长度
size_t sz; /* entry size in bytes */
// ... // 一些压缩参数
} quicklistNode;
// 压缩过的ziplist结构
typedef struct quicklistLZF {
size_t sz; /* LZF size in bytes*/ //表示被LZF算法压缩后的ziplist的大小
char compressed[]; //一个柔性数组,保存压缩后的ziplist的数组
} quicklistLZF;
// 管理quicklist中quicklistNode节点中ziplist信息的结构
typedef struct quicklistEntry {
const quicklist *quicklist; //指向所属的quicklist的指针
quicklistNode *node; //指向所属的quicklistNode节点的指针
unsigned char *zi; //指向当前ziplist结构的指针
unsigned char *value; //指向当前ziplist结构的字符串vlaue成员
long long longval; //指向当前ziplist结构的整数
size_t sz; //保存当前ziplist结构的字节数大小
int offset; //保存相对ziplist的偏移量
} quicklistEntry;
在前面的文章中,我们介绍了 Redis 用到的主要数据结构,比如简单动态字符串、双端链表、字典、压缩列表、整数集合。
然而 Redis 并没有直接使用这些数据结构来实现键值对的数据库,而是在这些数据结构之上又包装了一层 RedisObject(对象),RedisObject 有五种对象:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。
redisObject
结构体定义如下:
struct redisObject {
unsigned type:4; // 对象类型(5种)
unsigned encoding:4; // 编码(8种),记录对象所使用的编码,也就是底层的数据结构
unsigned lru:LRU_BITS; // 访问时间(用于记录空转时长、内存回收)
int refcount; // 引用计数(用于对象共享、垃圾回收)
void *ptr; // 指向底层实现结构的指针,由encoding决定
};
从结构体定义就可以归纳出使用redisObject
的优点:
代码示例:
# 字符串对象
127.0.0.1:6379> set msg "hello world!"
OK
127.0.0.1:6379> get msg
"hello world!"
127.0.0.1:6379> type msg
string
# 列表对象
127.0.0.1:6379> rpush numbers 1 3 5
(integer) 3
127.0.0.1:6379> type numbers
list
# 哈希对象
127.0.0.1:6379> hset profile name tom age 28 career programmer
(integer) 3
127.0.0.1:6379> type profile
hash
# 集合对象
127.0.0.1:6379> sadd fruits apple banana cherry
(integer) 3
127.0.0.1:6379> type fruits
set
# 有序集合对象
127.0.0.1:6379> zadd price 8.5 apple 5.0 banana 6.8 cherry
(integer) 3
127.0.0.1:6379> type price
zset
对象与数据结构的关系: 每种类型的对象都至少使用了两种不同的编码,也即每种类型的对象都用到了至少一种数据结构。如下表:
type类型 | encoding编码 | 对象Object |
---|---|---|
redis_string | redis_encoding_int | 使用整数值实现的字符串对象 |
redis_string | redis_encoding_embstr | 使用embstr编码的sds实现的字符串对象 |
redis_string | redis_encoding_raw | 使用sds实现的字符串对象 |
redis_list | redis_encoding_ziplist | 使用压缩列表实现的列表对象 |
redis_list | redis_encoding_linkedlist | 使用双端链表实现的列表对象 |
redis_hash | redis_encoding_ziplist | 使用字典实现的哈希对象 |
redis_hash | redis_encoding_ht | 使用整数值实现的哈希对象 |
redis_set | redis_encoding_intset | 使用整数集合实现的集合对象 |
redis_set | redis_encoding_ht | 使用字典实现的集合对象 |
redis_zset | redis_encoding_ziplist | 使用压缩列表实现的有序集合对象 |
redis_zset | redis_encoding_skiplist | 使用跳跃表和字典实现的有序集合对象 |
如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。
如果字符串对象保存的是一个字符串值,并且这个字符串的长度 > 39 字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为 raw。
如果字符串对象保存的是一个字符串值,并且这个字符串的长度<= 39 字节,那么字符串对象将使用 embstr 编码的方式来保存这个字符串。
使用 embstr 的编码方式有一些优点,如下:
bint转raw
127.0.0.1:6379> set num 10086
OK
127.0.0.1:6379> object encoding num
"int"
127.0.0.1:6379> append num " is a good number!"
(integer) 23
127.0.0.1:6379> get num
"10086 is a good number!"
127.0.0.1:6379> object encoding num
"raw"
embstr转raw
127.0.0.1:6379> set msg "hello world!"
OK
127.0.0.1:6379> object encoding msg
"embstr"
127.0.0.1:6379> append msg "Im redis!"
(integer) 22
127.0.0.1:6379> get msg
" hello world!Im redis!"
127.0.0.1:6379> object encoding msg
"raw"
注:
- 用 long 类型保存的整数在redis中是字符串对象,编码类型是int;用 long double类型的浮点数在redis中也是字符串对象,编码类型是embstr或者raw;在对它们进行运算时,会将它们转化为对应的类型再运算,保存时再转化为字符串对象。
- embstr编码的字符串对象是只读的,所以对其进行修改,总是会转化为raw编码。
- 另外,字符串对象stringObject是redis五种类型的对象中唯一一种会被其它四种对象嵌套的对象。
字符串在redis有如下命令:
set、get;append;incrbyfloat、incrby、decrby;strlen;setrange、getrange等
ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在列表对象的数据量小的时候使用压缩列表存储。
当列表的长度小于 512,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 linkedlist 存储。
使用ziplist编码必须满足两个条件:
条件1:列表对象保存所有的字符串元素的长度都小于64字节。
条件2:列表对象保存的元素数量小于512个。
ziplist转linkedlist:破坏条件1
127.0.0.1:6379> rpush blabla "hello" "world"
(integer) 2
127.0.0.1:6379> llen blabla
(integer) 2
127.0.0.1:6379> object encoding blabla
"ziplist"
# 新增列表值的字符串长度65
127.0.0.1:6379> rpush blabla wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
(integer) 3
127.0.0.1:6379> object encoding blabla
"linkedlist"
ziplist转linkedlist:破坏条件2
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"
列表在redis有如下命令:
lpush、rpush、lpop、rpop;lindex;llen;linsert、lrem、ltrim、lset
ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在哈希对象的数据量小的时候使用压缩列表存储。
当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 hashtable 存储。
使用ziplist编码必须满足两个条件:
条件1:哈希对象所有键值对的key和value的字符串元素的长度都小于64字节。
条件2:哈希对象保存的键值对数量小于512个。
ziplist转hashtable:破坏条件1
127.0.0.1:6379> hset anotherbook hello world
(integer) 1
127.0.0.1:6379> object encoding anotherbook
"ziplist"
# 新增哈希对象值的字符串长度65
127.0.0.1:6379> hset book author wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
(integer) 1
127.0.0.1:6379> object encoding book
"hashtable"
ziplist转hashtable:破坏条件2
127.0.0.1:6379> eval "for i=1, 511 do redis.call('hset', KEYS[1], i ,i)end" 1 "anotherbook"
(nil)
127.0.0.1:6379> object encoding anotherbook
"ziplist"
127.0.0.1:6379> hset anotherbook hello1 world1
(integer) 1
127.0.0.1:6379> object encoding anotherbook
"hashtable"
哈希在redis有如下命令:
hset、hget;hexists;hdel、hlen、hgetall
intset(整数集合)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在集合对象的数据量小的时候使用整数集合存储。
当集合的长度小于 512,并且所有元素都是整数时,使用整数集合存储;否则使用 hashtable 存储。
集合对象使用intset编码必须满足两个条件:
条件1:集合的所有元素都是整数,使用整数集合存储。
条件2:集合的长度小于 512。
intset转hashtable:破坏条件1
127.0.0.1:6379> sadd elements 1 3 5
(integer) 3
127.0.0.1:6379> object encoding elements
"intset"
127.0.0.1:6379> sadd elements "seven"
(integer) 1
127.0.0.1:6379> object encoding elements
"hashtable"
intset转hashtable:破坏条件2
127.0.0.1:6379> eval "for i=1, 511 do redis.call('sadd', KEYS[1], i)end" 1 "anotherelements"
(nil)
127.0.0.1:6379> scard anotherelements
(integer) 511
127.0.0.1:6379> object encoding anotherelements
"intset"
127.0.0.1:6379> sadd anotherelements 512
(integer) 1
127.0.0.1:6379> scard anotherelements
(integer) 512
127.0.0.1:6379> object encoding anotherelements
"hashtable"
集合在redis有如下命令:
sadd、spop;scard;sismember、smembers、srandmember;srem
ziplist(压缩列表)主要是为节省内存而设计的内存结构,它的优点就是节省内存,但缺点就是比其他结构要消耗更多的时间,所以 Redis 在有序集合对象的数据量小的时候使用压缩列表存储。
当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。
有序集合对象使用ziplist编码必须满足两个条件:
条件1:有序集合所有元素的长度都小于 64 字节。
条件2:有序集合的长度小于 128个。
ziplist转skiplist:破坏条件1
127.0.0.1:6379> zadd orderslen 2 www
(integer) 1
127.0.0.1:6379> object encoding orderslen
"ziplist"
127.0.0.1:6379> zadd orderslen 3 wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww
(integer) 1
127.0.0.1:6379> object encoding orderslen
"skiplist"
ziplist转skiplist:破坏条件2
127.0.0.1:6379> eval "for i=1,128 do redis.call('zadd',KEYS[1],i,i)end" 1 "orders"
(nil)
127.0.0.1:6379> zcard orders
(integer) 128
127.0.0.1:6379> object encoding orders
"ziplist"
127.0.0.1:6379> zadd orders 3.14 pai
(integer) 1
127.0.0.1:6379> zcard orders
(integer) 129
127.0.0.1:6379> object encoding orders
"skiplist"
有序集合在redis有如下命令:
zadd;zcard;zcount、zrange、zrevrange;zrank、zrevrank;zrem;zscore
五种对象在redis中命令的实现方法与该对象的编码方式有关。
Redis除了会根据值对象的类型来判断键是否能够执行指定命令【即类型检查】之外【基于类型Type的多态】,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。【基于编码的多态】
附注:
redis用于操作键的命令基本上可以分为两大类。
1、对任何类型的键都可以执行,如:del、expire、rename、type、object等命令
2、只能对特定类型的键执行的命令。如前面提到的set、append等只能对字符串键执行,hset、hdel等只能对哈希键执行,等等。这些指定命令在执行前会进行类型检查。
object refcount 命令可以查看值对象的引用次数。
object idletime 命令可以查看值对象的lru时间。