Redis底层数据结构及基本类型的实现

Redis底层数据结构及基本类型的实现

底层数据结构

简单动态字符串(SDS)

数据结构

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作为字符串的结束:
Redis底层数据结构及基本类型的实现_第1张图片
在SDS中,存储数据的buf数组不一定是字符的数量加一(加一为\0),而是还包括未使用的字节,这些字节的长度由free属性记录。

C字符串的问题以及SDS的解决方案

C字符中出现的问题:

由于C字符串中不记录自己的长度,所以会出现内存溢出和内存泄漏:

  • 如果将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属性记录下来,并等待以后使用。

链表(linkedlist)

数据结构

Redis基本类型之一的链表键的实现方式之一就是链表这种数据结构,除了用来作为链表键的底层数据结构外,链表还用作**”发布与订阅“、”慢查询“、”监视器“等功能的实现,在Redis中的链表是一种双向链表**,首先看下链表底层实现的整体结构:
Redis底层数据结构及基本类型的实现_第2张图片
然后看其源码数据结构定义如下:

/*
 * 双端链表节点
 */
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字段为一个指针,所以链表实际可以存储各种数据,即数据多态性

字典(dict)

数据结构

字典(映射、符号表、关联数组)就是一种保存键值对key-value的抽象数据结构,在Redis中其应用较为广泛,比如Redis中的数据库就是使用字典来作为底层实现的,因为Redis中的每条记录本身就是个键值对;除了用字典表示数据库外,当一个哈希键key包含的键值对field-value较多时,Redis也会使用字典作为hash的底层实现。首先我们看在普通状态下的字典结构如下:
Redis底层数据结构及基本类型的实现_第3张图片
我们发现在字典中存在两个哈希表,通常情况下,我们只会用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;

对于字典,typeprivdata属性是为创建多态字典有关的,这里不详细进行介绍,而**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指针,只是为了将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突问题**。

Redis中采用的哈希算法

当将一个新值添加到字典结构中时,程序根据要加入键值对的键计算出哈希值和索引值,然后再根据索引值,将新的哈希表节点(即要加入的键值对)放到哈希表数组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

上述对于哈希表的扩容和收缩操作时通过执行rehash操作完成的,对于字典中的哈希表,rehash步骤如下:

  • 为字典的ht[1]分配空间,ht[1]的空间大小取决于是扩容还是收缩操作以及ht[0]的键值对数量(即ht[0].used):
    • 操作为扩容:ht[1].size = 第一个≥ht[0].used * 22^n
    • 操作为收缩:ht[1].size = 第一个≥ht[0].used2^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;
}

压缩列表(ziplist)

压缩列表的组成部分如下:
Redis底层数据结构及基本类型的实现_第4张图片
其中各个组成部分的含义如下:

属性 长度 说明
zlbytes 4字节 记录整个压缩列表占用的内存大小
zltail 4字节 记录压缩列表尾结点举例压缩列表的起始地址有多少字节
zllen 2字节 记录压缩列表包含的节点数量
entryX 不定 压缩列表节点,即存储的具体内容
zlend 1字节 特殊值0xFF(255),用于标记压缩列表的末端

在压缩列表中,存储实际的数据是由压缩列表节点,即上图中的entry属性完成的,而每个entry的组成部分如下:
Redis底层数据结构及基本类型的实现_第5张图片
其中各组成部分的含义如下:

属性 说明
previous_entry_length 记录压缩列表中前一个节点的长度,其长度为1或5字节(若前一个节点长度小于254字节,则为1字节,如0x05就表示前一个节点长度为5;若前一个节点长度大于254字节,则为5字节,且第一字节设置为0xFE,然后后4个字节为前一个节点的长度,如0xFE00002766就表示前一个节点长度为10086)
encoding 记录节点的content属性保存的数据类型以及长度
content 保存节点的值(可以是字节数组或者整数)

由于每个节点记录了上一个节点的长度,所以可以通过当前节点的起始地址来计算出前一个节点的起始地址,即前一个节点起始地址 = 当前节点起始地址 - previous_entry_length,这样就可以实现压缩列表从表尾向表头的遍历。

作为底层的数据结果,hash结构在存储少量数据时选择压缩列表作为底层实现,list结构在包含少量列表项时,也是由压缩列表作为底层实现的。

整数集合(intset)

当一组集合键只包含整数值元素,并且这个集合的元素数量不多时,set 就由整数集合这种数据结构实现。下面展示了一个intset的存储结构:
Redis底层数据结构及基本类型的实现_第6张图片
contents数组是整数集合的底层实现,intset存储的每一项数据都是contents数组中的一项,contents数组中的数值按照从小到大排列,且不含重复元素,其存储的数据类型由encoding属性决定,而length属性记录了存储数据的数量,即contents数组的长度,其源码如下:

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

} intset;

跳跃表(skip list)

下面展示了存储3个数据的一个跳跃表:
Redis底层数据结构及基本类型的实现_第7张图片
一个跳跃表由两个数据结构所定义,分别是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的取值) 对象名称 TYPE命令的输出
REDIS_STRING 字符串对象 string
REDIS_LIST 列表对象 list
REDIS_HASH 哈希对象 hash
REDIS_SET 集合对象 set
REDIS_ZSET 有序集合对象 zset

encoding字段

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字段——内存回收

