第2章简单动态字符串

Redis中,涉及可以被修改的字符串值时,都用简单动态字符串(simple dynamic string,SDS)来实现。比如包含字符串值的键值对在底层的实现。C字符串(C语言中传统字符串,以空字符串结尾的字符数组)则用于无须对字符串进行修改的地方。

set msg "hello world"
rpush fruits "apple" "banana" "cherry"

SDS还被用作缓冲区,比如AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区。

2.1SDS定义

struct sdshdr{
    //buf已使用的字节数
    int len;
    //buf未使用的字节数
    int free;
    //字节数组,用于保存字符串
    char buf[];
}

buf遵循C字符串以空字符串结尾的惯例,保存空字符串的1字节空间不计算在SDS的len属性里面,并为空字符分配额外1字节空间,对用户来说是透明的。

redis 设置值的时候报对象无法转化成字符串_开发语言


如中展示了SDS的数据结构,free属性值为0,表示这个SDS没有分配任何未使用的空间;Len属性为5,已使用5字节,buf存储了字符串值,最后一个字节保存了空字符'\0'。这里要注意的是,free和len的计算不涉及空字符。

2.2SDS于C字符串的区别

2.2.1SDS有常数级的时间复杂度获取字符串长度。

由于C字符串不会记录自身长度,因此只能遍历,直到遇到结尾的空字符为止,时间复杂度为O(N)。而SDS对于字符串长度的记录都是在其API中执行的,所以时间复杂度为O(1)

2.2.2SDS杜绝缓冲区溢出。

由于C字符串未记录自身长度,容易导致缓冲区溢出。在执行字符串拼接时,如果没有足够的空间,并且相邻内存地址被其他字符串占用时,字符串的数据将溢出,且容易意外修改相邻的字符串内容。相比而言,SDS会将这种情况扼杀在摇篮之中,SDS API先判断空间是否满足,如果不满足则将空间扩展至执行修改所需的大小

2.2.3减少修改字符串时带来的内存重分配次数

  • 空间预分配

空间与分配用于优化SDS字符串增长的操作,当SDS的API对一个SDS进行修改,并且需要对SDS进行扩展的时候,程序回味SDS分配修改所必须要的空间,还会为SDS分配额外的未使用的空间

分配方式:SDS长度小于1MB,程序分配和len属性同样大小的未使用空间

SDS长度大于等于1MB,分配1MB空间

  • 惰性空间释放

释放用于优化SDS的字符串缩短操作:当SDS的API缩短保存的字符串时,不利己使用内存重分配来回收缩段多出来的字节,而是使用free属性记录这些字节的数量,留给将来使用。(提供相应API,删除未使用的空间)

2.2.4SDS API都是二进制安全的。

C字符串的字符必须符合某种编码,并且中间不能有空字符,否则读取时会被误以为是字符串结尾。种种局限使得C字符串只能存文本,不能存图片,音频,视频,压缩文件等二进制数据。 为确保Redis对不同使用场景的支持,SDS API都是二进制安全的,也就是所有SDS API都会以二进制的方式存取buf中的数据,数据的写入和读出都是一个样的。由于SDS读取时并不是依靠空字符来判断结束的,而是len属性,所以是二进制安全的。

2.2.5兼容部分C字符串函数。

SDS虽然都是二进制安全的,但也遵循以空字符结尾的习惯。SDS API总会在buf数组分配空间时多分配一个字节用于容纳空字符,这是为了保存文本的SDS重用一部分<string.h>库函数,避免代码重复。

2.6总结

redis 设置值的时候报对象无法转化成字符串_字符串_02

第3章链表

当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

3.1链表和链表节点的实现

typedef struct listNode{
    //前置节点
    struct listNode *prev;
    //后置节点
    struct listNode *next;
    //节点的值
    void *value;
}listNode;

多个listNode可以使用prev和next指针组成双端链表

redis 设置值的时候报对象无法转化成字符串_键值对_03

typedef struct list{
    //表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void (*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr,void *key);
}list;

redis 设置值的时候报对象无法转化成字符串_后端_04

