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计算哈希值和索引值方法如下:
- 使用字典设置的哈希函数,计算键 key 的哈希值:
hash = dict->type->hashFunction(key);
- 用哈希表的sizemask属性和第一步得到的哈希值,计算索引值。
index = hash & dict->ht[x].sizemask
;根据情况不同,ht[x]可以是ht[0]或者ht[1]
2. 解决哈希冲突:方法是链地址法。表头插入
3. 扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
-
为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也就是ht[0].used属性的值)
-
如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于 h t [ 0 ] . u s e d ∗ 2 ht[0].used * 2 ht[0].used∗2的 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
-
-
将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新就按键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
-
当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时,程序自动开始对哈希表执行收缩操作
- 渐近式 rehash
为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]
哈希表渐进式rehash的详细步骤:
- 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
- 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始
- 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序出了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash至ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一
- 随着字典操作的不断进行,最终在某个时间点上,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、升级
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面
升级整数集合并添加新元素共分为三步进行:
-
根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
-
将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变
-
将新元素添加到底层数组里面
一个包含三个int16_t类型的元素的整数集合:
contents数组的各个元素,以及它们所在的位:
从int16_t类型转换为int32_t类型:
整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存
整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态