Redis对象
Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成,而每种对象类型对应若干编码方式,不同的编码方式所对应的底层数据结构是不同的。
Redis通过对象系统构建对不同数据结构的使用、优化。
- 类型与编码对应,选择适用的底层数据结构
- 采用引用计数法实现对象回收和共享机制
对象设计
在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。
Redis 必须让每个键都带有类型信息, 使得程序可以检查键的类型, 并为它:
- 选择合适的处理方式
- 根据数据类型的不同编码进行多态处理
为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:
- redisObject 对象.
- 基于 redisObject 对象的类型检查.
- 基于 redisObject 对象的显式多态函数.
- 对 redisObject 进行分配、共享和销毁的机制
redisObject数据结构
redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数, 都表示为这种数据类型.
/*
* Redis 对象
*/
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码方式
unsigned encoding:4;
// LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
unsigned lru:LRU_BITS; // LRU_BITS: 24
// 引用计数
int refcount;
// 指向底层数据结构实例
void *ptr;
} robj;
下图对应上面的结构
其中type、encoding和ptr是最重要的三个属性。
- type记录了对象所保存的值的类型,它的值可能是以下常量中的一个:
/*
* 对象类型
*/
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表
- encoding记录了对象所保存的值的编码,它的值可能是以下常量中的一个:
/*
* 对象编码
*/
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
- ptr是一个指针,指向实际保存值的数据结构,这个数据结构由type和encoding属性决定。举个例子, 如果一个redisObject 的type 属性为
OBJ_LIST, encoding 属性为OBJ_ENCODING_QUICKLIST,那么这个对象就是一个Redis 列表(List),它的值保存在一个QuickList的数据结构内,而ptr 指针就指向quicklist的对象;
下图展示了redisObject 、Redis 所有数据类型、Redis 所有编码方式以及底层数据结构之间的关系:
命令的类型检查和多态
那么Redis是如何处理一条命令的呢?
当执行一个处理数据类型命令的时候,redis执行以下步骤
- 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
- 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
- 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
- 返回数据结构的操作结果作为命令的返回值。
对象共享
redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间。
redis预分配的值对象如下:
- 各种命令的返回值,比如成功时返回的OK,错误时返回的ERROR,命令入队事务时返回的QUEUE,等等
- 包括0 在内,小于REDIS_SHARED_INTEGERS的所有整数(REDIS_SHARED_INTEGERS的默认值是10000)
注意:共享对象只能被字典和双向链表这类能带有指针的数据结构使用。像整数集合和压缩列表这些只能保存字符串、整数等自勉之的内存数据结构
为什么redis不共享列表对象、哈希对象、集合对象、有序集合对象,只共享字符串对象?
- 列表对象、哈希对象、集合对象、有序集合对象,本身可以包含字符串对象,复杂度较高。
- 如果共享对象是保存字符串对象,那么验证操作的复杂度为O(1)
- 如果共享对象是保存字符串值的字符串对象,那么验证操作的复杂度为O(N)
- 如果共享对象是包含多个值的对象,其中值本身又是字符串对象,即其它对象中嵌套了字符串对象,比如列表对象、哈希对象,那么验证操作的复杂度将会是O(N的平方)
如果对复杂度较高的对象创建共享对象,需要消耗很大的CPU,用这种消耗去换取内存空间,是不合适的
引用计数以及对象的消毁
redisObject中有refcount属性,是对象的引用计数,显然计数0那么就是可以回收。
- 每个redisObject结构都带有一个refcount属性,指示这个对象被引用了多少次;
- 当新创建一个对象时,它的refcount属性被设置为1;
- 当对一个对象进行共享时,redis将这个对象的refcount加一;
- 当使用完一个对象后,或者消除对一个对象的引用之后,程序将对象的refcount减一;
- 当对象的refcount降至0 时,这个RedisObject结构,以及它引用的数据结构的内存都会被释放。
String
String是redis中最基本的数据类型,一个key对应一个value。
String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
图例
下图是一个String类型的实例,其中键为hello,值为world
命令使用
| 命令 | 简述 | 使用 |
|---|---|---|
| GET | 获取存储在给定键中的值 | GET name |
| SET | 设置存储在给定键中的值 | SET name value |
| DEL | 删除存储在给定键中的值 | DEL name |
| INCR | 将键存储的值加1 | INCR key |
| DECR | 将键存储的值减1 | DECR key |
| INCRBY | 将键存储的值加上整数 | INCRBY key amount |
| DECRBY | 将键存储的值减去整数 | DECRBY key amount |
底层结构
Redis对象的字符串对象结构如下:
type表明为字符串对象
encoding表示编码方式
Redis String底层由动态字符串SDS实现,C语言字符串只用在日志等不能修改字符的功能。
当调用SET name value时,Redis会创建两个字符串对象,而字符串对象对应的底层实现就是SDS
编码
字符串对象有三种编码方式,分别为embstr,raw, int。对于不同的数据会采用合适的编码 如果是整数且可以用long储存就使用int编码,字符串转为long值保存。 如果为字符,小于32字节使用embstr编码;大于使用raw编码。
embstr
专门用于保存短字符串的一种优化编码方式。一次内存分配给redisObject和sdshdr。
- 相比raw分配函数调用次数更少,释放也更少
- 连续的内存空间,能更好的利用缓存,比raw编码少一次内存寻址
其中元数据占⽤了20字节,剩下44字节用于保存数据,超过44字节会使用raw编码
SDS的结构
结构体如下:
struct sdshdr{
int len;
int free;
char buf[];
}
- len表示字符串的长度
- free表示还未使用空间的长度
- buf是实际储存char的数组
SDS的特性
由于SDS的结构实现可以知道,SDS使用char数组保存字符串,且SDS的内存空间可以大于实际储存的字符,因此SDS具有以下特性
-
常数时间获取字符串长度,len记录了实际字符串长度
-
自动扩容,杜绝了缓冲区溢出
-
空间预分配,基于字符串的长度
- 小于1MB额外分配与字符串相同的空间即free = len
- 大于1MB,free = 1MB
-
惰性空间释放,缩短字符串时不会立刻减少char数组空间,由Redis自己绝对缩短的时机,也提供指令,立刻回收空间
-
二进制安全,不需要用/0表示字符串结束
使用注意事项
批量读写
过期时间
用于计数,注意计数范围,超出signed long最值会报错
String的最大长度为512MB
尽量使用embstr编码,即String长度应该在44字节之内,20字节用于保存元数据
Set
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
图例
命令使用
| 命令 | 简述 | 使用 |
|---|---|---|
| SADD | 向集合添加一个或多个成员 | SADD key value |
| SCARD | 获取集合的成员数 | SCARD key |
| SMEMBER | 返回集合中的所有成员 | SMEMBER key member |
| SISMEMBER | 判断 member 元素是否是集合 key 的成员 | SISMEMBER key membe |
底层结构
集合对象的编码可以是intset或hashtable,当使用hashTable进行保存时,
当集合对象保存的所有元素都是整数值且数量不超过512个,会使用inset编码
inset
typedef struct inset{
uint32_t encoding;
uint32_t length;
int8_t contents[];
}
整数集合中的每个元素由contents数组保存,并按值得大小从小到大有序排列,且不包含重复项。
encoding
决定了contents中保存的类型
包括int16、int32、int64
length
记录了contents数组的长度
升级
当需要插入一个值,这个值的类型比contents类型大,此时会进行升级
即重新分配数组空间,将原来的元素转换类型并插入到合适的位置,最后将当前值进行插入
在升级后集合不会再降级
hashtable
即字典
字典结构
typedef struct dict{
dictType *type;
void *privdata;
dictht ht[2];
int rehashidx;
}
type和privdata针对不同类型的键值对,实现了多态字典
type是指向dictType的指针,每个dictType都保存了对应类型的函数
privdata保存了需要传给特定类型函数的可选参数
ht是保存了两个dictht哈希表的数组,一般情况下只会使用ht[0],ht[1]用于协助进行rehash
rehashidx用于记录rehash的进度,
dictht哈希表
typedef struct dictht{
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
}
**table是以dictEntry为元素的数组
size保存了数组大小
sizemask哈希表掩码用于计算索引值,总是等于size - 1
used表示已有节点数量
dictEntry哈希节点
typedef struct dictEntry{
void *key;
union{
void * val;
uint64_tu64;
int64_ts64;
} v;
struct dictEntry *next;
}dictEntry;
key保存键,v保存值,值可以为指针、uint64_t或int64_t的值。
next保存了指向像一个节点的指针,使用拉链法解决哈希冲突
hash算法
使用Murmur算法作为hash函数,使用拉链法解决hash冲突
rehash
当负载因子超出合理范围之后,哈希表保存的键值对数量太多或太少,需要对哈希表的大小进行调整。
rehash时机
当以下条件中任意一个被满足时,会自动对哈希表进行扩容
- 目前没有执行bgsave命令或者bgrewriteAOF命令,且负载因子大于等于1
- 正在执行bgsave命令或者bgrewriteAOF命令,负载因子大于等于5
当负载因子小于0.1时,会对哈希表进行收缩
rehash过程
- 为ht[1]分配合适的空间,空间大小取决于操作和used
- 在每次进行更新或查询操作时,会顺带将ht[0] [rehashidx]上的键值对
- 重新计算hash值和索引值,并迁移到ht[1]的对应位置上,rehashidx + 1
- 直到rehash == ht[0].size,说明完成迁移,rehashidx 设置为-1
- 迁移结束后将ht[1]设置为ht[0],再创建一个新的ht[1]
rehash期间查找,首先下在ht[0]进行,没有找到再去ht[1]上查询
List
Redis中的List其实就是链表(Redis用双端链表实现List)。
使用List结构,我们可以轻松地实现最新消息排队功能(比如新浪微博的TimeLine)。List的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在List中,然后工作线程再用 POP 操作将任务取出进行执行。
图例
命令使用
| 命令 | 简述 | 使用 |
|---|---|---|
| RPUSH | 将给定值推入到列表右端 | RPUSH key value |
| LPUSH | 将给定值推入到列表左端 | LPUSH key value |
| RPOP | 从列表的右端弹出一个值,并返回被弹出的值 | RPOP key |
| LPOP | 从列表的左端弹出一个值,并返回被弹出的值 | LPOP key |
| LRANGE | 获取列表在给定范围上的所有值 | LRANGE key 0 -1 |
| LINDEX | 通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 | LINEX key index |
使用列表的技巧
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
底层结构
列表的redisObject结构如下:
type指明该object的类型
encoding是具体数据的储存的编码格式有ziplist和linkedlist两种
当保存的所有字符串都小于64字节,且列表的保存的元素数量少于512个时,会使用ziplist
ziplist
为节约内存而存在,是由一系列特殊编码的连序数据结构。
一个ziplist可以保存任意个节点,每个节点可以保存一个字节数组或者一个整数值
组成
Entry
每个压缩列表节点都可以保存一个字节数组或一个整数值
previous_entry_length
以字节为单位,记录了压缩列表中前一个节点的长度,长度可以为一个字节或5个字节
由长度是否超过254个字节
encoding
记录了节点的content保存的数据结构和长度
- 1字节、2字节或5字节,值的高位为00, 01,10表示是字节数组编码,其他位记录字节数组的长度,具体为多少字节由字节数组的长度决定
- 一字节长,值的最高位是11,表示整数编码,其他未记录整数的长度和类型。11之后两位00、01、10、分别表示int16、int32、int64类型。11110000、11111110分别表示24为有符号整数和8位有符合整数。
content
具体储存节点值,值的长度和类型由encoding决定。
连锁更新
当存在大量长度接近254字节的节点时,添加节点可能会导致的出现上一个节点长度超过254,previous_entry_length重新分配为5字节,此时当前节点也超过254字节,下一节点也需要重新分配空间,以此类推导致连续更新。
linkedlist
typedef struct list{
listNode *head;
lsitNode *tail;
unsigned long len;
void *(*dup) (void *ptr);
void (*free) (void *ptr);
int (*match) (void *ptr, void key)
}
特性
- 双向链表
- 无环
- 有头尾节点
- len记录链表长度
- 多态
QuickList
链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。因此Redis3.2版本开始对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist.
组成
typedef struct quicklistNode{
quicklistNode *prev;
quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
- prev: 指向链表前一个节点的指针。
- next: 指向链表后一个节点的指针。
- zl: 数据指针。如果当前节点的数据没有压缩,那么它指向一个ziplist结构;否则,它指向一个quicklistLZF结构。
- sz: 表示zl指向的ziplist的总大小(包括
zlbytes,zltail,zllen,zlend和各个数据项)。需要注意的是:如果ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。 - count: 表示ziplist里面包含的数据项个数。这个字段只有16bit。稍后我们会一起计算一下这16bit是否够用。
- encoding: 表示ziplist是否压缩了(以及用了哪个压缩算法)。目前只有两种取值:2表示被压缩了(而且用的是LZF压缩算法),1表示没有压缩。
- container: 是一个预留字段。本来设计是用来表明一个quicklist节点下面是直接存数据,还是使用ziplist存数据,或者用其它的结构来存数据(用作一个数据容器,所以叫container)。但是,在目前的实现中,这个值是一个固定的值2,表示使用ziplist作为数据容器。
- recompress: 当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩。
- attempted_compress: 这个值只对Redis的自动化测试程序有用。我们不用管它。
- extra: 其它扩展字段。目前Redis的实现里也没用上
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
quicklistLZF结构表示一个被压缩过的ziplist。其中:
- sz: 表示压缩后的ziplist大小。
- compressed: 是个柔性数组(flexible array member),存放压缩后的ziplist字节数组。
Copy
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
- head: 指向头节点(左侧第一个节点)的指针。
- tail: 指向尾节点(右侧第一个节点)的指针。
- count: 所有ziplist数据项的个数总和。
- len: quicklist节点的个数。
- fill: 16bit,ziplist大小设置,存放
list-max-ziplist-size参数的值。 - compress: 16bit,节点压缩深度设置,存放
list-compress-depth参数的值。
注意事项
Hash
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
图例
命令使用
| 命令 | 简述 | 使用 |
|---|---|---|
| HSET | 添加键值对 | HSET hash-key sub-key1 value1 |
| HGET | 获取指定散列键的值 | HGET hash-key key1 |
| HGETALL | 获取散列中包含的所有键值对 | HGETALL hash-key |
| HDEL | 如果给定键存在于散列中,那么就移除这个键 | HDEL hash-key sub-key1 |
底层结构
哈希对象的编码可以是ziplist和hashtable,使用ziplist底层的哈希对象会将键值作为两个节点连续插入队尾。
当哈希对象同时满足以下两个条件,会使用ziplist:
- 哈希对象保存的所有键和值都小于64字节
- 哈希对象保存的键值对数量小于512个
Zset
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
图例
命令使用
| 命令 | 简述 | 使用 |
|---|---|---|
| ZADD | 将一个带有给定分值的成员添加到哦有序集合里面 | ZADD zset-key 178 member1 |
| ZRANGE | 根据元素在有序集合中所处的位置,从有序集合中获取多个元素 | ZRANGE zset-key 0-1 withccores |
| ZREM | 如果给定元素成员存在于有序集合中,那么就移除这个元素 | ZREM zset-key member1 |
底层结构
有序集合的编码可以是ziplist或者skiplist
使用ziplist编码会将元素和对应的分值相邻保存,并从小到大排序
使用skiplist编码,对象中会包含一个字典和一个跳表。
- 编码转换
当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
1、保存的元素数量小于128;
2、保存的所有元素长度都小于64字节。
不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。
注意事项
Zset score 存储数据结构为浮点型(float64),如果超过 17 位时会出现精度问题。
可以采取Key 不同范围数字来表征不同的维度分值