Redis基础数据类型

138 阅读9分钟

基础知识

redisObject

redis通过redisObject对象进行存储

typedef struct redisObject {
  unsigned type:4;    //表示对象类型
  unsigned encoding:4;    //表示对象的内部编码
  unsigned lru:REDIS_LRU_BITS;    //对象最后一次被命令程序访问的时间
  int refcount;    //记录的是该对象被引用的次数
  void *ptr;    //指针指向具体的数据
} robj;

string

主要有三种编码方式:int、embstr、raw

Int

8个字节的长整型。存储整数。

embstr

存储<=39字节的字符串。使用redisObject和sds保存数据,只分配一次内存空间(因此redisObject和sds是连续的)

raw

大于39个字节的字符串,创建时分配两次内存空间(分别为redisObject和sds分配空间)

补充:

sds

注意:以下仅是一个简单的示例,事实上sds根据长度范围定义了不同的结构

struct sdshdr {
    unsigned int len;   //buf中已经使用的长度
    unsigned int alloc;  //buf中未使用的长度
    unsigned char flags;  //flag用3bit来标明类型,其余5bit目前没有使用
    char buf[];         //柔性数组buf
};

image.png

使用场景

string主要有以下几个典型使用场景:

  • 缓存功能
  • 计数
  • 共享Session
  • 限速

Hash

主要有两种编码方式:listpack、hashtable

listpack

/* Each entry in the listpack is either a string or an integer. */
typedef struct {
    /* 当使用字符串时,它提供长度(slen) */
    unsigned char *sval;
    uint32_t slen;
    /* 当使用整数时,“sval”为NULL,lval 保存该值。 */
    long long lval;
} listpackEntry;

hashtable

一个hashtable由1个dict结构、2个dictht结构、多个dictEntry组成(7.0版本之前,7.0版本之后dicht被去除,dict直接指向dictEntry **table链表)

image.png

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

使用场景

hash主要有以下典型应用场景:

  • 缓存用户信息
  • 缓存对象

List

主要有两种编码方式:linklist、ziplist、quicklist

linklist

主要是一个双向链表

ziplist

压缩链表,主要是一块连续内存块

image.png

  • 1、zlbytes:压缩列表的字节长度,占4个字节
  • 2、zltail:压缩列表尾元素相对于压缩列表起始地址的偏移量,占4个字节
  • 3、zllen:压缩列表的元素数目,占两个字节
  • 4、entryX:压缩列表存储的若干个元素,可以为字节数组或者整数
  • 5、zlend:压缩列表的结尾,占一个字节,恒为0xFF。
typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

quicklist

为了结合linklist和ziplist的优点,list 的底层实现使用快速列表 quicklist。

quicklist 包含多个内存不连续的节点,但每个节点本身就是一个 ziplist

    typedef struct quicklistNode {
        struct quicklistNode *prev;  // 上一个 quicklist 
        struct quicklistNode *next;  // 下一个 quicklist
        unsigned char *zl;           // 数据指针,指向 ziplist 结构,或者 quicklistLZF 结构
        unsigned int sz;             // ziplist 占用内存长度(未压缩)
        unsigned int count : 16;     // ziplist 记录数量
        unsigned int encoding : 2;   // 编码方式,1 表示 ziplist ,2 表示 quicklistLZF
        unsigned int container : 2;  // 
        unsigned int recompress : 1;         // 临时解压,1 表示该节点临时解压用于访问
        unsigned int attempted_compress : 1; // 测试字段
        unsigned int extra : 10;             // 预留空间
    } quicklistNode;
    typedef struct quicklistLZF {
        unsigned int sz;    // 压缩数据长度
        char compressed[];  // 压缩数据
    } quicklistLZF;
    typedef struct quicklist {
        quicklistNode *head;        // 列表头部
        quicklistNode *tail;        // 列表尾部
        unsigned long count;        // 记录总数
        unsigned long len;          // ziplist 数量
        int fill : 16;              // ziplist 长度限制,每个 ziplist 节点的长度(记录数量/内存占用)不能超过这个值
        unsigned int compress : 16; // 压缩深度,表示 quicklist 两端不压缩的 ziplist 节点的个数,为 0 表示所有 ziplist 节点都不压缩
    } quicklist;

image.png

使用场景

list主要有以下几种使用场景:

  • 消息队列
  • 文章列表

Set

主要是两种编码方式:hashtable、intset

intset

typedef struct intset {
    uint32_t encoding; /* 集合类型 */
    uint32_t length; /* 集合长度 */
    int8_t contents[]; /* 集合 */
} intset;

使用场景

