三.Redis中那些你不知道的秘密-五大基本结构set的实现原理

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis中的set和java中的set集合有相似之处,它的元素不会按照插入的向后顺序而存储,且元素是不允许重复的。set内部使用到了intset(整数集合)和hashtable(哈希表)两种方式来存储元素,如果set存储的元素是整数,且当元素个数小于512个会选择intset存储,目的是减少内存空间,遇到两种情况会发生变化,就是当存储的元素个数达到512(通过set-max-intset-entries 配置)或者添加了非整数值时如:‘b’,set会选择hashtable作为存储结构。

前言

Redis中的set和java中的set集合有相似之处,它的元素不会按照插入的向后顺序而存储,且元素是不允许重复的。set内部使用到了intset(整数集合)和hashtable(哈希表)两种方式来存储元素,如果set存储的元素是整数,且当元素个数小于512个会选择intset存储,目的是减少内存空间,遇到两种情况会发生变化,就是当存储的元素个数达到512(通过set-max-intset-entries 配置)或者添加了非整数值时如:‘b’,set会选择hashtable作为存储结构。
在这里插入图片描述

整数集合intset

整数集合intset是用来存储整数的集合,且存储是按照小到大的顺序来存储(可以二分查找),intset目的是用来节省内存,当Redis集合类型的元素都是整数并且都处在64位有符号整数范围之内时,使用该结构体存储,我们看一下它的结构:查看源码

typedef struct intset{
   
   
    //编码类型
    uint32_t encoding;
    //集合元素数量
    uint32_t length;
    //存储元素的数组
    int8_t contents[];
} intset;

//下面是相关函数
intset *intsetNew(void);
intset *intsetAdd(intset *is, int64_t value, uint8_t *success);
intset *intsetRemove(intset *is, int64_t value, int *success);
uint8_t intsetFind(intset *is, int64_t value);
int64_t intsetRandom(intset *is);
uint8_t intsetGet(intset *is, uint32_t pos, int64_t *value);
uint32_t intsetLen(const intset *is);
size_t intsetBlobLen(intset *is);
int intsetValidateIntegrity(const unsigned char *is, size_t size, int deep);

在这里插入图片描述

解释:

  • length: contents中元素的个数
  • contents:存储元素的数组,需要根据encoding来决定多少个字节表示一个元素

  • encoding: 编码类型,决定每个元素占用几个字节,有三种类型

编码
INTSET_ENC_INT16 该方式每个元素占2个字节,存储 -32768 到 32767
INTSET_ENC_INT32 该方式每个元素占4个字节,存储 32767到2147483647 或 -2147483647 到 -32768
INTSET_ENC_INT64 该方式每个元素占8个字节,存储 2147483647 到 922337203684775807 或 -922337203684775808 到 -2147483648

intset会根据插入的值判断是否扩容,根据插入内容来修改encoding使用什么类型,从而决定contents每个元素的字节数,紧跟着对contents进行扩容。

intset的升级

假如我们添加这样的数据 sadd 1 2 3 ,底层会执行intsetAdd函数,它会选择INTSET_ENC_INT16类型 去存储,且按照元素大小顺序,它在intset中的结构是这样的
在这里插入图片描述
如果我们再执行 sadd 32999 会发生什么?32999是超过了INTSET_ENC_INT16 类型的存储大小,如果硬塞进去就会内存溢出。所以为了防止内存溢出,intset在添加新元素的时候会先判断是直接插入新元素还是先扩容过后再插入新元素,扩容流程如下:

  • 判断新元素的编码类型是否大于以后元素的编码类型,如果大于就进行扩容
  • 根据新元素的编码类型,为contents的存储空间扩容,同时为新元素分配空间
  • 将所有元素都转换成和新元素相同的编码类型,并调整元素存储位置,且要保证底层数组的有序性质不变
  • 将新元素添加到contents中

intset升级源码如下:

/* Upgrades the intset to a larger encoding and inserts the given integer. */
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
   
   
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    / * 判断新插入的值是小于 0 就插首部,否则插尾部 */
    int prepend = value < 0 ? 1 : 0;

    / *首先设置新的编码并调整大小* /
    /* First set new encoding and resize */
    is->encoding = intrev32ifbe(newenc);
    is = intsetResize(is,intrev32ifbe(is->length)+1);
    / * 从最后一个往前扩容,否则会发生制的覆盖 */
    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));
    / *如果插入的值小于 0 ,则插头部,否则插入尾部。* /
    /* Set the value at the beginning or the end. */
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);

    / *修改intset的长度,将其加1 */
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);
    return is;
}

