[redis] map和set

1、基础

1.1 hash命令

  • hset key k v:为指定的key设定k v键值对。
  • hget key k:返回指定的key中的k的值
  • hmset key k1 v1 k2 v2 …:设置key中的多个k v
  • hmget key k1 k2 …:获取key中的多个k的值
  • hexists key k:判断指定的key中的filed是否存在,1存在,0不存在
  • hlen key:获取key所包含的field的数量
  • hincrby key field increment:设置key中filed的值增加increment

1.2 set命令

  • sadd key value1、value2…:向set中添加数据
  • smembers key:获取set中所有的成员
  • scard key:获取set中成员的数量
  • sismember key member:判断member是否在该set中,1表示存在,0表示不存在或者该key本身就不存在
  • srem key member1、member2…:删除set中指定的成员
  • srandmember key:随机返回set中的一个成员
  • sdiff/sinter/sunion key1 key2:返回key1与key2的差集/交集/并集。
  • sdiffstore/sinterstore/sunionstore destination key1 key2:将差集/交集/并集存储在destination上

2、底层

2.1 字典

  字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。C 语言中没有内置这种数据结构的实现,所以字典依然是 Redis自己构建的。

  Redis 的字典使用哈希表作为底层实现。

字典的结构定义:

typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privedata;
    // 哈希表
    dictht  ht[2];
    // rehash 索引
    // 在rehash不在进行时,值为-1
    int rehashidx;
}dict;

哈希表结构定义:

typedef struct dictht{
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于计算索引值
    //总是等于 size-1
    unsigned long sizemask;
    //该哈希表已有节点的数量
    unsigned long used; 
}dictht

  哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,dictEntry 结构定义如下:

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

  key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。

  注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。这里采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突。

在这里插入图片描述

1. 哈希算法:Redis计算哈希值和索引值方法如下:

  1. 使用字典设置的哈希函数,计算键 key 的哈希值:hash = dict->type->hashFunction(key);
  2. 用哈希表的sizemask属性和第一步得到的哈希值,计算索引值。index = hash & dict->ht[x].sizemask;根据情况不同,ht[x]可以是ht[0]或者ht[1]

2. 解决哈希冲突:方法是链地址法。表头插入

3. 扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:

  1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也就是ht[0].used属性的值)

    • 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于 h t [ 0 ] . u s e d ∗ 2 ht[0].used * 2 ht[0].used2 2 n 2^n 2n

    • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于 h t [ 0 ] . u s e d ht[0].used ht[0].used 2 n 2^n 2n

  2. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新就按键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上

  3. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备)

哈希表的扩展与收缩:

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:

  • 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1
  • 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5

哈希表的负载因子计算公式:

负载因子=哈希表已保存节点数量/哈希表大小
load_factor = ht[0].used / ht[0].size

在执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免必要的内存写入操作,最大限度地节约内存

当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作

  1. 渐近式 rehash
    为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]

哈希表渐进式rehash的详细步骤:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
  3. 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序出了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
  4. 随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成

因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作会在两个哈希表上进行。而新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作

参考:https://blog.csdn.net/qq_40378034/article/details/89295462

2.2 整数集合

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

1、整数集合的实现

整数集合是Redis用于保存整数值的集合抽象数据结构,它可以保证类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素

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

contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序地排列,并且数组中不包含任何重复项

length属性记录了整数集合包含的元素数量,也即是contents数组的长度

contents数组的真正类型取决于enconding属性的值

2、升级

每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面

升级整数集合并添加新元素共分为三步进行:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间

  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变

  3. 将新元素添加到底层数组里面

一个包含三个int16_t类型的元素的整数集合:
在这里插入图片描述
contents数组的各个元素,以及它们所在的位:
在这里插入图片描述
从int16_t类型转换为int32_t类型:
在这里插入图片描述

整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存

整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值