refcount字段即引用计数(reference counting)用来记录对象是否要被内存回收:

  • 在创建一个新对象时,引用计数值被初始化为1;
  • 对象每被一个新程序使用时,其引用计数值+1;
  • 对象不再被原来使用它的一个程序使用时,其引用计数值-1;
  • 当对象的引用计数值变为0时,对象占用的内存被释放

字符串(string)

基本操作命令

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 设置数据具有指定的生命周期,单位为毫秒

Redis底层数据结构及基本类型的实现_第8张图片
string作为数字时支持的简单运算:

命令 说明
incr key 在原字段上加1
incrby key increment 在原字段上加上整数increment
increbyfloat key increment 在原字段上加上浮点数increment
decr key 在原字段上减1
decrby key increment 在原字段上减去整数increment

Redis底层数据结构及基本类型的实现_第9张图片

底层实现

编码
可以用long类型保存的整数 int
可以用long double类型保存的浮点数 embstr或raw
字符串值,或因长度太大无法用long类型表示的整数,或因长度太大无法用long double表示的浮点数 embstr或raw

在底层数据结构中讲过简单动态字符串的结构(即raw编码),而embstr是专门用于保存短字符串的优化编码方式,它和raw一样底层由redisObject结构和sdshdr结构来表示字符串对象,其区别如下:
Redis底层数据结构及基本类型的实现_第10张图片
而至于int编码,只不过是在redisObject中,其ptr指向了一个整数而已。

int编码的字符串对象和embstr编码的字符串对象在条件满足时会转换成raw编码的字符串对象,转换规则如下:

  • 对于int,当向对象执行了一些命令,使对象保存的不再是整数值,则编码由int变为raw
  • 对于embstr,Redis并为为其编写任何修改程序(只有intraw可以修改),所以对embstr编码的字符串对象执行任何修改命令时,其会变为raw编码的字符串对象;
  • 字符串值的长度大于32字节时,一定由raw编码的字符串对象存储。

下面是其转换的一些例子:
Redis底层数据结构及基本类型的实现_第11张图片

列表(list)

基本操作命令

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 移除并获取最后一个元素,如果列表没有元素会阻塞列表直到超时时间或有元素可以弹出

Redis底层数据结构及基本类型的实现_第12张图片

底层实现

列表对象的底层编码可以为ziplistlinkedlist,即列表对象可以由 “redisObject + 压缩列表/双端列表” 实现,其区别如下:
Redis底层数据结构及基本类型的实现_第13张图片
注意在linkedlist编码的列表其底层双端链表中包含了多个字符串对象,这种嵌套字符串对象的行为在哈希对象、集合对象和有序集合对象中存在,字符串对象也是五大基本类型中唯一一种被嵌套的对象(且会被其他四种对象都嵌套)。

列表对象同时满足如下两个条件时,才会使用ziplist编码,否则使用linkedlist编码:

  • 列表对象保存的所有字符串元素长度均小于64字节;
  • 列表对象保存的元素数量小于512个。

上面两个条件可以通过Redis配置文件中的list-max-ziplist-valuelist-max-ziplist-entries选项修改。

哈希(hash)

基本操作命令

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中所有的字段值(值)

Redis底层数据结构及基本类型的实现_第14张图片
若键值对中的value存储为数字字符串时的操作

命令 说明
hincrby key field increment 指定字段的数值数据加increment
hincrbyfloat key field increment 指定字段的数值数据加increment

底层实现

哈希对象的底层编码可以是ziplisthashtable,其区别如下:
Redis底层数据结构及基本类型的实现_第15张图片
对于ziplist编码的哈希对象,每当有新键值加入到哈希对象中时,会一次向压缩列表表尾添加保存了键的压缩列表节点保存了值的压缩列表节点;而对于hashtable编码的哈希对象其底层由字典实现,哈希对象的每个键值对都由字典键值对保存。
Redis底层数据结构及基本类型的实现_第16张图片
哈希对象只有同时满足下面两个条件时,才有ziplist编码,否则由hashtable编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个。

上面两个条件可以通过Redis配置文件中的hash-max-ziplist-valuehash-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 将指定成员从原集合移动到目标集合中

Redis底层数据结构及基本类型的实现_第17张图片

底层实现

集合对象的底层编码可以是intsethashtable,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,set 就由整数集合实现,而当sethashtable实现时,其底层就是类似于hash对象一样,只不过字典中的每个键存储一个字符串对象(该字符串对象包含一个集合元素),而字典中的每个值则全部设置为NULL,如下图所示:
Redis底层数据结构及基本类型的实现_第18张图片
当集合对象同时满足如下两个条件时,才使用intset编码,否则使用hashtable编码:

  • 集合对象保存的所有元素都是整数值;
  • 集合对象保存的元素数量不超过512个。

第二个条件可以通过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 获取集合中分数满足条件的数量

Redis底层数据结构及基本类型的实现_第19张图片

底层实现

有序集合对象的底层编码可以是ziplistskiplist,其区别如下:
Redis底层数据结构及基本类型的实现_第20张图片
当有序集合对象同时满足如下两个条件时,才使用ziplist编码,否则使用skiplist编码:

  • 有序集合保存的元素数量小于128个;
  • 有序集合保存的所有元素成员的长度都小于64字节。

上面两个条件可以通过Redis配置文件中的zset-max-ziplist-entrieszset-max-ziplist-value选项修改。

你可能感兴趣的:(Redis)