流程图如下:
在这里插入图片描述

上面案例升级之后的效果如下:
在这里插入图片描述

这样设计的目的就是为了节约内存,通常情况下当我们存储的元素在 -32768 到 32767之间,就使用更小的存储空间,INT16,当插入元素超过该范围就升级更大的内存空间去存储,我们可以任意的存储INT16,INT32,INT64类型的整数不用担心内存问题。

需要注意的是:intset是没有降级的概念,一旦存储了INT32类型的元素那么数组的元素一直都是INT32类型了。

set的添加流程

下面来分析一下添加的详细流程: 见Set源码

void saddCommand(redisClient *c) {
   
   
    robj *set;
    int j, added = 0;

    set = lookupKeyWrite(c->db,c->argv[1]);

    / *如果set是空的就创建一个set*/
    if (set == NULL) {
   
   
        set = setTypeCreate(c->argv[2]);
        dbAdd(c->db,c->argv[1],set);
    } else {
   
   
        if (set->type != REDIS_SET) {
   
   
            addReply(c,shared.wrongtypeerr);
            return;
        }
    }

    / *将元素添加到集合中*/
    for (j = 2; j < c->argc; j++) {
   
   
        c->argv[j] = tryObjectEncoding(c->argv[j]);
        / *setTypeAdd中会进行编码转化 */
        if (setTypeAdd(set,c->argv[j])) added++;
    }

    // 如果有添加成功
    if (added) {
   
   
        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);
        // 发送事件
        notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",c->argv[1],c->db->id);
    }

    // 将数据库设为dirty
    server.dirty += added;

    // 返回添加数量
    addReplyLongLong(c,added);
}

这里添加操作会判断set是否是空的,空的就创建一个set,然后执行setTypeAdd把元素依次添加到集合中,接着看setTypeAdd方法的源码

int setTypeAdd(robj *subject, robj *value) {
   
   
    long long llval;

    / * 这里判断编码,是选择 HT (hash)存储还是选择 intset */
    if (subject->encoding == REDIS_ENCODING_HT) {
   
   
        / * 将 value 作为键值为 NULL,把元素元素存储到字典中*/
        if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {
   
   
            incrRefCount(value);
            return 1;
        }

    / * 这里判断encoding 是否为intset,如果是整数就可以 */
    } else if (subject->encoding == REDIS_ENCODING_INTSET) {
   
   

        / *  判断对象的值可以编码为整数,使用 intset存储 ,否则使用hash*/
        if (isObjectRepresentableAsLongLong(value,&llval) == REDIS_OK) {
   
   
            uint8_t success = 0;
            subject->ptr = intsetAdd(subject->ptr,llval,&success);
            if (success) {
   
   
                / *添加成功 ,这里在判断长度是否大于 512 ,大于就进行转换成hash*/
                / *  REDIS_SET_MAX_INTSET_ENTRIES 默认 512 */
                if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                    setTypeConvert(subject,REDIS_ENCODING_HT);
                return 1;
            }

        / * 如果对象的值不能编码为整数,转换为 HT 编码 然后再执行添加操作*/
        } else {
   
   
            setTypeConvert(subject,REDIS_ENCODING_HT);

            redisAssertWithInfo(NULL,value,dictAdd(subject->ptr,value,NULL) == DICT_OK);
            incrRefCount(value);
            return 1;
        }

    / *编码错误* /
    } else {
   
   
        redisPanic("Unknown set encoding");
    }

    / *添加失败,元素已经存在* / 
    return 0;
}

setTypeAdd的流程是这样的

  • 首先判断对象的编码是否是REDIS_ENCODING_HT,是就走字典存储(hashtable)
  • 否则就判断如果编码是REDIS_ENCODING_INTSET,如果能转码成INTSET就使用INTSET场次,然后再判断保存的长度是否达到512,达到就转换成字典存储(hashtable)
  • 其他情况都用hashtable存储,当然如果对象的编码即不是REDIS_ENCODING_HT也不是REDIS_ENCODING_INTSET就做编码未知处理