Redis的链表特性可以总结如下:

双端:链表节点带有prev和next指针,获取前置和后置节点的复杂度都是O(1)。
无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。 带表头指针和表尾指针 带链表长度计数器 。
头尾指针:将程序获取头尾节点的复杂度降为O(1)。
长度计数器:将程序获取表长的复杂度降为O(1)。
多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

第4章 字典

字典又称符号表(symbol table),关联数组映射,用于保存键值对的抽象数据结构。当一个哈希键包含的键值对比较多时,或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。

在字典中,一个**键(key)可以和一个值(value)**进行关联,这些关联称为键值对

4.1字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对

4.1.1哈希表

typedef struct dictht{
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于size-1
    unsigned long sizemask;
    //该哈希表已有节点的数量
    unsigned long used;
}dictht;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EM5y8ZG2-1646302504100)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211124145450676.png)]

每个dictEntry结构保存着一个键值对,size属性记录了哈希表的大小,即table数组大小,而used属性记录了哈希表目前已有的键值对数量。sizemask属性总是等于size-1。

4.1.2哈希表节点

dictEntry就是一个键值对,也是哈希表的一个结点。

typedef struct dictEntry{
    //键
    void *key;
    //值
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    //指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

key属性保存着键值对中的键,而v属性则保存着键值对中的值,其中简直对的值可以时一个指针,或者一个uint64_t整数,next指向下个哈希结点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,解决哈希冲突问题

redis 设置值的时候报对象无法转化成字符串_字符串_05

4.1.3字典

typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引
    //当rehash不在进行时,值为-1
    int rehashidx;
} dict;

type和privdata属性是针对不同类型的键值对,为丰富键值对的使用场景而设置的。

  • type属性是一个指向dictType的结构指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis为用途不同的字典设置不同类型特定函数。
  • redis 设置值的时候报对象无法转化成字符串_字符串_06

  • privdata属性保存了需要传给那些类型特定函数的可选参数。
  • ht属性是包含两个项的数组,每项都是一个哈希表,ht[0]平时使用,而ht[1]仅在rehash时使用。
  • rehashidx记录了rehash的进度,初始为-1。

4.2哈希算法

Redis计算哈希值和索引值的方法:

Redis计算哈希值方法: hash=dict->type->hashFunction(key); 计算索引值的方法:index=hash & dict->ht[x].sizemask;

4.3解决键冲突

Redis使用链地址法来解决键冲突问题,没个哈希表结点都有一个next指针,多个哈希表可以用next来构成一个单向链表,被分配到同一个索引上的多个结点可以用这个单向链表连接起来

redis 设置值的时候报对象无法转化成字符串_键值对_07


redis 设置值的时候报对象无法转化成字符串_开发语言_08

4.4 rehash

为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行响应的扩容或缩容。扩容和缩容通过执行rehash来完成,Redis中重新散列的步骤如下:

  1. 为字典ht[1]哈希表分配空间,大小取决于要执行的操作与ht[0]当前键值对的数量
  2. 将保存在ht[0]中的所有键值对存放到ht[1]指定的位置
  3. 当ht[0]的所有键值对都迁移完毕后,释放ht[0],并指向ht[1],并在ht[1]上创建一个空的哈希表,为下次rehash准备。

哈希表的扩展和收缩

扩容操作场景:

  • 服务器目前没有在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子>=1
  • 服务器正在执行BGSAVE命令或BGREWRITEAOF命令,并且哈希表的负载因子>=5

负载因子=哈希表已存储节点数/哈希表大小:load_factor=ht[0].used/ht[0].size

4.5渐进式rehash

rehash时会将ht[0]中所有的键值对rehash到ht[1],如果键值对很多并且一次性操作的话,容易导致服务器在一段时间内停止服务。为避免这种情况,Redis采用渐进式rehash,将ht[0]中的键值对分多次,慢慢的rehash到ht[1]之中。