set主要有如下使用场景:

  • 标签(tag)
  • 共同关注

ZSet

主要是两种编码方式:skiplist、ziplist

skiplist

主要是跳表

image.png

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;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

ziplist

参见List。

使用场景

ZSet主要应用场景:

  • 用户点赞统计
  • 用户排序

常见问题

1. 说说Redis主要数据结构和底层实现

redis主要数据结构有string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)五种。这些只是 Redis 对外的数据结构,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现, 而Redis 会在合适的场景选择合适的内部编码。

先说string,就是字符串,主要用于记录计数或一些简单的缓存对象,它主要有三种编码的实现,分别是int、embstr、raw。当记录对象为整数时,才会采用int编码;而存储<=39字节的字符串。使用emstr编码,使用redisObject和sds保存数据,此时只分配一次内存空间(因此redisObject和sds是连续的);当存储大于39个字节的字符串,使用raw,创建时分配两次内存空间(分别为redisObject和sds分配空间)。而sds则是redis自己定义的数据结构,用以存储数据,它主要由柔性数组(char[])buf、buf中已经使用的长度(int)len、buf中未使用的长度(int)alloc、buf类型(char)flags,flags一般用3bit来标明类型,其余5bit目前没有使用

然后是hash,就是哈希,缓存复杂且有较频繁修改部分属性需求的对象。其两种编码方式:listpack、hashtable,主要使用的是hashtable,其在7.0版本之前由1个dctht结构、2个dicht、多个dictEntry组成,7.版本之后dicht被去除,dict直接指向dictEntry **table链表

dict由字典的类型(dictType*)type、一个包含两个 dictht 结构体的数组(dictht[2])ht以用于实现哈希表的两个哈希表(ht[0] 和 ht[1])、用以表示当前是否在进行 rehash 操作(long)rehashidx 如果值为 -1则表示没有正在进行的 rehash 操作、表示当前正在运行的迭代器的数量(unsigned long)iterators,比较特殊的是通常情况下只有一个哈希表 ht[0] 有值,在扩容的时候,把ht[0]里的值rehash到ht[1],然后进行 渐进式rehash ——所谓渐进式rehash,指的是这个rehash的动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。待搬迁结束后,h[1]就取代h[0]存储字典的元素。

dictht由一个指向 dictEntry 指针数组的指针(dictEntry **)table、table 数组的长度(unsigned long)size、哈希表大小的掩码以用于快速计算哈希值对应的数组索引(unsigned long)sizemask、哈希表中已经使用的槽位数量也就是当前哈希表中存储的元素个数(unsigned long)used组成。

dictEntry由一个指向键的指针(void )key、一个联合体,可以存储值的不同类型,包括指针、64 位无符号整数、64 位有符号整数和双精度浮点数()v、 一个指向下一个 dictEntry 结构体的指针(dictEntry)next*组成。

接着是list,主要用于消息队列、列表。主要有三种编码方式:linklist、ziplist、quicklist。着重说一下quicklist快速列表,quicklist 包含多个内存不连续的节点,这些节点以双向链表相连,且每个节点本身就是一个 ziplist

还有set,集合,主要有在标签(tag)和共同关注使用,主要是两种编码方式:hashtable、intset。

最后是ZSet,有序集合,主要在用户点赞统计和用户排序等排行榜使用,主要是两种编码方式:skiplist、ziplist。主要是skiplist跳表,实现原理如下:

  1. 层级结构: 跳表包含多个层级,每个层级都是一个链表,最底层是包含所有元素的完整有序链表。每个元素在每个层级中以某种概率出现,这样就可以通过跳过多个元素,加速查找。
  2. 索引: 在每个层级的链表中,节点包含一个指向下一个节点的指针,同时也包含一个指向同一层级中下一个元素的指针。这两种指针构成了节点在同一层级中的链表。在跳表的最顶层,有一个额外的头节点,指向最左和最右的元素。
  3. 查找操作: 从最顶层的头节点开始,沿着每一层的链表进行搜索,如果当前节点的下一个节点的值大于目标值,则在同一层级中继续搜索,如果小于或等于目标值,则转移到下一层级。这样反复进行,直到找到目标值或者确定目标值不在跳表中。
  4. 插入操作: 在执行插入操作时,首先执行查找操作找到插入位置,然后在相应层级中插入新节点,并更新索引。在每一层级中,决定是否在该层级插入新节点的概率是通过随机生成的方式确定的。
  5. 删除操作: 执行删除操作时,同样需要通过查找找到待删除节点,然后在每一层级中删除相应的节点,并更新索引。

作者水平有限。若有错漏还请指出,感谢!