一.redis数据类型和底层数据结构的对应关系

     

Redis核心技术与实战-学习笔记(二)_数据

二.Redis 使用哈希表实现从键到值的快速访问

哈希表的O(1)复杂度和快速查找特性

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。

一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。

不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针地址。

哈希桶中的entry元素保存了*key和*value指针,分别指向了实际的键和值,这样一来。即使值一个集合,也可以通过*value指针被查找到。

Redis核心技术与实战-学习笔记(二)_database_02

哈希表最大的好处就是可以用O(1)的时间复杂度来快速查找键值对

---我们只需计算键的哈希值,就可以知道哈希桶的位置,然后访问相应的entry元素。

三.哈希表的冲突问题和rehash可能带来的操作阻塞。

    当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这就是哈希冲突,两个key的哈希值和哈希桶计算对应关系时,正好落到同一个哈希桶中。

链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

     

Redis核心技术与实战-学习笔记(二)_数据库_03

  但是,这里依然存在一个问题,哈希冲突链上的元素只能通过指针逐以查找再操作。如果哈希表里写入的数据越来越多,哈希冲突越来越多,会导致哈希冲突链过长,进而导致这个链上的元素查找耗时过长,效率降低。

rehash操作。rehash也就是增加现有的哈希桶数量,让逐渐增多的entry元素能在更多桶之间分散保存,减少单个桶中的元素数量,从而减少哈希冲突。

哈希表1和哈希表2。

      一开始当插入数据时,默认使用哈希表1,此时哈希表2并没有被分配空间。随着数据的增多redis开始rehash,这个过程分为三步:

分配更大的空间,例如是当前哈希表1大小的两倍

      2.把哈希表1中的数据重新映射并拷贝到哈希表2中

释放哈希表1的空间

这个过程第二步涉及大量数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成Redis线程阻塞没无法服务其他请求。此时,Redis无法处理其他请求。

四.渐进式 rehash

Redis仍然可以正常处理客户端请求,

从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中;即将哈希冲突链复制过去。下一次也一样

一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

      渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,redis本身还有一个定时任务来执行,会周期性的搬移数据到新哈希表中,缩短整个rehash的过程。

五.集合数据操作效率

      Redis 的键和值是怎么通过哈希表组织的了。对于 String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度了。

通过全局哈希表找到对应的哈希桶位置,第二步在集合中再进行增删改查。

    压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,

压缩列表在表头有三个字段zlbytes,zltail和zllen,分别表示列表长度,列表尾的偏移量和列表中的entry个数;压缩列表在表尾还有一个zlend,表示列表结束。

Redis核心技术与实战-学习笔记(二)_数据_04

查找第一个元素,时间复杂度是O(1);

查找最后一个元素通过zltail时间复杂度是O(1);

查找其他元素时候,只能逐个查找,时间复杂度是O(n);

跳表

    有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。

    跳表是在链表地基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位

链表的二分查找)

Redis核心技术与实战-学习笔记(二)_时间复杂度_05

跳表的核心是链表是有序的,所以查找时间复杂度跟二分查找一样是O(logn)

不同数据结构查找的时间复杂度

名称

时间复杂度

哈希表

O(1)

跳表

O(logn)

双向链表

O(n)

压缩列表

O(n)

整数数组

O(n)

不同操作的复杂度

  1. 单元素操作是基础;
  2. 范围操作非常耗时;
  3. 统计操作通常高效;
  4. 例外情况只有几个;

第一.单元素操作是指每一种集合类型对单个数据实现的增删改查操作。

hash类型的hget,hset,hdel,是对哈希表或压缩列表操作,时间复杂度是O(1);

set类型sadd,srem,srandmember是对哈希表或者整数数组操作,时间复杂度是O(1);

hash 类型的 HMGET 和 HMSET,Set 类型的 SADD 也支持同时增加多个元素复杂度就从 O(1) 变成 O(M) 了。

第二,范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据。

hash类型中的hgetall

set类型中的smembers

或者返回一个范围内的部分数据,

list类型的lrange和zset类型的zrange。

时间复杂度是O(n)。比较耗时,我们应该尽量避免。

Redis从2.8版本开始提供SCAN系列操作(包括HSCAN,SSCAN和ZSCAN),这类操作实现渐进式遍历,每次只返回有限数量的数据。避免相比如hgetall,smembers这种操作一次性返回所有元素导致的redis阻塞。

第三,统计操作,是指集合类型对集合中所有元素个数的记录

时间复杂度只有O(1),这些集合采用压缩列表,双向链表或者整数数组的时候,会专门记录元素的个数统计,以空间换时间的形式减少时间复杂度。

第四,例外情况

list类型的lpop,rpop,lpush,rpush四个操作来说,可以直接通过偏移量定位尾部元素,时间复杂度也是O(1),实现快速操作。

六. 整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

内存利用率,数组和压缩列表都是非常紧凑的数据结构,他比链表占用内存少。redis是内存数据库,大量数据存在内存中,此时需要做尽可能的优化,提高内存利用率。

CPU告诉缓存支持更友好,所以redis在集合数据元素较少的情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阀值,避免查询时间过高,转为哈希表和跳表数据结构存储,保证查询效率。