redis数据结构

redis中的数据结构
redis总结

1.字符串--SDS

struct sdshdr {
    unsigned int len;  //字符长度
    unsigned int free; //当前可用空间
    char buf[];        //具体存放字符的buf
};

通过预分配内存和维护字符串长度,实现动态字符串
减少修改字符串带来的内存分配次数(空间预分配和惰性空间释放)
(1)空间预分配:小于1MB,多分配与len同样大小的未使用空间,大于1MB,多分配1MB的未使用空间
(2)惰性空间释放:字符串收缩多余的的字节存放在free等待将来使用

Bitmaps

String类型上的一组面向bit操作的集合,最大长度是512m, 所以bitmaps能最大设置2^32个不同的bit
1.Bitmaps的最大优点就是存储信息时可以节省大量的空间
2.Bitmaps局限性:样本分布不均匀,造成空间上的浪费,以及key值如果是url等可能hash出相同的int值,降低Bitmap准确性
3.Bitmaps使用场景:各种实时分析和存储与对象ID关联的节省空间并且高性能的布尔信息(例如使用 bitmap 实现用户上线次数统计、统计活跃用户)

vs 布隆过滤器
Bloom Filter与单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应,从而降低了冲突的概率

2. list -- 双端链表

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

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;
双端链表
  • 双端链表
  • 无环
  • 多态: void*保存节点值

利用redis构建轻量级消息队列
(1)维护两个队列:pending 队列和 doing 表(hash表)
(2)由 pending 队列出队后,workers 分配一个线程去处理任务,并将时间戳和当前线程名称以及任务 id 写入 doing 表,如果 worker 完成消息的处理则自行擦除 doing 表中该任务 id 信息,如果处理业务失败,则主动回滚
(3)启用一个定时任务,每隔一段时间去扫描doing队列,检查每隔元素的时间戳,如果超时,把任务 rollback,即把该任务从 doing 表中删除,再重新 push 进 pending 队列

3.hash 表

typedef struct dictEntry { //保存K-V值的结构体
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;  //one of 结构
    struct dictEntry *next;//链地址法解决冲突
} dictEntry;

