Redis源码学习----Redis的基本数据结构

本文详细介绍了Redis中的核心数据结构,包括SDS、链表、字典和整数集合,强调了它们的特性、优化策略以及在实际操作中的注意事项。例如,SDS提供了常量时间获取字符串长度和避免缓冲区溢出的能力,链表采用了双向链表结构,字典则利用了哈希表和渐进式rehash策略。同时,文章也提到了压缩列表的存储方式和潜在的性能问题。
摘要由CSDN通过智能技术生成
###Redis版本:Redis5.0.14
###人生建议,一定要按照对应的版本阅读,否则会相当难受
一、redis的简介、特性、部署
    · 简介
        Redis是一个开源的,内存中的数据结构存储系统,它可以用作数据库、缓存、消息中间件。
        需要注意他和levelDB的区别。
    
    · 特性
        1. 性能高
        2. 丰富的数据类型   
        3. 支持事务     
        4. 内建replication和集群    (主从复制、Redis Cluster)
        5. 支持持久化   (RDB设置阈值存储、AOF保存每一条指令)
        6. 单线程、原子性操作   
    
    · 压测
        官方自带性能测试工具
        redis-banchmark [-h] [-p] [-c] [n] [-k]

二、简单动态字符串(SDS)
    · 数据结构
        redis为了节省内存,针对不同长度的数据采用不同的数据结构。
            #define SDS_TYPE_5  0
            #define SDS_TYPE_8  1
            #define SDS_TYPE_16 2
            #define SDS_TYPE_32 3
            #define SDS_TYPE_64 4
        但是SDS_TYPE_5并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放。
            typedef char *sds;
            // __attribute__ ((__packed__))用来表示取消结构在编译过程中的优化对齐
            struct __attribute__ ((__packed__)) sdshdr8 {
                uint8_t len; /* 表示数据长度 */
                uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度 */
                unsigned char flags; /* 3 lsb of type, 5 unused bits ,小端*/
                // 变长数据
                char buf[];
            };
    
    · 空间扩容
        1. 当前有效长度>=新增长度,直接返回     sdsavail(s)=(当总长度alloc-当前长度len) >= 新增长度addlen
        2. 如果newlen < SDS_MAX_PREALLOC,翻倍增;     (SDS_MAX_PREALLOC=1024*1024)
                          否则增到newlen;
                          更新类型为newlen对应的SDS_TYPE
        3. 更新之后,判断新旧类型是否一致:
            一致使用remalloc:重新分配内存
            如果不是相同类型:使用malloc+free
                重新分配新内存,然后把老数据copy到新数据中,再把老数据free释放。
    
    · 空间缩容
        在sdstrim操作时,采用的是惰性空间释放;即,不会立即使用内存重分配来回收缩短的字节,只是进行移动和标记,并修改数据长。
        真正的删除:sdsRemoveFreeSpace()
    
    · 优点:
        1. 常量获取字符长度     (直接查len字段)
        2. 避免缓冲区溢出       (alloc和len对比)
        3. 减少字符串修改带来的内存频繁重分配次数       (成倍扩容)
        4. 二进制操作安全:可以保持文本数据,也可以保持任意格式的二进制数据 (视频流数据)
        5. 以'\0'结尾,兼容C字符串函数
    
    · 从typedef char *sds可以发现,sds时char* 的别名,可以理解为分配的是一块连续内存,
        根据局部性原理可以提高访问速度。
    · 数据存储不使用 SDS_TYPE_5 ,是因为这个类型每次更新数据时都要进行扩容。
    · 利用C语言内存布局,既可以保证变长,又能保证内存连续,
        在sds的函数操作中,可以发现频繁使用s[-1],是因为C语言这样表示前一位(buf位)


三、链表(adlist)
    · 数据结构
        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;
    
    · 迭代器
        这里链表的迭代和数据时分开的,采用了类似迭代器模式,这个思想被用到很多场景(如 leveldb)
            typedef struct listIter {
                listNode *next;
                int direction;      //方向
            } listIter;

            listIter *listGetIterator(list *list, int direction)
            {
                listIter *iter;     
                if ((iter = zmalloc(sizeof(*iter))) == NULL) return NULL;
                if (direction == AL_START_HEAD)
                    iter->next = list->head;
                else
                    iter->next = list->tail;
                iter->direction = direction;
                return iter;
            }


