redis的set hash命令 redis set hash区别_数据结构


目录

  • 前言
  • Hash 字典
  • 是什么
  • 为什么
  • 如何实现
  • 渐进式rehash
  • Set 集合
  • 是什么
  • 为什么
  • 如何实现
  • ZSet 有序集合
  • 是什么
  • 怎么实现
  • 为什么
  • 总结


前言

1. 一文干翻Integer、int等基础数据类型和包装类型相关问题2. 面试必问 容器 ArrayList3. 面试必问 Redis 持久化4. 面试必问 Redis 数据结构底层原理一5. 面试必问 Redis 数据结构底层原理二 前面已经讲了面试必问 Redis 数据结构底层原理String、List篇;
链接如下:

redis版本:6.0.6

Hash 字典

是什么

听名称就知道很像Java中的HashMap,原理也有很多相似之处,但是Reids做了很多优化,Redis的hash的底层存储有两种数据编码,一种是ziplist,另外一种是hashtableziplist之前已经讲过了。

redis的set hash命令 redis set hash区别_redis_02

我们来用redis自带的debug命令来看下hash类型的encoding编码字段:

ziplist

> hset key4 name Nick
1
> debug object key4
Value at:0x7f21f2eadde0 refcount:1 encoding:ziplist serializedlength:24 lru:13214504 lru_seconds_idle:21

hashtable

> hset key5 name 01234567890123456789012345678901234567890123456789012345678901234
1
> debug object key5
Value at:0x7f21f2eade00 refcount:1 encoding:hashtable serializedlength:28 lru:13214661 lru_seconds_idle:3

hash类型,会优先使用ziplist编码,当下面任一条件被破坏了才会采用hashtable编码

  • hash对象保存的键和值字符串长度都小于64字节
  • hash对象保存的键值对数量小于512 ziplist

这两个条件是可以修改的,在redis.conf

hash-max-ziplist-value 64     #字符串长度都小于64字节
hash-max-ziplist-entries 512  #元素数量小于512

为什么

为什么要优先使用ziplist压缩列表?

  • 压缩列表是非常紧凑的数据结构,占用的内存已经压缩的非常小了,可以提高内存的利用率。
  • 压缩列表底层是连续的内存空间,对CPU高速缓存支持更友好,提高查询效率。

如何实现

下面来剖析hashtable原理;

是什么

其实就是上一篇中的dict,内部有2个hashtable,通车情况下只有一个hashtable是有值的,但是在字典扩容、缩容的时候,需要用的另一个hashtable过渡,进行渐进式搬迁,搬迁完成后,旧的hashtable删除,新的hashtable取而代之。

typedef struct dict {
    // 包含2个hashtable
    dictht ht[2];
    // ...
} 

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

typedef struct dictEntry {
    // 键
    void *key;
    // 值
    void *val;
    // 指向下个哈希表节点,形成链表
    dictEntry *next;

}

结构图如下:

redis的set hash命令 redis set hash区别_redis的set hash命令_03

为什么

为什么内部要用2个hashtable

核心原理基本上跟Java中HashMap一样,随着对哈希表的操作,键会逐渐增多或减少,也会存在hash冲突,解决版本也是链表法。为了保证查询时间复杂度不会退化为链表,所以也会进行rehash操作。

与HashMap最大的不同在rehash的方式,Java中的HashMap一次性把所有数据rehash完成,由于rehash是个很耗时的过程,而redis核心处理线程模型又是单线程的,唯恐阻塞其他指令,所以不能像HashMap的一次性处理方式,而选择了渐进式rehash方式。

如何实现

渐进式rehash

为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2,起始时哈希表2没有分配空间,但是随着数据增多,Redis执行分三步执行rehash;

  • 给哈希表2分配更大的内存空间,如是哈希表1的两倍
  • 把哈希表1中的数据重新映射并拷贝到哈希表2中
  • 释放hash1的空间

由于步骤2重新映射非常耗时,会阻塞redis,于是把集中迁移数据,改成每处理一个请求时,就从哈希表1中的第一个索引位置, 顺带将这个索引位置上的所有entries拷贝到哈希表2中。

触发前提

数据已经达到rehash条件,hash表中的元素等于数组长度时。

  • 被动式:每次客户端发起命令时,做rehash(如hadd、hdel等);
  • 主动式:服务器定时rehash;

扩容倍数

这块倒是跟HashMap一样是2倍扩容

缩容

当hash表中元素被删除到元素个数小于数组长度的10%,就会去缩容。

Set 集合

是什么

类似于Java中的HashSet,内部实现是一个特殊的字典,字典中所有的value都是null,常被用于去重功能。

底层采用了intset整数集合和hashtable字典两种方式来实现的,当满足如下两个条件的时候,采用整数集合实现,否则用哈希表。

  • 集合中的所有元素都为整数
  • 集合中的元素个数不大于 512可以通过修改 set-max-intset-entries调整集合大小,默认512
set-max-inset-entries 512