typedef struct dictType {
    uint64_t (*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;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht { //哈希表结构体
    dictEntry **table;//指向kv结构;
    unsigned long size;//表大小
    unsigned long sizemask;//计算索引值
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;//操作特定类型键值对的函数
    void *privdata;//私有数据指针, 传递给类型特定函数的可选参数
    dictht ht[2];//两个bucket,用于扩容
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */ //-1时候不在扩容中
    unsigned long iterators; /* number of iterators currently running */ //运行的迭代器数
} dict;

/* If safe is set to 1 this is a safe iterator, that means, you can call
 * dictAdd, dictFind, and other functions against the dictionary even while
 * iterating. Otherwise it is a non safe iterator, and only dictNext()
 * should be called while iterating. */
typedef struct dictIterator {
    dict *d;
    long index;
    int table, safe;
    dictEntry *entry, *nextEntry;
    /* unsafe iterator fingerprint for misuse detection. */
    long long fingerprint; //安全迭代器,能迭代所有已存在的kv;先rehash再迭代
} dictIterator;
hash表

平滑扩容, 定义了ht[2]这个数组字段. 其用意是这样的:

  1. 一般情况下, 字典dict仅持有一个哈希表dictht的实例, 即整个字典由一个bucket实现.
  2. 随着插入操作, bucket中出现冲突的概率会越来越大, 当字典中存储的结点数目, 与bucket数组长度的比值达到一个阈值(1:1)时, 字典为了缓解性能下降, 就需要扩容
  3. 扩容的操作是平滑的, 即在扩容时, 字典会持有两个dictht的实例, ht[0]指向旧哈希表, ht[1]指向扩容后的新哈希表. 平滑扩容的重点在于两个策略:
  4. 后续每一次的插入, 替换, 查找操作, 都插入到ht[1]指向的哈希表中
  5. 每一次插入, 替换, 查找操作执行时, 会将旧表ht[0]中的一个bucket索引位持有的结点链表, 迁移到ht[1]中去. 迁移的进度保存在rehashidx这个字段中.在旧表中由于冲突而被链接在同一索引位上的结点, 迁移到新表后, 可能会散布在多个新表索引中去.
  6. 当迁移完成后, ht[0]指向的旧表会被释放, 之后会将新表的持有权转交给ht[0], 再重置ht[1]指向NULL

这种平滑扩容的优点有两个:

  1. 平滑扩容过程中, 所有结点的实际数据, 即dict->ht[0]->table[rehashindex]->k与dict->ht[0]->table[rehashindex]->v分别指向的实际数据, 内存地址都不会变化. 没有发生键数据与值数据的拷贝或移动, 扩容整个过程仅是各种指针的操作. 速度非常快
  2. 扩容操作是步进式的, 这保证任何一次插入操作都是顺畅的, dict的使用者是无感知的. 若扩容是一次性的, 当新旧bucket容量特别大时, 迁移所有结点必然会导致耗时陡增.

链地址法解决hash冲突
rehash负载因子: used/size
平滑扩容
(1)后续每一次的插入,替换,查找操作,都插入到ht[1]指向的哈希表中
(2)每一次插入,替换,查找操作执行时,会将旧表ht[0]中的一个bucket索引位持有的结点链表,迁移到ht[1]中去,迁移的进度保存在rehashidx这个字段中。在旧表中由于冲突而被链接在同一索引位上的结点,迁移到新表后,可能会散布在多个新表索引中去
(3)当迁移完成后, ht[0]指向的旧表会被释放,之后会将新表的持有权转交给ht[0], 再重置ht[1]指向NULL
字典遍历迭代器
(1)安全迭代器: 禁止rehash(bgaofrewrite和bgsave指令需要遍历所有对象进行持久化, 不允许出现重复)
(2)不安全迭代器: rehash中会发生元素重复遍历

4.set --intset 对象保存的所有元素都是整数值

否则,用hashtable

typedef struct intset {
    uint32_t encoding;  // 编码方式
    uint32_t length;    // 集合包含的元素数量
    int8_t contents[];   // 保存元素的数组
} intset;

//3种编码方式
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

1.凡有一个数值元素的值超过了int32_t的取值范围, 整个intset都要进行升级, 即所有的数值都需要以int64_t的形式存储. 显然升级的开销是很大的
2.intset中的数值是以升序排列存储的, 插入与删除的复杂度均为O(n). 查找使用二分法, 复杂度为O(lgn)
3。intset的代码实现中, 不预留空间, 即每一次插入操作都会调用zrealloc接口重新分配内存. 每一次删除也会调用zrealloc接口缩减占用的内存. 省是省了, 但内存操作的时间开销上升了.
4.intset的编码方式一经升级, 不会再降级.

总之, intset适合于如下数据的存储:
所有数据都位于一个稳定的取值范围中. 比如均位于int16_t或int32_t的取值范围中
数据稳定, 插入删除操作不频繁. 能接受O(lgn)级别的查找开销

5. zset 有序集--ziplist / dict + skiplist

5.1 ziplist

ziplist内存

entry

当数据量少时, sorted set是由ziplist来实现的。任意类型数据
(1)zlbytes存储整个ziplist所占用的内存的字节数
(2)zltail指的是ziplist中最后一个entry的偏移量
(3)zllen指的是整个ziplit中entry的数量
(4)zlend是一个终止字节, 其值为全F, 即0xff
(5)每个entry中prevlen存储了它前一个entry所占用的字节数,encoding存储着当前结点的数据类型:(二进制,还是数值。。。。),如何编码(如果是数值,数值如何存储,如果是二进制数据,二进制数据的长度),data存储真实的数据

5.2 dict+skiplist

当sorted set中的元素个数, 即(数据, score)对的数目超过128的时候或者当sorted set中插入的任意一个数据的长度超过了64, sorted set是由dict + skiplist来实现

数据结构中跳表原理

跳表

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
redis中跳表skiplist

1.skiplist 在有序链表和多层链表的基础上发展起来的,每个节点随机出一个层数(level),除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多),这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置,同时插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整,降低了插入操作的复杂度

  1. level[]是一个柔性数组,存放指向各层链表后一个节点的指针(后向指针),另外,每个后向指针还对应了一个span值,它表示当前的指针跨越了多少个节点,span用于很方便地计算出元素排名(rank)
  2. 分数(score)允许重复,即skiplist的key允许重复,经典skiplist中是不允许的
  3. 在比较时,不仅比较分数(相当于skiplist的key),还比较数据本身,在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识,当多个元素分数相同的时候,还需要根据数据内容来进字典排序

对比其树

  1. skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的, 而哈希表不是有序的,在哈希表上只能做单个key的查找, 不适宜做范围查找
  2. 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂, 而skiplist的插入和删除只需要修改相邻节点的指针, 操作简单又快速
    3.查找单个key,skiplist和平衡树的时间复杂度都为O(log n),
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343