四、字典(dict)
    · 数据结构:
        非常像Golang中的map
            typedef struct dictEntry {
                void *key;
                union {         // union节省内存
                    void *val;
                    uint64_t u64;
                   int64_t s64;
                    double d;
                } v;
                struct dictEntry *next;     // 采用链地址法,针对新节点采用头插法
                void *metadata[];          // 对齐
            } dictEntry;
        值得注意的是7.0.0中把原有的dictht整合到了dict中,把定位和size加入了defind
        #define DICTHT_SIZE(exp) ((exp) == -1 ? 0 : (unsigned long)1<<(exp))
        #define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
            struct dict {
                dictType *type;         //记录一些回调函数
                dictEntry **ht_table[2];    // 两个表,表里存dictEntry,一般只会使用0,ht[1]哈希表只会对ht[0]哈希表进行rehash操作
                unsigned long ht_used[2];   // 记录使用情况
                long rehashidx;         // 记录rehash的进度,为-1表示不会进行rehash
                int16_t pauserehash;    // 记录rehash状态
                signed char ht_size_exp[2];     //两个表的大小
            };

    · size的设置每次都是2的幂或者扩到最大,和Golang一样
    · rehash==-1代表没有在rehash
    · 定位用的是取模的方法,和Golang一样
        #define DICTHT_SIZE_MASK(exp) ((exp) == -1 ? 0 : (DICTHT_SIZE(exp))-1)
    · 扩容,缩容,除了细节其他和GOlang一样
        扩容总结:
            1. 先判断是否正在持久化;
            2. 校验合法性,并容量size分配为2的幂(或者是虽大限额)
            3. 迁移完成,释放ht[0],将ht[1]指向ht[0],并初始化ht[1],等待下一次使用
        缩容总结:
            1. 当超过了初始值且填充率小于10%,代表字典中空洞很多;或者心跳函数触发
            2. 判断是否需要缩容;
            3. 确保内存尽量==1:1
    
    · 渐进式rehash:
        指rehash不是一次性、集中式完成的;
        1. redis是单线程的,如果hash的key太多,rehash可能会阻塞服务器过长,
           所以redis本身将rehash操作分散在了后续的每次增删改查中
        2. 定时rehash,每次1ms
    · rehash注意点
        1. rehash过程中添加新的字典,只会插入到ht[1]中,确保ht[0]只减不增;
        2. 持久化过程中,服务器rehash的负载因子会变高,默认是5  (Golang语言,时刻复习Golang ahha)
    
    · 迭代器
        1. 安全模式:
            支持一边遍历一边增删改查,但是不支持rehash
        2. 非安全模式
            只支持只读模式,但是支持rehash;使用增删改查操作会造成不可预期的问题
    
    · 跳表
        src找不到了hahaha找不到,算法栏目中有一个python的跳表


五、整数集合(intset)
    · 数据结构:
        整数集合(intset)是redis用于保存整数值的集合抽象数据结构,他也可以保存类型为int16、int32、int64的值;
        且保证集合中不会出现重复元素,数据也是从小到大存储。
            typedef struct intset {
                uint32_t encoding;      // 表示元素类型,默认是INTSET_ENC_INT16
                uint32_t length;        // 表示元素个数,按需分配元素
                int8_t contents[];      // 动态数组,按需分配空间,按照从小到大的顺序保存元素
            } intset;
    
    · 数据操作
        1. 查找:采用的是折半查找
        2. 插入和升级:
            当新插入的元素类型大于当前intset类型时,为防止溢出,会对其进行升级;
            1. 计算输入值编码类型
            2. 判断编码是否大于当前intset编码
            如果不是:
                3. 查找插入最佳位置
                4. remalloc新的内存,更新intset的长度信息
            如果是:
                3. 更新当前intset编码,并添加元素
                4. 分别计算输入值和当前intset编码类型
                5. remalloc新的内存,更新intset的长度信息
                6. 按照新编码类型,从后向前重新把元素放到正确的位置
                7. 插入元素
            intset会有一个优化,先进行首尾比较再二分
    
    · 特点:  
        1. 灵活性
            可以通过自动升级底层数组来适应新元素,所以可以将任意类型的整数添加至集合,而不用担心类型错误
        2. 节约内存
            不同类型采用不同类型的空间对其存储,从而最小的避免空间浪费
        3. 不支持降级
        4. 添加和删除都要进行remalloc操作,会引起性能波动,慎用 


六、压缩列表
    · 数据结构:   
        压缩列表是有一系列特殊编码的连续内存组成的顺序性数据结构,一个压缩列表可以包含任意多个节点;
        每个节点可以保存一个字节数组或者一个整数值。适合存储小对象和长度有限的数据。
    
        ziplist数据结构没有被真正的定义出来,而是通过宏定义提取出来;
        存储在ziplist的entry并非按照zlentry定义的,他也是通过宏来总结出来的

        // 只用于接受,并不是实际存储的
        typedef struct zlentry {
            unsigned int prevrawlensize;    
            unsigned int prevrawlen;        
            unsigned int lensize;       

            unsigned int len;           

            unsigned int headersize;     
            unsigned char encoding;     

            unsigned char *p;           
        } zlentry;
    
    · 总的来说就是通过头位置和偏移量来找到内存中的位置
    
    · prevrawlen 记录前一个节点的长度,以字节为单位。
        当前前一个字节长度小于254时,prevrawlen只需要一个字节;
        否则prevrawlen需要5个字节来进行存储,其中第一个固定为0xFE(254),后四位存储前一个字节的长度;
      可以根据该属性快速定位到前一个节点的起始位置,支持反向遍历

    · 注意点
        1. 查找的时间复杂度为O(N)
        2. 列表的长度超过了UINT16_MAX,此时,zllen不再表示节点的个数
        3. 连锁更新
            在entry会存储前一个节点的长度;
            因此,当插入或删除操作,会打破ziplist具有的特性,此时需要进行节点的更新;
            reids中只处理节点的扩张,即由1个字节变成5个字节,不进行收缩操作。
            最差情况是:连接更新对ziplist执行N次空间分配操作,而每次空间分配的最坏复杂度为O(N),所以连锁更新最坏的情况是O(N^2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值