步骤:

  1. 为ht[1]分配空间,让字典同时持有两个哈希表。
  2. 在字典中维持一个索引计数器变量rehashidx,将其设置为0,表示rehash正式开始。
  3. 在rehash进行期间,每次对字典进行添加,删除,查找或更新操作时,程序除了执行指定的操作外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成后,将rehashidx++。
  4. 某个时刻,ht[0]中的所有键值对都被rehash至ht[1],此时设置rehashidx=-1时,表示rehash操作已经完成。

渐进式rehash执行期间的哈希表操作

在rehash的期间,字典同时使用ht[0],ht[1]两个哈希表。对哈希表的操作会在两个表上进行,比如查找键时,先在ht[0]里面查找,如果为空,就继续到ht[1]里查找。在此期间,新增的键值对都会被添加到ht[1]中,ht[0]不承担任何添加操作,保证ht[0]中的键值对只能是越来越少

第5章跳跃表

跳跃表是一种有序数据结构,它通过在每个结点中维持多个指向其他结点的指针,从而达到快速访问结点的目的。

如果一个有序集合中包含的元素数量比较多,又或者有序集合中元素的成员是较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。Redis只有在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中作为内部数据结构。

5.1跳跃表的实现

typedef struct zskiplist{
    //表头节点和表尾节点
    structz zskiplistNode *header,* tail;
    //表中节点的数量
    unsigned long length;
    //表中层数最大的节点的层数
    int level;
} zskiplist;

redis 设置值的时候报对象无法转化成字符串_键值对_09


header:指向跳跃表的表头节点

tail:指向条约的标为节点

level:记录目前条约表内,层数最的那个节点的层数(表头节点的层数不计算在内) 层,用L1、L2、L3……

length:记录跳跃表的长度

backward指针:节点中,用BW字样标记节点的后退指针,他指向位于当前节点的前一个节点。

分值:各个节点中的1.0、2.0和3.0是节点所保存的分支

成员对象:各个节点中的o1、o2和o3是节点所保存的成员对象

5.1.1跳跃表节点

typedef strct zskiplistNode{
    //后退指针
    struct zskiplistNode *backward;
    //分值
    double score;
    //成员对象
    robj *obj;
    //层
    struct zskiplistlevel{
        //前进指针
        struct zskiplistNode *forward;
        //跨度
        unsigned int span;
    }level[];
} zskiplistNode;

1、层:跳跃表节点的level数组可以包含多个元素,每个元素豆瓣含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。

2、前进指针:每个层都有一个指向表尾的前进指针,用于从表头向表尾方向访问节点

3、跨度:用于记录记录连个节点之间的距离

4、后退指针:用于从表尾向表头方向表头方向访问节点

5、分值和成员 分值是一个double类型的浮点数,跳跃表中的所有节点都按分支从小到大来排序

5.1.2跳跃表

考多个跳跃表节点可以组成一个跳跃表

typedef struct zskiplist{
	struct skiplistNode *tail;
	unsigned long length;
	int level;
}zskiplist;

header和tail指针分别指向跳跃表的表头和表尾节点

第6章 整数集合

整数集合(insert)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素不多时,Redis就会使用整数集合作为集合键底层实现。

6.1整数集合的实现

