• Redis原理篇——五种基本数据类型


    一、Redis底层数据结构

    1. SDS

    获取字符串长度耗时: 由于Redis底层是C语言编写的,C语言中没有字符串这个概念,本质上都是字符数组,获取字符串长度就是遍历数组获取长度(遍历到 '\0'结束标识结束 )时间复杂度O(N)比较耗时

    非二进制安全: 不能出现特殊字符 比如转义字符 '\0'

    不可修改: 直接通过字面值定义的字符串,字符数组的长度固定,如果要修改需要重新申请空间,比较麻烦

    Redis构建了一个新的字符串结果,称为简单动态字符串SDS

    struct __attribute__((__packed__)) sdshdr8{
    uint8_t len;  // buf已保存的字符串字节数,不包含结束标示
    uint8_t alloc; //buf 申请的总的字节数,不包含结束标示
    unsigned char flags; // 不同SDS的头类型,用来控制SDS的头大小(8 16 32 64位 不同sds)
    char buf[];  // 字符数组
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    uint8_t 无符号整型8位 范围(0 ~ 255)

    注: Redis实现SDS使用了多个此格式的结构体 来表示不同编码格式的字符串

    如:一个"name" 的sds结构:
    在这里插入图片描述

    动态扩容:

    如果给一个SDS追加一段字符串,首先会申请新的内存空间

    • 假如新字符串小于1M,则新空间为扩展后字符串长度的两倍+1
    • 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。

    +1 是给结束标识的 '\0'

    内存预分配 :减少内存分配的频率,(申请内存需要从用户态切换用户态)提高性能

    优点:

    • 获取字符串长度的时间复杂度为O(1)
    • 支持动态扩容
    • 减少内存分配次数
    • 二进制安全(支持 图片、音频、视频存储)

    2.IntSet

    IntSet是Redis中Set集合的一种实现方式,基于整数数组来实现,并且具备长度可变、元素唯一有序特性。

    typedef struct intset{
        uint32_t encoding; // 编码方式,支持16位、32位、64位
        uint32_t length;  // 元素个数
        int8_t contents[];  // 整数数组,保存集合数据
        
    }intset;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    为方便查找,Intset中的所有整数按照升序依次保存在contents数组中
    在这里插入图片描述

    如果元素所占总空间超过编码范围,Intset会自动升级编码方式,并按照新的编码方式及元素个数扩容数组。

    倒序依次将数组中的语速拷贝到扩容后的正确位置(防止覆盖)

    适用于数量少的情况

    3.Dict

    键值映射关系,Dict由三部分组成:哈希表(DictHashTable)哈希结点(DictEntry)字典(Dict)

    typedef struct dictht {
        // entry数组 数组中保存的是指向entry的指针
        dictEntry **table;
        // 哈希表的大小 2的n次方
        unsigned long size;
        // 哈希表大小的掩码,size -1
        unsigned long sizemask;
        // entry 个数
        unsigned long used;
    } dictht;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    typedef struct dictEntry {
        void *key; //键
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v; // 值
        // 下一个Entry的指针
        struct dictEntry *next;
    } dictEntry;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当向Dict添加键值对时,Redis首先根据key计算出hash值(h),然后利用h & sizemask 来计算元素应该存储到数组中的哪个索引位置

    typedef struct dict {
        dictType *type;  // dict 类型,内置不同hash函数
        void *privdata;  // 私有数据,在做特殊hash运算时使用
        dictht ht[2];   // 一个Dict包含两个哈希表,其中一个时当前数据,另一个一般是空,rehash时使用
        long rehashidx; /* rehash的进度, -1表示未进行 0表示开始rehash */
        int16_t pauserehash; /* rehash是否暂停,1暂停,0继续 */
    } dict;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    Dict的扩容

    Dict中的HashTable就是数组+单向链表实现,当集合中元素较多时,必然导致哈希冲突增多,链表过长则查询效率大大降低

    Dict在每次新增键值对都会检查负载因子 (LoadFactor = used / size),以下两种情况会进行哈希扩容

    • 哈希表的LoadFactor >= 1, 并且服务器没有执行BGSAVE或者BGREWRITEAOF等后台进程 (CPU空闲)
    • 哈希表的LoadFactor > 5

    当删除元素时,也会对负载因子做检查,小于0.1时,会做哈希表收缩

    扩缩容策略:渐进式rehash

    1. 计算新hash表的size,值取决于当前要做的是扩容还是收缩:
    • 如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2^n

    • 如果是收缩, 则新size为第一个大于等于dict.ht[0].used的2^n(不得小于4)

    1. 按照新的size申请内存空间, 创建dictht, 并赋值给dict.ht[1]

    2. 设置dict.rehashidx = 0,标示开始rehash

    3. 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1, 如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]

    4. 将dict.ht[1]赋值给dict.ht[0], 给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存

    5. 将rehashidx赋值为-1, 代表rehash结束

    6. 在rehash过程中, 新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1 ]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空

    字典使用指针,内存不连续,可能产生大量内存锁片,且大量指针比较浪费空间

    4.ZipList

    是一种双端链表,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/ 弹出操作,并且该操作的时间复杂度为O(1)

    在这里插入图片描述

    ZipList中的Entry并不像普通链表那样记录前后节点的指针,因为记录两个指针要占用16个字节,浪费内存。而是采用

    previous_ entry_ length encoding contents

    • previous_ entry_ length: 前一 节点的长度,占1个或5个字节。

      • 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
      • 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
    • encoding: 编码属性,记录content的数据类型( 字符串还是整数)以及长度,占用1个、2个或5个字节

    • contents: 负责保存节点的数据,可以是字符串或整数

    通过这三个属性来进行遍历
    在这里插入图片描述

    ZipList的连锁更新问题,因为每个节点都会保存前一个结点的长度,如果新加的一个节点长度大于254字节,那么后面的结点记录的前一个结点长度占5个字节,此时假如这个结点+5字节后 超过了254字节,继续影响下一个结点,产生连锁更新问题。频繁申请空间了,影响性能

    5.QuickList

    ZipList虽然节省了内存,但是申请内存必须是连续空间,如果内存占用较多,申请内存效率很低

    使用多个ZipList来分片存储数据

    QuickList相当于使用一个双端链表将多个ZipList 挂起

    list-max-ziplist-size 来限制ZipList中entry的个数 (参数为正表示个数,负表示占用内存)

    • -1 每个ziplist的内存占用不超过4kb
    • -2 每个ziplist的内存占用不超过8kb
    • -3 每个ziplist的内存占用不超过16kb
    • -4 每个ziplist的内存占用不超过32kb
    • -5 每个ziplist的内存占用不超过64kb

    list-compress-depth 来控制对节点ziplist压缩

    • 0不压缩

    • 1 首位各有1个节点不压缩,中间节点压缩

    • 2 首位各有2个节点不压缩,中间节点压缩

    typedef struct quicklist {
        quicklistNode *head;   // 头结点指针
        quicklistNode *tail;   // 尾结点指针
        unsigned long count;        /* 所有ziplist中entry的数量 */
        unsigned long len;          /* ziplist的总数量 */
        int fill : QL_FILL_BITS;              /* ziplist的entry上限,默认-2 */
        unsigned int compress : QL_COMP_BITS; /* 首位不压缩的节点数量 */
        unsigned int bookmark_count: QL_BM_BITS;            
        quicklistBookmark bookmarks[];
    } quicklist;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    typedef struct quicklistNode {
        // 前一个节点指针
        struct quicklistNode *prev;
        // 后一个节点指针
        struct quicklistNode *next;
        // 当前节点的ziplist指针
        unsigned char *zl;
        unsigned int sz;    // 当前节点的ziplist的字节大小
    
        unsigned int count : 16;     /* 当前ziplist的entry个数 */
        unsigned int encoding : 2;   /* 编码方式 1 ziplist 2 lzf压缩模式 */
        unsigned int container : 2;  /* 数据容器类型 1其他 2 ziplist */
        unsigned int recompress : 1; /* 是否被解压缩,1表示被解压了,将来要重新压缩 */
        unsigned int attempted_compress : 1; /*  */
        unsigned int extra : 10; /* 预留字段 */
    } quicklistNode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    6.SkipList

    跳表

    • 元素按照升序排序存储(分数,分数若相同就按ele字典排序)
    • 节点可能包含多个指针,指针跨度不同

    最多32级指针
    在这里插入图片描述

    
    /* ZSETs use a specialized version of Skiplists */
    typedef struct zskiplistNode {
        sds ele;  // 节点存储的值
        double score;  // 分数,排序、查找用
        struct zskiplistNode *backward;   // 前一个节点指针
        struct zskiplistLevel {
            struct zskiplistNode *forward;  // 下一个节点指针
            unsigned long span;   // 索引跨度
        } level[];   // 多级索引数组
    } zskiplistNode;
    
    typedef struct zskiplist {
        // 头尾节点指针
        struct zskiplistNode *header, *tail;
        // 节点数量
        unsigned long length;
        // 最大的索引层级,默认是1
        int level;
    } zskiplist;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    与红黑树对比:增删改查效率基本一致,实现却更简单

    7.RedisObject

    Redis中的任意数据类型的key和value都会被封装为一个RedisObject(Redis对象)

    typedef struct redisObject {
        unsigned type:4; // 对象类型,分别为五种基本数据类型  占4个bit位
        unsigned encoding:4; // 编码方式 11 中 占4个bit位
        unsigned lru:LRU_BITS; /*LRU表示对象最后一次被访问的时间,占24个bit位。便于判断空闲时间太久的key*/
        int refcount;  // 引用计数器 0表示对象无人引用 可以被回收
        void *ptr;  // 指针,指向存放实际数据的空间
    } robj;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    共16字节 相当于Redis中key或者value的头部分

    Redis中会根据存储的数据类型不同,选择不同的编码方式。
    在这里插入图片描述

    二、Redis中五种基本数据结构

    1. String

    底层实现:
    • 基本编码方式是RAW,基于简单动态字符串(SDS)实现,存储上限为512mb
      在这里插入图片描述

    • 如果存储的SDS长度小于44字节,则会采用embstr编码,此时object head与SDS是一段连续空间。申请内存时只需要调用依次内存分配函数,效率更高

    在这里插入图片描述

    (SDS长度为44字节 使用8位格式的sds 头部占3个字节尾部1个字节,而RedisObject占16字节,总共64字节 便于分配内存空间)

    • 如果存储的字符串是整数值,大小在LONG_MAX范围内,会采用Int编码:直接将数据保存在RedisObjectptr指针位置(刚好8字节),无需SDS

    在这里插入图片描述

    应用场景:
    1. json字符串格式的对象

    2. 常规计数 (redis单线程,执行命令的过程是原子的)

    3. 分布式锁 SET 命令 NX参数 可以实现 key不存在才插入,EX设置过期时间,防止客户端异常造成锁无法释放

    • 如果key不存在,则显示插入成功,可以表示加锁成功

    • 如果key存在,显示插入失败,可以用来表示加锁失败

    ​ 解锁就是删除 key ,在删除前需要判断是否是自己的锁,防误删。因此解锁就是两步操作,使用Lua脚本保证原子性

    共享session问题

    在分布式系统中,用户的session由于存储在单个服务器上,当用户第二次访问,系统(负载均衡)把请求分配到其他的服务器上由于没有session,出现重复登录的问题。

    一种比较高效的方案就是: 将session存入Redis中,以key的过期时间代表session的过期时间。服务器验证用户是否登录时,从redis中取session进行验证。

    2. List

    底层实现

    底层实现QuickList:LinkedList+ZipList 可以双端访问,内存占用低,包含多个ZipList,存储上限高

    参考QuickList

    应用场景:

    消息队列

    通过lpush+Rpop 或者 Rpush+Lpop 命令来实现消息队列,按照消息的先进先出特性可以保证消息的有序,

    Brpop命令阻塞式读取,消费者没有读到数据时会自动阻塞,防止消费者不断尝试取消息(没有通知机制)

    重复消费问题:

    使用 自定义全局ID标记消息,在消费者中维护一个记录已经消息的消息ID

    可靠性:

    消费者在处理消息时如果宕机就造成了消息丢失,List中提供一个BRPOPLPUSH 命令,作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息放入到另一个 List(可以叫作备份 List)留存

    缺陷: 不支持多个消费者消费同一条消息

    3. Set

    底层实现

    单列集合,元素唯一,对查询效率要求极高,不一定元素有序

    • 为了查询效率和唯一性,set底层使用HT编码(Dict)。Dict中的key用来存储元素,value统一位null
    • 当存储数据都是整数,且不超过set-max-intset-entries时(默认512),Set采用IntSet编码,节省内存
    robj *setTypeCreate(sds value) {
        // 判断value是否 是数值类型 longlong
        if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
            // 如果是数值类型,采用IntSet编码
            return createIntsetObject();
        // 否则采用默认编码HT
        return createSetObject();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    应用场景:

    点赞:key可以是帖子、blog、评论, value是用户id列表,也方便求点赞总数

    共同关注:交集运算 key为用户id,value为关注的id列表

    抽奖:key为活动名称,value为参与人 SRANDMEMBER 允许重复中奖 SPOP 不允许重复中奖

    4. ZSet

    底层实现

    也就是SortedSet,其中每一个元素都需要指定一个score值和member值

    • 可以根据score值排序
    • member必须唯一
    • 根据member查询分数

    底层实现为 SkipList跳表(排序) + Dict(键值存储、唯一)

    当元素数量不多时,HT和SkipList的优势不明显,而且更耗内存。因此zset还会采用ZipList结构来节省内存,需要同时满足两个条件:

    1. 元素数量小于zset_max_ziplist_entries,默认128

    2. 每个元素都小于zset_max_ziplist_value字节,默认值64

    ZipList本身无排序功能,没有键值对概念,zipList 将score和element紧挨着放一起,ele在前score在后。score越小越接近链表头部

    应用场景

    排行榜:

    根据评分对帖子进行排行

    获取指定评分范围内的数据,或者指定排名范围内的数据

    ZRANGE key min max : 按照score排序后,获取指定排名范围内的元素

    ZRANGEBYSCORE key min max: 按照score排序后,获取执行score范围内的元素

    5. Hash

    • 键值存储

    • 根据键获取值

    • 键必须唯一

    底层实现
    • Hash结构默认采用ZipList编码,用以节省内存。ZipList中两个相邻的entry分别保存field和value

    • 当数据量较大时,Hash结构会转为HT编码,也就是Dict

    1. ZipList中的元素数量超过了hash-max-ziplist-entries (默认512)
    2. ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)
    int hashTypeSet(robj *o, sds field, sds value, int flags) {
        int update = 0;
       // 判断是否时ZipList编码
        if (o->encoding == OBJ_ENCODING_ZIPLIST) {
            unsigned char *zl, *fptr, *vptr;
                
            zl = o->ptr;
             // 查询 头指针
            fptr = ziplistIndex(zl, ZIPLIST_HEAD);
            if (fptr != NULL) {   // 头指针不为NULL ziplist不为空,开始查key
                fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
                if (fptr != NULL) {     // 判断key是否存在 如果存在直接更新覆盖
                    /* Grab pointer to the value (fptr points to the field) */
                    vptr = ziplistNext(zl, fptr);
                    serverAssert(vptr != NULL);
                    update = 1;
    
                    /* Replace value */
                    zl = ziplistReplace(zl, vptr, (unsigned char*)value,
                            sdslen(value));
                }
            }
                // 不存在,直接 插入ziplist尾部
            if (!update) {
                /* Push new field/value pair onto the tail of the ziplist */
                zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                        ZIPLIST_TAIL);
                zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                        ZIPLIST_TAIL);
            }
            o->ptr = zl;
         
            /* 插入了新元素 检查 list长度是否超出,超出则转为HT */
            if (hashTypeLength(o) > server.hash_max_ziplist_entries)
                hashTypeConvert(o, OBJ_ENCODING_HT);
        } else if (o->encoding == OBJ_ENCODING_HT) {
            // HT编码 直接插入新元素或者覆盖旧值
            dictEntry *de = dictFind(o->ptr,field);
            if (de) {
                sdsfree(dictGetVal(de));
                if (flags & HASH_SET_TAKE_VALUE) {
                    dictGetVal(de) = value;
                    value = NULL;
                } else {
                    dictGetVal(de) = sdsdup(value);
                }
                update = 1;
            } else {
                sds f,v;
                if (flags & HASH_SET_TAKE_FIELD) {
                    f = field;
                    field = NULL;
                } else {
                    f = sdsdup(field);
                }
                if (flags & HASH_SET_TAKE_VALUE) {
                    v = value;
                    value = NULL;
                } else {
                    v = sdsdup(value);
                }
                dictAdd(o->ptr,f,v);
            }
        } else {
            serverPanic("Unknown hash encoding");
        }
    
        /* Free SDS strings we did not referenced elsewhere if the flags
         * want this function to be responsible. */
        if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
        if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
        return update;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    应用场景:

    存储对象类型的数据很方便,可以关联同一个对象的不同属性

    购物车,以用户id为key,商品id为field,商品数量为value

  • 相关阅读:
    【CMake】使用ctest配置googletest
    基于Or-Tools的整数规划问题求解
    计算机网络第一章习题_网络概述
    栈队列数组试题(四)——数组和特殊矩阵
    PostgreSQL插件的安装使用与删除
    Java框架 SpringMVC--拦截器与异常处理器
    zenmap无法运行扫描操作
    C++ 20语言特性
    基于装饰器对通用表格的封装
    后端开发总结(3):代码健壮性:容错处理+测试
  • 原文地址:https://blog.csdn.net/qq_52595134/article/details/128164826