总结

  • Redis的set用到了intset和hashtable两种结构,当元素是整数且少于512个就使用intset来存储,否则采用hashtable来存储。

  • intset是为了节约内存的设计,针对于不同范围的数据设定了INTSET_ENC_INT16(2字节),INTSET_ENC_INT32(4字节),INTSET_ENC_INT64(8字节)三种编码方式,如果存储元素过大会进行intset升级,从而扩充存储空间。

  • set在添加元素的时候会做一些判断,如果元素是整数,能转码成REDIS_ENCODING_INTSET就采用intset存储,当然如果存储的元素超过512就转换成hash存储,以value作为键,值为null存储到hash,和JDK里面的HashSet很像。

文章结束,希望对你有数帮助,喜欢的话别忘了给个好评

相关文章
|
5月前
|
存储 缓存 NoSQL
Redis中的常用命令-get&set&keys&exists&expire&ttl&type的详细解析
总的来说,这些Redis命令提供了处理存储在内存中的键值对的便捷方式。通过理解和运用它们,你可以更有效地在Redis中操作数据,使其更好地服务于你的应用。
381 17
|
6月前
|
存储 NoSQL Redis
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 +  无锁架构 +  EDA架构  + 异步日志 + 集群架构
|
8月前
|
消息中间件 缓存 NoSQL
Redis原理—5.性能和使用总结
本文详细探讨了Redis的阻塞原因、性能优化、缓存相关问题及数据库与缓存的一致性问题。同时还列举了不同缓存操作方案下的并发情况,帮助读者理解并选择合适的缓存管理策略。最终得出结论,在实际应用中应尽量采用“先更新数据库再删除缓存”的方案,并结合异步重试机制来保证数据的一致性和系统的高性能。
Redis原理—5.性能和使用总结
|
8月前
|
NoSQL 算法 安全
Redis原理—1.Redis数据结构
本文介绍了Redis 的主要数据结构及应用。
Redis原理—1.Redis数据结构
|
8月前
|
缓存 NoSQL Redis
Redis原理—2.单机数据库的实现
本文概述了Redis数据库的核心结构和操作机制。
Redis原理—2.单机数据库的实现
|
8月前
|
存储 缓存 NoSQL
Redis原理—4.核心原理摘要
Redis 是一个基于内存的高性能NoSQL数据库,支持分布式集群和持久化。其网络通信模型采用多路复用监听与文件事件机制,通过单线程串行化处理大量并发请求,确保高效运行。本文主要简单介绍了 Redis 的核心特性。
|
8月前
|
存储 缓存 NoSQL
Redis哈希结构在提升数据检索速度中的实践应用
本文详细介绍了 Redis 哈希结构的特点、常见使用场景以及如何在实际应用中利用哈希结构提升数据检索速度。通过合理使用 Redis 哈希结构,可以显著提高系统的性能和响应速度。在实际开发中,结合具体业务需求,灵活运用 Redis 提供的多种数据结构,构建高效的缓存和数据存储解决方案。希望本文能帮助您更好地理解和应用 Redis 哈希结构,提升数据检索速度。
211 18
|
8月前
|
缓存 NoSQL Redis
Redis原理—3.复制、哨兵和集群
详细介绍了Redis的复制原理、哨兵原理和集群原理。
|
11月前
|
存储 NoSQL PHP
如何用Redis高效实现点赞功能?用Set?还是Bitmap?
在众多软件应用中,点赞功能几乎成为标配。本文从实际需求出发,探讨如何利用 Redis 的 `Set` 和 `Bitmap` 数据结构设计高效点赞系统,分析其优缺点,并提供 PHP 实现示例。通过对比两种方案,帮助开发者选择最适合的存储方式。
321 3
|
11月前
|
存储 NoSQL Redis
【赵渝强老师】Redis的存储结构
Redis 默认配置包含 16 个数据库,通过 `databases` 参数设置。每个数据库编号从 0 开始,默认连接 0 号数据库,可通过 `SELECT &lt;dbid&gt;` 切换。Redis 的核心存储结构包括 `dict`、`expires` 等字段,用于处理键值和过期行为。添加键时需指定数据库信息。视频讲解和代码示例详见内容。
174 0