typedef struct intset{
    //编码方式
    uint32_t encoding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

redis 设置值的时候报对象无法转化成字符串_java_10


contents数组时整个这的底层实现,整数集合的每个元素都是contents数组的一个数组项(item)各个项在数组中按值得大小从小到大有序地排列,并给数组中不包含任何重复项

length属性记录了整数集合包含地元素数量,即contents数组地长度。

encoding属性的值Intest_enc_int16,那么contents就是一个init16_t类型的值,数组真正类型取决于encoding属性的值。

6.2升级

当我们要将一个新元素添加至集合时,并且新元素的类型比现有集合类型都长时,整数集合就要升级。

步骤:

  1. 根据新元素类型,扩展数组空间,为新元素分配空间。
  2. 将底层数组现有所有元素都转为新元素相同类型,并将类型转换后的元素放到正确位置。
  3. 将新元素添加到底层数组。

由于每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中已有元素进行类型转换,所以添加的时间复杂度为O(N)

6.3升级的好处

6.3.1提升灵活性

随意将16位、32位64位整型数添加到集合中,而不必担心出现类型错误。

6.3.2节约空间

既可以同时保存三种不同类型的值,又可以升级操作只会在有需要的时候进行,尽可能节省空间。

6.4降级

整数集合不支持降级操作,一旦对数组进行了升级,编码会一直保持升级后的状态。

第7章压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一,当一个列值键只包含少量列表项,并且每个列表要么是小整数值,要么是长度较短的字符串,Redis会使用压缩列表做列表键实现

7.1压缩列表的构成

是为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序性数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数或者一个整数值。

压缩列表的组成

redis 设置值的时候报对象无法转化成字符串_字符串_11

zlbytes:记录整个压缩列表占用的内存字节数,4字节

zltail:记录压缩列表尾节点距离压缩列表起始地址有多少字节,通过这个偏移量,程序无需变量整个压缩列表即可确定表尾节点的地址

zllen:记录压缩列表包含的节点数量,小于65532时,属性值就是压缩列表好汉节点的数量,大于65532时,节点的真实数量需要遍历整个压缩列表才能计算得到

entryX:压缩列表包含的各个节点,节点长度有节点保存的内容决定。

zlend,用于标记末端

7.2压缩列表节点的构成

每个压缩列表节点可以是一个字节数组,也可以是一个整数。由previous_entry_length,encoding,content组成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdyvWkDA-1646302504101)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211126092404620.png)]

7.2.1previous_entry_length

previous_entry_length属性以字节问单位,记录了压缩列表在前一个节点的长度,可以是1字节或者5字节。

  • 前一个节点的长度<254字节时,该属性只有2位,且前一节点的长度就保存在这两位。如0x05,表示前一个字节长度为5字节。
  • 前一个节点的长度>=254字节时,该属性有10位,且前两位表示这是一个5字节的长度,后8位表示前一个节点的长度。如0xFE0000,表示前一个字节长度为0x00002766,换算为10进制就是我们熟悉的数字。

7.2.2encoding

记录了节点的content属性所保存数据的类型长度;

一字节、两字节或者五字节长,值得最高位为00,01或者10得是字节数组编码。

一字节长,值得最高位以11开头得是整数编码:这种编码表示节点得content属性保存着整数值,整数值得类型和长度由编码出去最高两位之后得其他位记录。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7WhOcN9-1646302504101)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211126093846381.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4XAu3yTm-1646302504102)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211126094333467.png)]

7.2.3content

节点得content属性负责保存节点得值,节点值可以是一个自己数组或者整数,值得类型和长度由节点encoding属性决定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iyAM0eJE-1646302504102)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211126094704791.png)]

7.3连锁更新

多个连续的长度介于250字节到253字节之间的节点,插入新的头节点(长度大于等于245字节),后面节点的previous_entry_length就要新增4字节的空间(1字节变成5字节),需要进行内存重分配,由于前一个节点的变更,每个节点的previous_entry_length属性也需要记录之前的长度而发生相应的变更,所以会出现连锁更新。除了新增节点,删除节点也可能会遇到这种情况。