其中hashtablekeyset中元素的值,而valuenullinset为可以理解为数组。

intset

> sadd key6 1 2 3
3
> debug object key6
Value at:0x7f21f2eaddd0 refcount:1 encoding:intset serializedlength:15 lru:13223640 lru_seconds_idle:7

hashtable

> sadd key6 a b c
3
> debug object key6
Value at:0x7f21f2eaddd0 refcount:1 encoding:hashtable serializedlength:13 lru:13223654 lru_seconds_idle:5

为什么

为什么要优先使用intset整数集合?

  • intset是非常紧凑的数据结构,占用的内存已经压缩的非常小了,可以提高内存的利用率。
  • 查询方式一般采用二分查找法,实际查询复杂度也就在log(n).
  • intset底层是连续的内存空间,对CPU高速缓存支持更友好,提高查询效率。

如何实现

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

redis的set hash命令 redis set hash区别_数据结构_04

intset底层实现为有序无重复数组保存集合元素。 intset这个结构里的整数数组的类型可以是16位的,32位的,64位的。如果数组里所有的整数都是16位长度的,如果新加入一个32位的整数,那么整个16的数组将升级成一个32位的数组。升级可以提升intset的灵活性,又可以节约内存,但不可逆。

ZSet 有序集合

是什么

类似Java中的TreeMap,用set保证value一致性,给每个value一个score属性代表排序权重。

redis的set hash命令 redis set hash区别_redis的set hash命令_05

zset有序,自动去重的集合数据类型,其底层实现为 ziplist + skiplist跳跃表,当数据比较少的时候用ziplist编码结构存储,否则改为跳跃表。

默认按照score排序,score相同则按照元素字典序

同时满足以下两个条件采用ziplist存储:

  • 有序集合保存的元素数量小于默认值128个
  • 有序集合保存的所有元素的长度小于默认值64字节

可以通过修改redis.conf修改默认值

zset-max-ziplist-entries 128 #配置元素个数最多512个
zset-max-ziplist-value 64 #配置value最大为64字节

ziplist

> zadd key7 1 a 2 b
2
> debug object key7
Value at:0x7f21f2eade10 refcount:1 encoding:ziplist serializedlength:22 lru:13224623 lru_seconds_idle:6

skiplist

> zadd key7 1 a 2 b67777777777777777777777777777777777777777777777777777777777777777777777777777777
1
> debug object key7
Value at:0x7f21f2eade10 refcount:1 encoding:skiplist serializedlength:43 lru:13224690 lru_seconds_idle:2

怎么实现

ziplist,之前讲不过,zskiplist编码其实就是zset

typedef struct zset {    
    // 字典,键为成员,值为分值    
    // 用于支持 O(1) 复杂度的按成员取分值操作    
    dict *dict;    
    // 跳跃表,按分值排序成员    
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作    
    // 以及范围操作    
    zskiplist *zsl;
}

typedef struct zskiplist {
     // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
 } zskiplist;
typedef struct zskiplistNode {
    // 成员对象
    robj *obj;
    // 分值
    double score;
     // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
         // 跨度---前进指针所指向节点与当前节点的距离
        unsigned int span;
    } level[];
} zskiplistNode;

zskiplist编码分为两部分,dict+ zskiplistdict跳跃表都存储数据,实际上 dict 和跳跃表最终使用指针都指向了同一份数据,即数据是被两部分共享的,dict结构,主要key是其集合元素,而value就是对应分值,而zkiplist作为跳跃表,按照分值排序,方便定位成员。

盗个图:

redis的set hash命令 redis set hash区别_redis_06

来源:

skiplist的查找时间复杂度是 O(log N),可以和平衡二叉树相当,但实现起来又比它简单。

为什么

这里redis为什么用跳表而不像Java的TreeMap一样用红黑树呢?

对于这个问题,Redis的作者 @antirez 是怎么说的:

There are a few reasons:

1) They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

简单概括下

  • 内存占用相比于树更少,跳跃表使用的指针比树更少,64位操作系统每个指针占用8字节
  • 跳跃表范围查询支持更友好
  • 算法实现难度上相比于树实现起来更简单

总结

redis涉及的常用基本数据结构和相关底层实现数据编码全部介绍完了,下面总结一下:

数据结构

底层实现(数据编码)

string

int、raw、embstr

list

quicklist

set

intset/hashtable

hash

ziplist/hashtable

zset

ziplist/skiplist

底层实现(数据编码介绍):

  • int:其实就是long型
  • ziplist(压缩列表):内存地址连续,元素之间紧凑存储,功能类似链表的一种数据结构。
  • quicklist(快速列表):是ziplist和linkedlist的结合。
  • intset(整数集合):用有序无重复数组保存集合元素。
  • hashtable(字典或者也叫哈希表):就是dict,里面有个hashtable数组ht[0]、ht[1],用于渐进式rehash,可以看做升级版的HashMap
  • skiplist(跳跃表):查找时间复杂度是 O(log N),可以和平衡二叉树相当,但实现起来又比它简单。