redis中用一种名为简单动态字符串(simple dynamic string)的抽象类型作为默认字符串表示,如在命令set name sher
中,在redis数据中创建了新的键值对 name-sher
,而在底层就是存储了保存着name
的SDS和sher
的SDS,除了保存redis数据库中的字符串值外,SDS还用作缓冲区(AOF持久化中的AOF缓冲区、客户端中的输入缓冲区都是由SDS实现),SDS底层是这样的数据结构:
struct sdshdr {
// buf 中已占用空间的长度 等于SDS所保存的字符串长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
也就是说与传统的字符串相比,SDS由于在len
属性中记录了其存储的字符串长度,所以获取字符串长度时不需要遍历整个字符串,而是直接获取其len
属性即可,所以获取SDS存储的字符串的长度时间复杂度为O(1)(而获取C字符串的长度时间复杂度为O(n)),而在SDD中也是由\0
作为字符串的结束:
在SDS中,存储数据的buf
数组不一定是字符的数量加一(加一为\0
),而是还包括未使用的字节,这些字节的长度由free
属性记录。
C字符中出现的问题:
由于C字符串中不记录自己的长度,所以会出现内存溢出和内存泄漏:
SDS中的解决方案:
空间预分配:该策略用于优化字符串的增长操作,其策略用伪代码表示为:
if (len < 0.5MB)
buf.length = buf.length + len
else
buf.length = buf.length + 1MB
也就是如果对SDS进行修改之后,其长度若小于1MB,则分配给和len
属性同样大小的未使用空间,(分配后SDS中len
属性的值和free
值相同);如果对SDS进行修改后,其长度将大于1MB,则分配个1MB的空间。也就是在扩展SDS的buf
数组前,会先检查未使用空间是否够用,够用的话就不会执行内存分配了,这样就减少了内存分配的次数。
惰性空间释放:该策略用于优化字符串的缩短操作,当要缩短SDS字符串时,并不会立即回收缩短后多出来的字节,而是将这些空出来的字节用free
属性记录下来,并等待以后使用。
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;
通过上面的图例以及数据结构的源码我们可以知道Redis中的双端链表具有以下性质:
prev
指针和表尾节点的next
指针都指向null
,链表的访问以null
为终点;list
结构中定义),获取头结点和尾结点的时间复杂度为O(1);list
结构中的len
字段),获取链表节点数量的时间复杂度为O(1);value
字段为一个指针,所以链表实际可以存储各种数据,即数据多态性。字典(映射、符号表、关联数组)就是一种保存键值对key-value
的抽象数据结构,在Redis中其应用较为广泛,比如Redis中的数据库就是使用字典来作为底层实现的,因为Redis中的每条记录本身就是个键值对;除了用字典表示数据库外,当一个哈希键key
包含的键值对field-value
较多时,Redis也会使用字典作为hash
的底层实现。首先我们看在普通状态下的字典结构如下:
我们发现在字典中存在两个哈希表,通常情况下,我们只会用ht[0]
这个哈希表,那么ht[1]
是用来做什么的?ht[1]
哈希表只会在对ht[0]
哈希表进行rehash
时使用。而在哈希表中存放了哈希表节点的数组,而哈希表节点的数组存放了每个哈希表节点,在哈希表节点中就是真正的键值对数据,这几个数据结构在Redis底层源码实现如下:
/*
* 字典
*/
typedef struct dict {
dictType *type;// 类型特定函数,如计算哈希值,复制、对比键值的函数等
void *privdata;// 私有数据
dictht ht[2];// 哈希表
int rehashidx; /* 当 rehash 不在进行时,值为 -1 */
} dict;
对于字典,type
和privdata
属性是为创建多态字典有关的,这里不详细进行介绍,而**dictth ht[2]
字段恰恰就是存储了用来存储键值对数据的哈希表和用来rehash
的哈希表**,rehashidx
字段在注释中已经说明其用途。
/*
* 哈希表
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
dictEntry **table;// 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
unsigned long used;// 该哈希表已有节点的数量
} dictht;
对于哈希表,其sizemask
和哈希表节点计算出的哈希值共同决定了一个键应该被放到table
数组的哪个索引上。
/*
* 哈希表节点
*/
typedef struct dictEntry {
void *key; // 键
union { // 值为以下三种结构的之一
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表,解决哈希冲突
} dictEntry;
阅读其源码可知,在Redis中,键值对的值可以是一个指针,也可以是一个uint64_t
整数,也可以是一个int64_t
整数,而在哈希表节点中有一个**next
指针,只是为了将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突问题**。
当将一个新值添加到字典结构中时,程序根据要加入键值对的键计算出哈希值和索引值,然后再根据索引值,将新的哈希表节点(即要加入的键值对)放到哈希表数组(ht[0]
或ht[1]
,根据是否在rehash)指定索引上。即如下的计算哈希值和索引值的方法:
# 使用字典type字段存储的计算哈希值的函数计算出哈希值
hash = dict->type->hashFunction(key); //作为hash键底层时,用MurmurHash2算法
# 使用哈希表中的sizemask属性和哈希值,计算出索引值
index = hash & dict->ht[x].sizemask; //x可能为0或1
而当字典被用作数据库的底层实现或者哈希键的底层实现时,Redis采用**MurmurHash2
算法**计算哈希值,其在Redis的源码如下:
static uint32_t dict_hash_function_seed = 5381;
/* MurmurHash2, by Austin Appleby
* 该算法假设机器如下属性成立:
* 1. 可以从任何地址读取4字节的值而不会崩溃
* 2. sizeof(int) == 4,即int类型的大小为4字节
*
* 该算法具有如下的限制:
* 1. 不会以增量方式工作
* 2. 不会在小端和大端机器上产生相同的结果。
*/
unsigned int dictGenHashFunction(const void *key, int len) {
/* 'm' and 'r' 不是魔数,只是该算法发明者发现这两个数刚好凑效 */
uint32_t seed = dict_hash_function_seed; // 5381
const uint32_t m = 0x5bd1e995;
const int r = 24;
/* 将哈希值初始化为随机值 */
uint32_t h = seed ^ len;
/* 一次将4个字节混合到哈希中 */
const unsigned char *data = (const unsigned char *)key;
while(len >= 4) {
uint32_t k = *(uint32_t*)data;
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
/* 处理输入数组的最后几个字节 */
switch(len) {
case 3: h ^= data[2] << 16;
case 2: h ^= data[1] << 8;
case 1: h ^= data[0]; h *= m;
};
/*对散列做一些最后的混合,以确保最后几个字节被很好地合并在一起 */
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return (unsigned int)h;
}
上面的这种MurmurHash2
算法即使在输入的键是有规律的情况下,仍能给出很好的随机分布性,较好的解决了哈希冲突的频率。而当两个或两个以上键被分配到到哈希表数组同一个索引上时,Redis使用链地址法解决键冲突。
首先说明在哈希表中的负载因子如何计算:
负 载 因 子 = 哈 希 表 中 已 经 保 存 的 节 点 数 量 / 哈 希 表 数 组 的 大 小 负载因子 = 哈希表中已经保存的节点数量 / 哈希表数组的大小 负载因子=哈希表中已经保存的节点数量/哈希表数组的大小
即:
load_factor = ht[0].used / ht[0].size //负载因子 = 哈希表中已经保存的节点数量 / 哈希表数组的大小
有以下两种情况会触发哈希表的扩容操作:
BGSAVE
或者BGREWRITEAOF
命令时,且哈希表的负载因子≥1,扩容;BGSAVE
或者BGREWRITEAOF
命令时,且哈希表的负载因子≥5,扩容。而对于哈希表的收缩操作:当哈希表的负载因子≤0.1时触发收缩操作。
对于扩容操作的具体源码如下:
/*
* 创建一个新的哈希表或是对一个已经存在的哈希表扩容,并根据字典的情况,选择以下其中一个动作来进行:
* 1) 如果字典的 0 号哈希表为空,那么将新哈希表设置为 0 号哈希表
* 2) 如果字典的 0 号哈希表非空,那么将新哈希表设置为 1 号哈希表,并打开字典的 rehash 标识,使得程序可以开始对字典进行 rehash
*
* size 参数不够大,或者 rehash 已经在进行时,返回 DICT_ERR 。
*
* 成功创建 0 号哈希表,或者 1 号哈希表时,返回 DICT_OK 。
*/
int dictExpand(dict *d, unsigned long size)
{
dictht n; /* 新哈希表 */
unsigned long realsize = _dictNextPower(size);//返回第一个 ≥ 2倍size 的 2的n次方幂
// 不能在字典正在 rehash 时进行,size 的值也不能小于 0 号哈希表的当前已使用节点
if (dictIsRehashing(d) || d->ht[0].used > size)
return DICT_ERR;
// 为哈希表分配空间,并将所有指针指向 NULL
n.size = realsize;
n.sizemask = realsize-1; //前面提到过的哈希表的sizemask字段永远比其size少1
n.table = zcalloc(realsize*sizeof(dictEntry*));
n.used = 0; //新哈希表中数据数量为0
// 如果 0 号哈希表为空,那么这是一次初始化:
// 程序将新哈希表赋给 0 号哈希表的指针,然后字典就可以开始处理键值对了。
if (d->ht[0].table == NULL) {
d->ht[0] = n;
return DICT_OK;
}
// 如果 0 号哈希表非空,那么这是一次 rehash :
// 程序将新哈希表设置为 1 号哈希表,
// 并将字典的 rehash 标识打开,让程序可以开始对字典进行 rehash
d->ht[1] = n;
d->rehashidx = 0;
return DICT_OK;
}
/* 计算第一个大于等于 size 的 2 的 N 次方,用作哈希表的新容量 */
static unsigned long _dictNextPower(unsigned long size)
{
unsigned long i = DICT_HT_INITIAL_SIZE; // 定义为4
if (size >= LONG_MAX) return LONG_MAX;
while(1) {
if (i >= size)
return i; // i为第一个 ≥ 2倍size 的 2的n次方幂
i *= 2;
}
}
即当进行扩容时,新的哈希表的大小为第一个大于等于"原来哈希表存储元素数量×2"的2^n(2的n次幂);
而对于收缩操作,新的哈希表的大小为第一个大于等于"原来哈希表存储元素数量"的2^n(2的n次幂)。
上述对于哈希表的扩容和收缩操作时通过执行rehash
操作完成的,对于字典中的哈希表,rehash步骤如下:
ht[1]
分配空间,ht[1]
的空间大小取决于是扩容还是收缩操作以及ht[0]
的键值对数量(即ht[0].used
):
ht[1].size
= 第一个≥ht[0].used * 2
的2^n
;ht[1].size
= 第一个≥ht[0].used
的2^n
。ht[0]
中的键值对rehash到ht[1]
中,即重新计算键的哈希值和索引值,将数据重新映射到ht[1]
中的新位置上;ht[0]
上的数据都重新rehash到ht[1]
后,释放ht[0]
,令ht[1]
成为新的ht[0]
,然后为新ht[0]
创建一个新的空白ht[1]
用作下次rehash。而一个哈希表中的数据可能有几百上千万,不可能一次rehash转移完,所以在Redis中采用渐进式rehash——在rehash的过程中:
对数据库的查询、更新操作首先会在ht[0]
中查找,没有找到,然后转到ht[1]
中操作;
对于插入操作,直接插入到ht[1]
中,而不再向ht[0]
中添加任何数据;
其渐进式rehash的源码如下:
/*
* 执行 N 步渐进式 rehash 。
*
* 返回 1 表示仍有键需要从 0 号哈希表移动到 1 号哈希表,
* 返回 0 则表示所有键都已经迁移完毕。
* 每步 rehash 都是以一个哈希表索引(桶)作为单位的,
* 一个桶里可能会有多个节点,
* 被 rehash 的桶里的所有节点都会被移动到新哈希表。
*/
int dictRehash(dict *d, int n) {
// 只可以在 rehash 进行中时执行
if (!dictIsRehashing(d)) return 0;
// 进行 N 步迁移
while(n--) {
dictEntry *de, *nextde;
// 如果 0 号哈希表为空,那么表示 rehash 执行完毕
if (d->ht[0].used == 0) {
zfree(d->ht[0].table); // 释放 0 号哈希表
d->ht[0] = d->ht[1]; // 将原来的 1 号哈希表设置为新的 0 号哈希表
_dictReset(&d->ht[1]); // 重置旧的 1 号哈希表
d->rehashidx = -1; // 关闭 rehash 标识
return 0; // 返回 0 ,向调用者表示 rehash 已经完成
}
assert(d->ht[0].size > (unsigned)d->rehashidx); // 确保 rehashidx 没有越界
// 略过数组中为空的索引,找到下一个非空索引
while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
de = d->ht[0].table[d->rehashidx] // 指向该索引的链表表头节点
// 将链表中的所有节点迁移到新哈希表
while(de) {
unsigned int h;
nextde = de->next; // 保存下个节点的指针
// 计算新哈希表的哈希值,以及节点插入的索引位置
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
// 插入节点到新哈希表
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
// 更新计数器
d->ht[0].used--;
d->ht[1].used++;
// 继续处理下个节点
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;// 将刚迁移完的哈希表索引的指针设为空
d->rehashidx++; // 更新 rehash 索引
}
return 1;
}
属性 | 长度 | 说明 |
---|---|---|
zlbytes | 4字节 | 记录整个压缩列表占用的内存大小 |
zltail | 4字节 | 记录压缩列表尾结点举例压缩列表的起始地址有多少字节 |
zllen | 2字节 | 记录压缩列表包含的节点数量 |
entryX | 不定 | 压缩列表节点,即存储的具体内容 |
zlend | 1字节 | 特殊值0xFF (255),用于标记压缩列表的末端 |
在压缩列表中,存储实际的数据是由压缩列表节点,即上图中的entry
属性完成的,而每个entry
的组成部分如下:
其中各组成部分的含义如下:
属性 | 说明 |
---|---|
previous_entry_length | 记录压缩列表中前一个节点的长度,其长度为1或5字节(若前一个节点长度小于254字节,则为1字节,如0x05 就表示前一个节点长度为5;若前一个节点长度大于254字节,则为5字节,且第一字节设置为0xFE ,然后后4个字节为前一个节点的长度,如0xFE00002766 就表示前一个节点长度为10086) |
encoding | 记录节点的content 属性保存的数据类型以及长度 |
content | 保存节点的值(可以是字节数组或者整数) |
由于每个节点记录了上一个节点的长度,所以可以通过当前节点的起始地址来计算出前一个节点的起始地址,即前一个节点起始地址 = 当前节点起始地址 - previous_entry_length
,这样就可以实现压缩列表从表尾向表头的遍历。
作为底层的数据结果,hash
结构在存储少量数据时选择压缩列表作为底层实现,list
结构在包含少量列表项时,也是由压缩列表作为底层实现的。
当一组集合键只包含整数值元素,并且这个集合的元素数量不多时,set
就由整数集合这种数据结构实现。下面展示了一个intset
的存储结构:
contents
数组是整数集合的底层实现,intset
存储的每一项数据都是contents
数组中的一项,contents
数组中的数值按照从小到大排列,且不含重复元素,其存储的数据类型由encoding
属性决定,而length
属性记录了存储数据的数量,即contents
数组的长度,其源码如下:
typedef struct intset {
uint32_t encoding;// 编码方式
uint32_t length;// 集合包含的元素数量
int8_t contents[];// 保存元素的数组
} intset;
下面展示了存储3个数据的一个跳跃表:
一个跳跃表由两个数据结构所定义,分别是zskiplist
以及zskiplistNode
,其数据结构源码如下:
/* 跳跃表 */
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
/* 跳跃表节点 */
typedef struct zskiplistNode {
robj *obj; // 成员对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel { // 层
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
} zskiplistNode;
Redis利用上面介绍的数据结构实现了一个对象系统,这个对象系统包含了Redis数据库的五大基本类型,即字符串对象、列表对象、哈希对象、集合对象、有序集合对象,也就是Redis使用对象来表示数据库中的键和值,每创建一个键值对(即一条数据)时,数据库至少会创建两个对象,一个用作存储键(总是字符串对象),一个用来存储值(即五大基本类型),而Redis中的每个对象其结构如下:
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
unsigned lru:REDIS_LRU_BITS; // 对象最后一次被访问的时间
int refcount; // 引用计数
void *ptr; // 指向实际值的指针
} robj;
type
字段记录了对象的类型,其值即五大基本类型,包括:
类型常量(即type 的取值) |
对象名称 | TYPE 命令的输出 |
---|---|---|
REDIS_STRING | 字符串对象 | string |
REDIS_LIST | 列表对象 | list |
REDIS_HASH | 哈希对象 | hash |
REDIS_SET | 集合对象 | set |
REDIS_ZSET | 有序集合对象 | zset |
encoding
字段记录了对象使用的编码,即该对象由哪一个底层数据结构实现,其值即底层的数据结构,包括:
编码常量(即encoding 的取值) |
底层数据结构 | 可实现的对象 | OBJECT ENCODING 命令输出 |
---|---|---|---|
REDIS_ENCODING_INT | long类型整数 | string | int |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 | string | embstr |
REDIS_ENCODING_RAW | 简单动态字符串 | string | raw |
REDIS_ENCODING_HT | 字典 | hash,set | hashtable |
REDIS_ENCODING_LINKEDLIST | 双端列表 | list | linkedlist |
REDIS_ENCODING_ZIPLIST | 压缩列表 | list,hash,zset | ziplist |
REDIS_ENCODING_INTSET | 整数集合 | set | intset |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 | zset | skiplist |
refcount
字段即引用计数(reference counting)用来记录对象是否要被内存回收:
string类型的通用操作:
命令 | 说明 |
---|---|
set key value | 添加/修改数据 |
get key | 获取数据 |
del key | 删除数据 |
mset key1 value1 key2 value2 … | 添加/修改多个数据 |
mget key1 key2 … | 获取多个数据 |
strlen key | 获取数据字符个数(字符串长度) |
getset key value | 修改原来key的对应值,并将旧值返回 |
getrange key start end | 获取子串,若获取整个字符串则start=0,end=-1 |
append key value | 追加信息到原始信息后部 |
setex key seconds value | 设置数据具有指定的生命周期,单位为秒 |
psetex key milliseconds value | 设置数据具有指定的生命周期,单位为毫秒 |
命令 | 说明 |
---|---|
incr key | 在原字段上加1 |
incrby key increment | 在原字段上加上整数increment |
increbyfloat key increment | 在原字段上加上浮点数increment |
decr key | 在原字段上减1 |
decrby key increment | 在原字段上减去整数increment |
值 | 编码 |
---|---|
可以用long 类型保存的整数 |
int |
可以用long double 类型保存的浮点数 |
embstr或raw |
字符串值,或因长度太大无法用long 类型表示的整数,或因长度太大无法用long double 表示的浮点数 |
embstr或raw |
在底层数据结构中讲过简单动态字符串的结构(即raw
编码),而embstr
是专门用于保存短字符串的优化编码方式,它和raw
一样底层由redisObject
结构和sdshdr
结构来表示字符串对象,其区别如下:
而至于int
编码,只不过是在redisObject
中,其ptr
指向了一个整数而已。
int
编码的字符串对象和embstr
编码的字符串对象在条件满足时会转换成raw
编码的字符串对象,转换规则如下:
int
,当向对象执行了一些命令,使对象保存的不再是整数值,则编码由int
变为raw
;embstr
,Redis并为为其编写任何修改程序(只有int
和raw
可以修改),所以对embstr
编码的字符串对象执行任何修改命令时,其会变为raw
编码的字符串对象;raw
编码的字符串对象存储。list常用的基本操作:
命令 | 说明 |
---|---|
lpush key value1 [value2] … | 把节点加入到链表最左边 |
rpush key value1 [value2] … | 把节点加入到链表最右边 |
lindex key index | 读取下标为index的节点(从0开始算) |
llen key | 获得链表的长度 |
lrange key start stop | 获取链表key的下标从start到end的节点值,[star, end] ,若获取所有节点可以用lrange key 0 -1 |
lpop key | 删除左边第一个节点并返回 |
rpop key | 删除右边第一个节点并返回 |
lset key index value | 设置下标为index的节点的值为value |
list还提供了阻塞命令,如下这几个命令是阻塞版本命令,比如阻塞取数据的时候,现在没有不代表以后没有,可以等!比如当前没有值,但是在timeout时间内有其他客户端添加了值,就可以取到了。
命令 | 说明 |
---|---|
blpop key1 [key2] timeout | 移除并获取第一个元素,如果列表没有元素会阻塞列表直到超时时间或有元素可以弹出 |
brpop key1 [key2] timeout | 移除并获取最后一个元素,如果列表没有元素会阻塞列表直到超时时间或有元素可以弹出 |
列表对象的底层编码可以为ziplist
和linkedlist
,即列表对象可以由 “redisObject
+ 压缩列表/双端列表” 实现,其区别如下:
注意在linkedlist
编码的列表其底层双端链表中包含了多个字符串对象,这种嵌套字符串对象的行为在哈希对象、集合对象和有序集合对象中存在,字符串对象也是五大基本类型中唯一一种被嵌套的对象(且会被其他四种对象都嵌套)。
列表对象同时满足如下两个条件时,才会使用ziplist
编码,否则使用linkedlist
编码:
上面两个条件可以通过Redis配置文件中的
list-max-ziplist-value
和list-max-ziplist-entries
选项修改。
hash类型的通用操作:
命令 | 说明 |
---|---|
hset key field value | 在hash中设置单个键值对 |
hget key field | 获取hash中指定的键的值 |
hgetall key | 获取所有hash中的键值对 |
hdel key field1 [field2 …] | 删除hash中的某个(些)字段 |
hmset key field1 value1 field2 value2 … | 在hash中设置多个键值对 |
hmget key field1 field2 … | 获取hash中指定的多个键的值 |
hlen key | 返回hash中键值对的数量 |
hexists key field | 判断hash中是否存在field字段 |
hkeys key | 获取hash中所有的字段名(键) |
hvals key | 获取hash中所有的字段值(值) |
命令 | 说明 |
---|---|
hincrby key field increment | 指定字段的数值数据加increment |
hincrbyfloat key field increment | 指定字段的数值数据加increment |
哈希对象的底层编码可以是ziplist
或hashtable
,其区别如下:
对于ziplist
编码的哈希对象,每当有新键值加入到哈希对象中时,会一次向压缩列表表尾添加保存了键的压缩列表节点和保存了值的压缩列表节点;而对于hashtable
编码的哈希对象其底层由字典实现,哈希对象的每个键值对都由字典键值对保存。
哈希对象只有同时满足下面两个条件时,才有ziplist
编码,否则由hashtable
编码:
上面两个条件可以通过Redis配置文件中的
hash-max-ziplist-value
和hash-max-ziplist-entries
选项修改。
命令 | 说明 |
---|---|
sadd key member1 [member2] | 给键为key的集合增添成员 |
smembers key | 返回集合所有成员 |
srem key member1 [member2] | 删除集合中成员 |
scard key | 获取集合中成员个数 |
sismember key member | 判断集合中是否包含指定数据 |
srandmember key [count] | 随机返回集合中一个或多个元素,count为限制返回总数,若count小于0,先对其求绝对值,若count大于集合成员数,则返回所有成员,若无count,默认返回1个 |
spop key [count] | 随机从集合中移除数据并返回,count作用同上 |
sinter key1 [key2] | 求key1和key2的交集,若无key2则返回key1的所有元素 |
sinterstore des key1 [key2] | 求key1和key2的交集并保存到des中 |
sunion key1 [key2] | 求key1和key2的并集,若无key2则返回key1的所有元素 |
sunionstore des key1 [key2] | 求key1和key2的并集并保存到des中 |
sdiff key1 [key2] | 求key1和key2的差集,若无key2则返回key1的所有元素 |
sdiffstore des key1 [key2] | 求key1和key2的差集并保存到des中 |
smove src des member | 将指定成员从原集合移动到目标集合中 |
集合对象的底层编码可以是intset
或hashtable
,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,set
就由整数集合实现,而当set
由hashtable
实现时,其底层就是类似于hash
对象一样,只不过字典中的每个键存储一个字符串对象(该字符串对象包含一个集合元素),而字典中的每个值则全部设置为NULL
,如下图所示:
当集合对象同时满足如下两个条件时,才使用intset
编码,否则使用hashtable
编码:
第二个条件可以通过Redis配置文件中的
set-max-intset-entries
选项修改。
命令 | 说明 |
---|---|
zadd key score1 member1 [score2 member2] | 添加数据 |
zrange key start stop [WITHSCORES] | 按照排序正向获取索引内的数据,可选参数带分数 |
zrevrange key start stop [WITHSCORES] | 按照排序逆向获取索引内的数据,可选参数带分数 |
zrem key member [member …] | 删除数据 |
zrangebyscore key min max [WITHSCORES] [LIMIT] | 按条件获取数据 |
zrevrangebyscore key min max [WITHSCORES] [LIMIT] | 按条件获取数据 |
zremrangebyrank key start stop | 根据排序条件删除数据 |
zremrangebyscore key min max | 根据分数值条件删除数据 |
zrank key member | 获取成员对应的索引(排名) |
zrevrank key member | 获取成员对应的倒序排名 |
zscore key member | 获取成员的分数 |
zincrby key increment member | 将成员分数加increment |
zcard key | 获取集合中成员数量 |
zcount key min max | 获取集合中分数满足条件的数量 |
有序集合对象的底层编码可以是ziplist
或skiplist
,其区别如下:
当有序集合对象同时满足如下两个条件时,才使用ziplist
编码,否则使用skiplist
编码:
上面两个条件可以通过Redis配置文件中的
zset-max-ziplist-entries
和zset-max-ziplist-value
选项修改。