因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,每次重分配的的最坏时间复杂度为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3b53lRyW-1646302504102)(https://www.zhihu.com/equation?tex=O%28N%29)] ,所以连锁更新的最坏时间复杂度为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U40QMQl8-1646302504102)(https://www.zhihu.com/equation?tex=O%28N%5E2%29)]

虽然代价很高,但是出现的几率比较低,而且只要更新节点的数量不多,就不会对性能产生影响。因此ziplistPush命令的平均复杂度为 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1CHAvuWF-1646302504103)(https://www.zhihu.com/equation?tex=O%28N%29)] 。

第8节对象

8.1对象得类型和编码

Redis每个对象都是由一个redisObject结构表示,该结构中有关得三个属性分别是唐渝鹏属性、encoding和ptr属性

typedef struct redisObject{
    //类型
    unsigned type :4;
    //编码
    unsigned encoding:4;
    //指向底层实现数据结构的指针
    void *ptr;
    ...
} robj;

8.1.1类型

redis 设置值的时候报对象无法转化成字符串_开发语言_12

8.1.2编码和底层实现

redis 设置值的时候报对象无法转化成字符串_开发语言_13


每种类型都至少使用了两种不同得编码

redis 设置值的时候报对象无法转化成字符串_java_14

Redis对象采用encoding属性来设置编码,从而决定底层数据结构,而不是为特定类型的对象关联一种固定编码。这种方式极大地提高了灵活性和效率。

8.2字符串对象

字符串编码可以是 int、raw、embstr

  • 如果字符串对象保存的是整数值,且这个数值可用long表示,底层就会以**REDIS_ENCODING_INT**编码来实现。
  • 如果字符串对象是一个字符串值,且这个字符串长度**>39字节**,字符串将使用一个SDS保存,底层编码为**REDIS_ENCODING_RAW**。
  • 如果字符串对象保存的是字符串,且这个字符串长度**<=39字节**,底层编码就是**REDIS_ENCODING_EMBSTR**,使用embstr编码的方式保存字符串。

redis 设置值的时候报对象无法转化成字符串_字符串_15

8.2.1编码得转换

int编码得字符串对象和embstr编码的字符串对象在条件满足得的情况下,会被转换成raw的字符串对象

eg 在int对象后面加上字符串 对象类型由int转变成raw

8.2.2字符串命令的实现

set key value 将字符串值 value 关联到 key 。如果 key 已经持有其他值, SET 就覆写旧值, 无视类型。当 SET 命令对一个带有生存时间(TTL)的键进行设置之后, 该键原有的 TTL 将被清除。

get key 返回与键 key 相关联的字符串值

append key value 如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。如果 key 不存在, APPEND 就简单地将键 key 的值设为 value , 就像执行 SET key value 一样。

incrbyfloat key increment 为键 key 储存的值加上浮点数增量 increment 。float 型

(decrby)incrby key increment 为键 key 储存的数字值加上增量 increment 。int型

setrange key offset value 从偏移量 offset 开始, 用 value 参数覆写(overwrite)键 key 储存的字符串值。

getrange key start end 返回键 key 储存的字符串值的指定部分, 字符串的截取范围由 startend 两个偏移量决定 (包括 startend 在内)。

8.3列表对象

列表对象的编码可以是ziplist或者linkedlist

ziplist编码的对象

redis 设置值的时候报对象无法转化成字符串_java_16


linkedlist编码对象

redis 设置值的时候报对象无法转化成字符串_后端_17

8.3.1编码转换

当列表可以同时满足以下两个条件时,列表对象使用**ziplist编码****:

  • 列表对象保存的所有字符串元素的长度都**<64字节**
  • 列表对象保存的元素数量**<512个**

8.3.2列表命令的实现

LPUSH key value [value …] 将一个或多个值 value 插入到列表 key 的表头

RPUSH key value [value …] 将一个或多个值 value 插入到列表 key 的表尾(最右边)。

LPOP key 移除并返回列表 key 的头元素。

RPOP key 移除并返回列表 key 的尾元素。

LINDEX key index 返回列表 key 中,下标为 index 的元素。

LINSERT key BEFORE|AFTER pivot value 将值 value 插入到列表 key 当中,位于值 pivot 之前或之后。

LSET key index value 将列表 key 下标为 index 的元素的值设置为 value

LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

8.4哈希对象

对象编码可以是ziplist或者hashtable

ziplist编码对象

redis 设置值的时候报对象无法转化成字符串_后端_18


redis 设置值的时候报对象无法转化成字符串_字符串_19


hashtable编码

redis 设置值的时候报对象无法转化成字符串_开发语言_20

8.4.1编码转换

当列表可以同时满足以下两个条件时,列表对象使用ziplist编码

  • 哈希对象保存的所有字符串元素的长度都**<64字节**
  • 哈希对象保存的元素数量**<512个**

8.4.2哈希表实现

HSET hash field value 将哈希表 hash 中域 field 的值设置为 value

HGET hash field 返回哈希表中给定域的值。

HEXISTS hash field 检查给定域 field 是否存在于哈希表 hash 当中。

HDEL key field [field …] 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。

HLEN key 返回哈希表 key 中域的数量。

HGETALL key 返回哈希表 key 中,所有的域和值。

8.5集合对象

集合对象的编码可以是intset或者hashtable

initset编码集合对象

redis 设置值的时候报对象无法转化成字符串_后端_21


hashtable编码集合对象

redis 设置值的时候报对象无法转化成字符串_后端_22

8.5.1编码的转换

当列表可以同时满足以下两个条件时,列表对象使用ziplist编码

  • 集合对象保存的所有字符串元素的长度都**<64字节**
  • 集合对象保存的元素数量**<512个**

8.5.2集合命令的实现

SADD key member [member …] 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。

假如 key 不存在,则创建一个只包含 member 元素作成员的集合。

SCARD key 返回集合 key 的基数(集合中元素的数量)。

SISMEMBER key member 判断 member 元素是否集合 key 的成员。

SRANDMEMBER key [count] 如果命令执行时,只提供了 key 参数,那么返回集合中的一个随机元素。从 Redis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数:

SPOP key 移除并返回集合中的一个随机元素。如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 [SRANDMEMBER key count] 命令。

SREM key member [member …] 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。

8.6有序集合对象

有序对象的编码为ziplist和skiplist

ziplist编码:

redis 设置值的时候报对象无法转化成字符串_字符串_23

typedef struct zset{
    zskiplist *zsl;
    dict *dict;
} zset;

skiplist编码

redis 设置值的时候报对象无法转化成字符串_开发语言_24

8.6.1编码的转换

当列表可以同时满足以下两个条件时,列表对象使用ziplist编码

  • 有序集合对象保存的所有字符串元素的长度都**<64字节**
  • 有序集合对象保存的元素数量**<128个**

8.6.2有序集合命令的实现

ZADD key score member [[score member] [score member] …] 将一个或多个 member 元素及其 score 值加入到有序集 key 当中

ZSCORE key member 返回有序集 key 中,成员 memberscore 值。

ZCARD key 当 key 存在且是有序集类型时,返回有序集的基数。 当 key 不存在时,返回 0

ZCOUNT key min max 返回有序集 key 中, score 值在 minmax 之间(默认包括 score 值等于 minmax )的成员的数量。

ZRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。其中成员的位置按 score 值递增(从小到大)来排序。

ZREVRANGE key start stop [WITHSCORES] 返回有序集 key 中,指定区间内的成员。

ZRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。

ZREM key member [member …] 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。

8.7类型检查与命令多态

del expire、rename、type、object适用于任意类型

以下命令只适用于单一类型:

redis 设置值的时候报对象无法转化成字符串_开发语言_25

8.7.1 类型检查的实现

为了确保只有制定类型的键可以执行某些特定命令,在执行前,Redis会先通过RedisObject的type属性检查输入键的类型是否正确。

8.7.2 多态命令的实现

Redis除了根据值对象判断键是否能够执行制定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行。比如基于编码的多态,列表对象的编码可能是ziplist或linkedlist,所以需要多态命令执行对应编码的API。基于类型的多态是一个命令可以同时处理多种不同类型的键

8.8内存回收

构建了引用计数法技术实现内存回收机制。每个对象的引用计数器信息由redisObject的refcount来记录。当对象的引用计数值为0时,所占用的内存会被释放

8.9对象共享

引用计数器还有共享对象的作用。如果两个不同键的值都一样(必须是整数值的字符串对象),则将数据库键的值指针指向一个现有的值对象,然后将被共享对象的引用计数加一。如果不是整数值的对象,则需要耗费大量的时间验证共享对象和目标对象是否相同,复杂度较高,消耗CPU时间,所以Redis不会共享包含字符串的对象

redis 设置值的时候报对象无法转化成字符串_java_26

Redis会共享值为0到9999的字符串对象。

8.10对象的空转时长

redisObject还包含了lru属性记录对象最后一个被命令程序访问的时间object idletime命令可打印键的空转时长,就是当前时间减去lru时间计算得到的。