redis学习之数据结构与对象(一)

1.SDS

简介:Redis没有采用C语言的以空字符串结尾的字符数组,而是构建一种简单动态字符串(Simple dynamic string,SDS),并将它作为string的表示。

struct sdshdr {
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[]; // 依然以’\0’结尾
};

SDS与C语言的字符串的区别

  1. 常数复杂度获取字符串长度。
  2. 通过free杜绝缓存区溢出
  3. 空间预分配(如果len小于1MB,那么分配的free将等于len,如果len大于1MB那么分配free1MB)和惰性空间释放(缩短SDS时不立即回收内存而是记录到free中)
  4. 二进制安全,所以能保存图片,视频,音频文件
    常用操作:

    • get:sdsrange—O(n)
    • set:sdscpy—O(n)
    • create:sdsnew—O(1)
    • len:sdslen—O(1)
        

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;

常用操作:

  • rpush: listAddNodeHead —O(1)
  • lpush: listAddNodeTail —O(1)
  • push:listInsertNode —O(1)
  • index : listIndex —O(N)
  • pop:ListFirst/listLast —O(1)
  • llen:listLength —O(N)

3.字典

字典在底层是由哈希表实现的,所以我们从哈希表开始入手。

(1)哈希表

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;

     // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

(2)哈希节点

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

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

(3)字典

typedef struct dict {
    // 类型特定函数
    dictType *type;

     // 私有数据
    void *privdata;

     // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

     // 目前正在运行的安全迭代器的数量

    int iterators; /* number of iterators currently running */
 } dict;

(4)字典特定函数

/*
 * 字典类型特定函数
 */
typedef struct dictType {
     // 计算哈希值的函数
    unsigned int (*hashFunction)(const void *key);

     // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);

     // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);

     // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);

    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);

    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);

} dictType;

这里写图片描述

总结:字典这种数据结构看起来十分复杂,其实十分简单,只要看过Java的HashMap的源代码都应该很容易懂。

扫描二维码关注公众号,回复: 1303812 查看本文章

注意:
1. 哈希表有一个数据结构,unsigned long sizemask;有什么用呢,就是让本来的 “哈希节点的key % size” 计算出索引变成 “哈希节点的key & sizemask”,位运算更加高效,当然前提是哈希节点是2的x次幂。
2. rehash也是跟Java相似,为什么dict的ht[2]有两个哈希表呢,原来dict[0]是原来的哈希表,dict[1]是扩容后的哈希表。

常用操作:

  • Hset: dictAdd/dictReplace —-O(1)
  • Hget: dictFetchValue—O(1)
  • Randomkey:dictGetRandomKey—O(1)
  • Hdel/del: dictDelete—O(1)

4.整数集合

整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

typedef struct intset {
    // 编码方式
    uint32_t encoding;

    // 集合包含的元素数量
    uint32_t length;

    // 保存元素的数组
    int8_t contents[];

} intset;

常用操作:

  • sadd:intsetAdd—O(1)
  • smembers:intsetGetO(1)—O(N)
  • srem:intsetRemove—O(N)
  • slen:intsetlen —O(1)

5.压缩列表

压缩列表是列表键和hash键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。

这里写图片描述

zlbytes代表整个压缩列表占用的内存字节数。
zltail代表压缩列表起始地址到尾节点的偏移量。
zllen节点数量
entry 节点
zlend 0xFF标记压缩列表末端。

常用操作:

  • Push/hset:ziplistPush/ziplistInsert 平均O(N), 最坏O(N2)
  • Llen/hlen:ziplislen平均O(N), 最坏O(N2)
  • Hget:ziplistGet —-O(1)
  • Index:ziplistIndex—-O(N)
  • Hdel/pop:ZiplistDelete—平均O(N), 最坏O(N2)
  • 遍历:ziplistPrev/ziplistNext—-O(1)

6. 对象

这里写图片描述

字符串对象

字符串对象的编码可以是int,raw,embstr
如果是整数值,则编码是int,如果是小于32字节的字符串,则编码是embstr,如果是大于32字节的字符串,则编码是raw。

下面是他们的对象结构:

这里写图片描述

这里写图片描述

这里写图片描述

embstr和raw的区别是 raw编码调用了两次内存分配函数来分配redisObject结构和sdshdr结构,而embstr只需要调用一次分配一段连续的空间。

列表对象

列表对象的编码可以是ziplist或者linkedlist。

当列表对象可以同时满足一下两个条件时,列表对象使用ziplist编码:
- 列表保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个。

不能满足这两个条件的都是用LinkedList编码。

下面就是这个对象的两种编码的结构:

这里写图片描述

这里写图片描述

每个StringObject实际上就是一个sdshdr。

哈希对象

哈希对象的编码可以是ziplist或者是hashtable。

当哈希对象同时满足以下两个条件时,哈希对象使用ziplist对象:
- 哈希对象保存的所有键和值的字符串长度都小于64个字节;
- 哈希对象保存的键值对数量小于512个

不能满足这两个条件的哈希对象需要使用hashtable编码。

下面就是这个对象的两种编码的结构:

这里写图片描述

这里写图片描述

这里写图片描述

集合对象

集合对象的编码可以是intset和hashtable

当集合对象同时满足以下条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个

不能满足这两个条件的集合对象需要使用hashtable编码。

下面就是这个对象的两种编码的结构:

这里写图片描述

这里写图片描述

有序集合对象

有序集合对象的编码可以是ziplist和skiplist

当集合对象同时满足以下条件时,对象使用intset编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节

不能满足这两个条件的集合对象需要使用skiplist编码。

下面就是这个对象的两种编码的结构:

ziplist编码的有序集合对象其实跟集合对象的差不多,区别是集合元素按分值从小到大进行排序。

这里写图片描述

7.对象的引用计数

因为C语言并不具备自动内存回收功能,所以redis对象系统构建了一个引用计数refcount技术来实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象进行内存回收。

  对象的引用计数信息会随着对象的使用状态而不断变化:

  • 在创建一个新对象时,引用计数的值会被初始化为1;
  • 当对象被一个新程序使用时,它的引用计数值会被增1
  • 当对象不再被一个程序使用时,它的引用计数会减1
  • 当对象的引用计数为0时,对象所占用的内存会被释放。

猜你喜欢

转载自blog.csdn.net/qq_33394088/article/details/80527255
今日推荐