《Redis设计与实现第二版》
主要介绍了redis中的字符串类型实现原理,以及redis哪些内容是使用字符串类型存储的。内存空间,扩容,内存大小,已使用大小等等规则。
SDS有一套自己的扩容与内存释放规则。
主要介绍了redis中的链表类型实现原理list,背后是双向链表实现,且带有头节点指针和尾节点指针,节点数量等。
// 每个链表节点使用一个adlist.h/listNode结构来表示
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
// 链表结构 adlist.h/list
typedef struct list {
// 表头节点
*head;
listNode // 表尾节点
*tail;
listNode // 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
} list;
key-value结构,Redis 的字典使用哈希表作为底层实现, 一个哈希表里面可以有2个哈希表节点, 而每个哈希表节点就保存了字典中的一个键值对。
结构有哈希表数组(一个二维指针,数组每个元素存储的是内存地址)、哈希表大小、大小掩码总是等于哈希表大小减去1、哈希表已有的节点数量。
// redis字典所使用的哈希表由dict.h/dictht结构定义
typedef struct dictht {
// 哈希表数组
**table;
dictEntry // 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
每个节点有key、union类型value、还有一个存储下一个节点地址的指针next,可以形成链表(解决键冲突问题)。
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
redis中一个字典内置有多个哈希表(可以缓解哈希冲突与单个哈希表过大的问题),dictType类型指针,私有数据void类型指针privdata,rehash索引int rehashidx,dictType内部存储了许多key相关的函数指针,如计算哈希值函数、复制键的函数、复制值得函数、对比键得函数、销毁键销毁值得函数等。
// redis中的字典由dict.h/dict结构表示
typedef struct dict {
// 类型特定函数
*type;
dictType // 私有数据
void *privdata;
// 哈希表
[2];
dictht ht// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
第2个哈希表只在第一个哈希表进行rehash时使用。rehashidx记录了rehash目前得进度,目前没有进行rehash则其值为-1。
哈希算法,redis用了MurmurHash2算法来计算键得哈希值,其优点是即使输入得键是有规律的,算法仍可以给出一个很好得随机分布性。
= dict->type->hashFunction(key);
hash = hash & dict->ht[x].sizemask; index
解决键冲突,两个或两个以上被分配到哈希表数组的同一个索引上面时,称这些键发生了冲突。每个哈希表节点都有一个next指针,使用单向链表连接起来解决冲突。 为了效率高,程序使用的是链表前插法。
rehash,当单个哈希表保存的键值数量太多或太少时,程序需要对哈希表的大小进行扩展或者收缩,使得负载因子维持在一个合理的范围之内。 为ht1哈希表分配空间,进行一定程度的扩展收缩策略确定ht1大小,将ht0已有的键值rehash到ht1,然后把ht1设置为ht0。
哈希表的扩展与收缩,当满足任意一个条件,程序会自动开始对哈希表执行扩展操作:
哈希表的负载因子公式
# 负载因子 = 哈希表已经保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
当负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
在BGSAVE或BGREWRITEAOF过程中,Redis需要创建服务器进程的子进程,大多数操作系统采用写时赋值优化子进程的使用效率,所以在子进程存在 期间,服务器会提高所需的负载因子,从而避免在子进程存在期间进行哈希表的扩展操作,避免不必要的内存写入操作。
渐进式rehash,为什么,rehash过程是从ht0 中的键值rehash到ht1中去,如果ht0中的键值比较少还好这个过程需要的时间比较短,但是 如果有几十万的键值需要处理呢,可能服务都会被迫中断一会。redis中的字典rehash是循序渐进进行的。
哈希表渐进式rehash的步骤:
渐进式rehash执行期间的哈希表操作:在进行渐进式rehash过程中,字典同时有ht0和ht1两个哈希表,在此期间字典的删除 查找 更新等操作会在 两个哈希表上进行,比如说,要在字典里面查找一个键会现在ht0中查找,没找到再在ht1里面查找。 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht1 里面, 而 ht0 则不再进行任何添加操作: 这一措施保证了 ht0 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。
跳跃表skiplist是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 跳跃表支持平均 O(log N) 最坏 O(N) 复杂度的节点查找, 还可以通过顺序性操作来批量处理节点。
Redis 只在两个地方用到了跳跃表, 一个是实现有序集合键, 另一个是在集群节点中用作内部数据结构。 维护跳跃表相关基础信息的结构为zskiplist结构,其包含属性:
// 跳跃表节点的实现由redis.h/zskiplistNode
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
*obj;
robj // 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
zkiplistNode结构
表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到。
层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以使用这些层加快访问其他节点的速度,一般来说层的数量越多,访问 其他节点的速度就越快(空间换时间没有免费的午餐)。
每次创建一个新跳跃表节点的时候, 程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的“高度”。
遍历操作只是用前进指针就可以完成了,跨度实际上是用来计算排位rank的,在查找某个节点的过程中,将沿途访问过的所有层的跨度累计 起来,得到的结果就是目标节点在跳跃表中的排位。
节点的后退指针(backward 属性)用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同, 因为每个节点只有一个后退指针, 所以每次只能后退至前一个节点。
节点的成员对象是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值。
整数集合intset 是集合键底层实现之一:当一个集合只包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合 键的底层实现。
整数集合的实现:整数集合intset是redis用于保存整数值的集合抽象数据结构,可以保存int16_t int32_t int64_t的整数,并保证 集合中不会出现重复元素。
typedef struct intset
{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
元素存在contents数组中,元素按值大小从小到大有序排列,并不会包含任何重复项。 contents元素类型取决于encoding属性的值
升级:将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有元素类型都要长时,整数集合需要先进行升级,然后才能 将新元素添加到整数集合里面。先申请目标大小的空间,末尾预留一个位置,然后从原有末尾一个个将元素升级处理放置到正确位置。最后将新元素 放到数组的末尾。然后更改encoding、将length属性值修改。
因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大, 所以这个新元素的值要么就大于所有现有元素, 要么就小于所有现有元素:
升级的好处: 一个是提升整数集合的灵活性, 另一个是尽可能地节约内存。
降级:整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
压缩列表ziplist 是列表键和哈希键的底层实现之一。
当一个列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,redis就会使用压缩列表 来做列表键的底层实现。
> RPUSH lst 1 3 5 10086 "hello" "world"
redis(integer) 6
> OBJECT ENCODING lst
redis"ziplist"
另外,当一个哈希键只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串, 要么redis就会使用压缩列表来做哈希键的底层实现。
"name" "Jack" "age" 28 "job" "Programmer"
redis》 HMSET profile
OK> OBJECT ENCODING profile
redis"ziplist"
压缩列表的构成:
压缩列表是redis为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。 一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
属性 类型 长度 用途
zlbytes uint32_t 4 字节 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltail uint32_t 4 字节 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
zllen uint16_t 2 字节 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。
entryX 列表节点 不定 压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
zlend uint8_t 1 字节 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。
压缩列表节点的构成:
每个压缩列表节点可以保存一个字节数组或者一个整数值,其中字节数组可以是以下三种长度的其中一种
整数值则可以是以下六种长度的其中一种:
每个压缩列表节点都由以下三部分组成
previous_entry_length
previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性的长度可以是1字节或者5字节。
程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。
encoding
节点的encoding属性记录了节点的content属性所保存数据的类型以及长度:
content
content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
连锁更新
每个节点的previous_entry_length属性都记录了前一个节点的长度:前一个节点的长度小于254节点,那么previous_entry_length属性占用1字节,否则占用5字节。
如果有一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点,如果我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,则需要后面扩容previous_entry_length,因为扩容导致节点又达到254及以上,这样下去后面都需要进行更新。Redis将这种在特殊情况下产生的连续多次空间扩展操作称之为 连锁更新。
除了添加新节点可能引发连锁更新之外,删除节点也可能会引发连锁更新。
因为连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配操作, 而每次空间重分配的最坏复杂度为 O(N) , 所以连锁更新的最坏复杂度为 O(N^2) 。
尽管连锁更新的复杂度较高, 但它真正造成性能问题的几率是很低的
因为以上原因,ziplistPush命令的平均复杂度为O(N)。
对象:
前面介绍了redis中所用主要数据结构,如简单动态字符串SDS、双端链表、字典、压缩列表、整数集合,等等。
redis并没有直接使用这些数据结构实现键值对数据库,而是基于这些数据结构创建了一个对象系统,对象系统包含了5种类型的对象。
redis的对象系统还实现了基于引用技术技术的内存回收机制;还通过引用计数技术实现了对象共享机制,通过让多个数据库键共享同一个对象来节约内存。
redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能情况下,空转时长较大 的哪些键可能会被优先被服务器删除。
对象的类型与编码:
redis使用对象来表示数据库种的键和值,至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。
redis> SET msg "hello world"
OK
每个对象都由一个redisObject结构表示,结构中有三个属性分别是type属性、encoding属性和ptr属性。
typedef struct redisObject
{
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型
类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象 REDIS_ZSET 有序集合对象
type查看键对应的值对象的类型,键对象类型永远是string类型
# 字符串对象
127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> keys *
1) "msg"
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> hmset profile name Tome age 25 career Programmer
OK
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.0 cherry
(integer) 3
127.0.0.1:6379> type price
zset
编码和底层实现:
对象的ptr指针指向对象的底层数据结构,数据结构由对象的encoding属性决定,encoding属性记录了对象使用了什么数据结构作为对象的底层实现。
编码常量 编码所对应的底层数据结构long 类型的整数 int
REDIS_ENCODING_INT
REDIS_ENCODING_EMBSTR embstr 编码的简单动态字符串 embstr
REDIS_ENCODING_RAW 简单动态字符串 raw
REDIS_ENCODING_HT 字典 hashtable
REDIS_ENCODING_LINKEDLIST 双端链表 linkedlist
REDIS_ENCODING_ZIPLIST 压缩列表 ziplist
REDIS_ENCODING_INTSET 整数集合 intset REDIS_ENCODING_SKIPLIST 跳跃表和字典 skiplist
每种类型的对象至少使用了两种不同的编码。下面列出了每种类型的对象可以使用的编码。
类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象。
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。
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 使用跳跃表和字典实现的有序集合对象。
object encoding命令查看一个数据库键的值对象的编码
# embstr
127.0.0.1:6379> set msg "hello world"
OK
127.0.0.1:6379> object encoding msg
"embstr"
# raw
127.0.0.1:6379> set story "long long brfuierbvdjfkkcdnsdcnwoejowifjoirejfoeoreggtghtruuibrivndlfnvkdfnvndfkncskdjcnkdcscdscvdbgfbfgbffew"
OK
127.0.0.1:6379> object encoding story
"raw"
# intset
127.0.0.1:6379> sadd numbers 1 3 5
(integer) 3
127.0.0.1:6379> object encoding numbers
"intset"
# hashtable
127.0.0.1:6379> sadd numbers "seven"
(integer) 1
127.0.0.1:6379> object encoding numbers
"hashtable"
redis可以根据不同的使用场景为一个对象设置不同的编码,从而优化对象在某一个场景下的效率。如 在列表对象包含的元素比较少时,使用压缩列表作为列表对象的底层实现:
字符串对象:
字符串编码可以是int、raw、embstr,一个字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int 。
127.0.0.1:6379> set number 10086
OK127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> set num 32948398498938493849384934394
OK127.0.0.1:6379> object encoding num
"embstr"
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么将使用一个简单动态字符串SDS
保存这个字符串值,并将对象编码设置为raw。
127.0.0.1:6379> set jack "cnf12345678901234567890123456789012345678901234567890"
OK127.0.0.1:6379> strlen jack
(integer) 53
127.0.0.1:6379> object encoding jack
"raw"
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于
39 字节, 那么字符串对象将使用 embstr
编码的方式来保存这个字符串值。
embstr 编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw 编码一样, 都使用 redisObject 结构和 sdshdr 结构来表示字符串对象, 但 raw 编码会调用两次内存分配函数来分别创建 redisObject 结构和 sdshdr 结构, 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject 和 sdshdr 两个结构。
emstr的好处
long double类型表示的浮点数在redis种也是作为字符串值来保存的, 如果我们要保存一个浮点数到字符串对象里面, 那么程序会先将这个浮点数转换成字符串值, 然后再保存起转换所得的字符串值。
127.0.0.1:6379> set pi 3.14
OK
127.0.0.1:6379> object encoding pi
"embstr"
在有需要的时候,程序会将字符串对象里的字符串值转换回浮点数值,执行某些操作,然后再将执行操作所得的浮点数值转换回字符串值,并继续保存在字符串对象里面。
127.0.0.1:6379> incrbyfloat pi 2.0
"5.14"
127.0.0.1:6379> object encoding pi
"embstr"
字符串对象保存各类型值的编码方式
值 编码
long 类型保存的整数。 int
可以用
long double 类型保存的浮点数。 embstr 或者 raw
可以用
字符串值, 或者因为长度太大而没办法用long 类型表示的整数, 又或者因为长度
double 类型表示的 embstr 或者 raw
太大而没办法用long 浮点数。
编码的转换:
int编码的字符串对象和embstr编码的字符串对象在满足条件的情况下,会被转换为raw编码的字符串对象。
对于 int 编码的字符串对象来说, 如果我们向对象执行了一些命令, 使得这个对象保存的不再是整数值, 而是一个字符串值, 那么字符串对象的编码将从 int 变为 raw 。
127.0.0.1:6379> set number 10086
OK
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> append number " is a good number!"
(integer) 23
127.0.0.1:6379> get number
"10086 is a good number!"
127.0.0.1:6379> object encoding number
"raw"
因为redis没有为embstr编码的字符串对象编写任何相应的修改程序,只有int和raw编码的字符串对象有这些程序,对embstr操作时会将编码从 embstr转换为raw,然后再执行修改命令。
字符串命令的实现:
字符串键的值为字符串对象,用于字符串键的所有命令都是针对字符串对象来构建的。
字符串命令的实现
命令 | int编码的实现方法 | embstr编码的实现方法 | raw编码的实现方法 |
---|---|---|---|
SET | 使用 int 编码保存值。 | 使用 embstr 编码保存值。 | 使用 raw 编码保存值。 |
GET | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后向客户端返回这个字符串值。 | 直接向客户端返回字符串值。 | 直接向客户端返回字符串值。 |
APPEND | 将对象转换成 raw 编码, 然后按raw 编码的方式执行此操作。 | 将对象转换成 raw 编码, 然后按raw 编码的方式执行此操作。 | 调用 sdscatlen 函数, 将给定字符串追加到现有字符串的末尾。 |
INCRBYFLOAT | 取出整数值并将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 | 取出字符串值并尝试将其转换成long double 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 | 取出字符串值并尝试将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 |
INCRBY | 对整数值进行加法计算, 得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令, 向客户端返回一个错误。 | raw 编码不能执行此命令, 向客户端返回一个错误。 |
DECRBY | 对整数值进行减法计算, 得出的计算结果会作为整数被保存起来。 | embstr 编码不能执行此命令, 向客户端返回一个错误。 | raw 编码不能执行此命令, 向客户端返回一个错误。 |
STRLEN | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 计算并返回这个字符串值的长度。 | 调用 sdslen 函数, 返回字符串的长度。 | 调用 sdslen 函数, 返回字符串的长度。 |
SETRANGE | 将对象转换成 raw 编码, 然后按raw 编码的方式执行此命令。 | 将对象转换成 raw 编码, 然后按raw 编码的方式执行此命令。 | 将字符串特定索引上的值设置为给定的字符。 |
GETRANGE | 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 |
列表对象:
列表对象的编码可以是ziplist或者linkedlist。ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点entry保存一个列表元素。
127.0.0.1:6379> rpush numbers 1 "three" 5
(integer) 3
链表节点里面存储字符串对象
完整的字符串对象表示
编码转换:
当列表对象同时满足两个条件时,列表对象使用ziplist编码。不同时满足下面两个条件的列表对象需要使用linkedlist编码。
条件中的上限值可以修改,配置文件 list-max-ziplist-value 与 list-max-ziplist-entries 选项说明
列表命令的实现:
列表键的值为列表对象,所以用于列表键的所有命令都是针对列表对象来构建的。
命令 | ziplist编码的实现方法 | linkedlist编码的实现方法 |
---|---|---|
LPUSH | 调用 ziplistPush 函数, 将新元素推入到压缩列表的表头。 | 调用 listAddNodeHead 函数, 将新元素推入到双端链表的表头。 |
RPUSH | 调用 ziplistPush 函数, 将新元素推入到压缩列表的表尾。 | 调用 listAddNodeTail 函数, 将新元素推入到双端链表的表尾。 |
LPOP | 调用 ziplistIndex 函数定位压缩列表的表头节点, 在向用户返回节点所保存的元素之后, 调用ziplistDelete 函数删除表头节点。 | 调用 listFirst 函数定位双端链表的表头节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表头节点。 |
RPOP | 调用 ziplistIndex 函数定位压缩列表的表尾节点, 在向用户返回节点所保存的元素之后, 调用ziplistDelete 函数删除表尾节点。 | 调用 listLast 函数定位双端链表的表尾节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表尾节点。 |
LINDEX | 调用 ziplistIndex 函数定位压缩列表中的指定节点, 然后返回节点所保存的元素。 | 调用 listIndex 函数定位双端链表中的指定节点, 然后返回节点所保存的元素。 |
LLEN | 调用 ziplistLen 函数返回压缩列表的长度。 | 调用 listLength 函数返回双端链表的长度。 |
LINSERT | 插入新节点到压缩列表的表头或者表尾时, 使用ziplistPush 函数; 插入新节点到压缩列表的其他位置时, 使用 ziplistInsert 函数。 | 调用 listInsertNode 函数, 将新节点插入到双端链表的指定位置。 |
LREM | 遍历压缩列表节点, 并调用 ziplistDelete 函数删除包含了给定元素的节点。 | 遍历双端链表节点, 并调用 listDelNode 函数删除包含了给定元素的节点。 |
LTRIM | 调用 ziplistDeleteRange 函数, 删除压缩列表中所有不在指定索引范围内的节点。 | 遍历双端链表节点, 并调用 listDelNode 函数删除链表中所有不在指定索引范围内的节点。 |
LSET | 调用 ziplistDelete 函数, 先删除压缩列表指定索引上的现有节点, 然后调用 ziplistInsert 函数, 将一个包含给定元素的新节点插入到相同索引上面。 | 调用 listIndex 函数, 定位到双端链表指定索引上的节点, 然后通过赋值操作更新节点的值。 |
哈希对象:
哈希对象的编码可以是 ziplist 或者 hashtable 。
ziplist编码的哈希对象:每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HSET profile name "Tom"
(integer) 1
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HSET profile age 25
(integer) 1
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HSET profile career "Programmer"
(integer) 1
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HGETALL profile
1) "name"
2) "Tom"
3) "age"
4) "25"
5) "career"
6) "Programmer"
ziplist编码的profile哈希对象
profile哈希对象的压缩列表底层实现
hashtable编码的哈希对象
哈希对象中的每个键值对都使用一个字典键值对来保存:
哈希对象编码转换:
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:
不能同时满足这两个条件的哈希对象需要使用 hashtable 编码。
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HSET book name "Mastering C++ in 21 days"
(integer) 1
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> OBJECT ENCODING book
"listpack"
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content"
(integer) 1
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686> OBJECT ENCODING book
"hashtable"
redis-18686.c290.ap-northeast-1-2.ec2.redns.redis-cloud.com:18686>
哈希命令的实现:用于哈希键的所有命令都是针对哈希对象来构建的。
命令 | ziplist编码实现方法 | hashtable编码实现方法 |
---|---|---|
HSET | 首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用 ziplistPush 函数, 将值推入到压缩列表的表尾。 | 调用 dictAdd 函数, 将新节点添加到字典里面。 |
HGET | 首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。 | 调用 dictFind 函数, 在字典中查找给定键, 然后调用dictGetVal 函数, 返回该键所对应的值。 |
HEXISTS | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 | 调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 |
HDEL | 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 | 调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。 |
HLEN | 调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。 | 调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。 |
HGETALL | 遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。 | 遍历整个字典, 用 dictGetKey 函数返回字典的键, 用dictGetVal 函数返回字典的值。 |
集合对象:
集合对象的编码可以是 intset 或者 hashtable
inset编码的集合对象:集合对象包含的所有元素都被保存在整数集合里面。
redis> SADD number 1 3 5
(integer) 3
hashtable编码的对象集合:使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。
redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
编码的转换:
当集合对象可以同时满足以下两个条件时,对象使用intset编码
不能满足上面两个条件的集合对象需要使用hashtable编码。当intset编码的不满足上面时则会编码从intset变为hashtable。
redis> SADD numbers 1 3 5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
redis> SADD numbers 1 3 5
(integer) 3
redis> OBJECT ENCODING numbers
"intset"
512个元素限制
redis> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers
(nil)
redis> SCARD integers
(integer) 512
redis> OBJECT ENCODING integers
"intset"
redis> SADD integers 10086
(integer) 1
redis> SCARD integers
(integer) 513
redis> OBJECT ENCODING integers
"hashtable"
集合命令的实现
命令 | intset编码实现方法 | hashtable编码实现方法 |
---|---|---|
SADD | 调用 intsetAdd 函数, 将所有新元素添加到整数集合里面。 | 调用 dictAdd , 以新元素为键, NULL 为值, 将键值对添加到字典里面。 |
SCARD | 调用 intsetLen 函数, 返回整数集合所包含的元素数量, 这个数量就是集合对象所包含的元素数量。 | 调用 dictSize 函数, 返回字典所包含的键值对数量, 这个数量就是集合对象所包含的元素数量。 |
SISMEMBER | 调用 intsetFind 函数, 在整数集合中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。 | 调用 dictFind 函数, 在字典的键中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。 |
SMEMBERS | 遍历整个整数集合, 使用 intsetGet 函数返回集合元素。 | 遍历整个字典, 使用 dictGetKey 函数返回字典的键作为集合元素。 |
SRANDMEMBER | 调用 intsetRandom 函数, 从整数集合中随机返回一个元素。 | 调用 dictGetRandomKey 函数, 从字典中随机返回一个字典键。 |
SPOP | 调用 intsetRandom 函数, 从整数集合中随机取出一个元素, 在将这个随机元素返回给客户端之后, 调用 intsetRemove 函数, 将随机元素从整数集合中删除掉。 | 调用 dictGetRandomKey 函数, 从字典中随机取出一个字典键, 在将这个随机字典键的值返回给客户端之后, 调用dictDelete 函数, 从字典中删除随机字典键所对应的键值对。 |
SREM | 调用 intsetRemove 函数, 从整数集合中删除所有给定的元素。 | 调用 dictDelete 函数, 从字典中删除所有键为给定元素的键值对。 |
有序集合对象:
有序集合的编码可以是ziplist或者skiplist
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存。第一个节点存元素成员member第二个元素存分值score。按分值从小到大排序。
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
struct zset
typdef {
*zsl;
zskiplist *dict;
dict } zset;
zset中zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点保存一个集合元素:跳跃表节点object属性保存元素的成员,跳跃表节点的score属性则保存了元素的分值。
除此外,zset中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值都保存了一个集合元素,字典的键保存了元素的成员,字典的值则保存了元素的分值,可以O(1)查找给定成员的分值。
有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。虽然zset同时用跳跃表和字典保存有序集合元素,这两种数据结构都会通过指针来共享相同元素的成员和分值
。
编码的转换:
当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码
不能同时满足以上两个条件的有序集合对象将使用skiplist编码。以上两个条件上限值可以改zset-max-ziplist-entries
和zset-max-ziplist-value
。
有序集合命令的实现:
命令 | ziplist编码的实现方法 | zset编码的实现方法 |
---|---|---|
ZADD | 调用 ziplistInsert 函数, 将成员和分值作为两个节点分别插入到压缩列表。 | 先调用 zslInsert 函数, 将新元素添加到跳跃表, 然后调用 dictAdd 函数, 将新元素关联到字典。 |
ZCARD | 调用 ziplistLen 函数, 获得压缩列表包含节点的数量, 将这个数量除以 2 得出集合元素的数量。 | 访问跳跃表数据结构的 length 属性, 直接返回集合元素的数量。 |
ZCOUNT | 遍历压缩列表, 统计分值在给定范围内的节点的数量。 | 遍历跳跃表, 统计分值在给定范围内的节点的数量。 |
ZRANGE | 从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素 | 从表头向表尾遍历跳跃表,返回给定索引范围内的所有元素 |
ZREVRANGE | 从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素 | 从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。 |
ZRANK | 从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 | 从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
ZREVRANK | 从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 | 从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
ZREM | 遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。 | 遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。 |
ZSCORE | 遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分值节点保存的元素分值 | 直接从字典中取出给定成员的分值 |
类型检查与命令多态:
Redis中用于操作键的命令基本可以分为两种类型。
一种是可以对任何类型的键执行 如 DEL、EXPIRE、RENAME、TYPE、OBJECT等。
# 字符串键
redis> SET msg "hello"
OK
#列表键
redis> RPUSH numbers 1 2 3
(integer) 3
#集合键
redis> SADD fruits apple banana cherry
(integer) 3
redis> DEL msg
(integer) 1
redis> DEL numbers
(integer) 1
redis> DEL fruits
(integer) 1
另一种命令只能对特定类型的键执行,如
redis> SET msg "hello world"
OK
redis> GET msg
"hello world"
redis> APPEND msg " again!"
(integer) 18
redis> GET msg
"hello world again!"
redis> LLEN msg
(error) WRONGTYPE Operation against a key holding the wrong kind of value
# LLEN不能用于字符串键
类型检查的实现:
类型特定命令所进行的类型检查是通过redisObject结构的type属性来实现的。
多态命令的实现:
redis除了根据值对象的类型来判断键是否能够执行指定名另外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。
DEL、EXPIRE、TYPE等命令也称为多态命令,无论输入的键是什么类型,命令都可以执行。
DEL、EXPIRE等命令和LLEN命令的区别在于,前者是基于类型的多态(一个命令可以同时用于处理多种不同类型的键),后者是基于编码的多态(一个命令可以同时用于处理多种不同编码)。