Redis深度历险笔记01 Redis数据结构


Redis所有的数据结构都是以唯一的key字符串作为名称,然后通过这个唯一的key来获取响应的value数据。value有5种基础数据结构,分别是string(字符串)、list(列表)、set(集合)、hash(字典)、zset(有序集合)。

1. 数据类型的应用场景

string:粉丝数
list:存储关注列表、粉丝列表
hash:用于存储用户信息,比如一个person对象,有姓名,年龄,生日等信息,就适合用hash来存储
set:存储关注人 粉丝等,redis实现了对两个set求交并差集,可以计算出两个人的共同关注等
zset:存储一个班级所以同学的考试成绩

2. string(字符串)

  1. Redis字符串是动态字符串,是可以修改的字符串,结构上类似于Java中的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。Redis 的字符串是一种简单动态字符串(SDS),因为普通的 String 字符串不记录长度信息,如果想知道它的长度,需要遍历整个字符串来得到,时间复杂度为 O(n),而 redis 的简单动态字符串中的 len 属性记录了长度信息,取得长度的时间复杂度仅为 O(1),这样确保了获取字符串长度的操作不会成为 redis 的性能瓶颈。
  2. 操作语句
    • set 进行写:set name test(mset name1 test1 name2 test2 name3 test3)
    • get 进行读:get name(mget name1 name2 name3)
    • expire 设置过期时间:expire name 5(表示5秒后过期)
    • setex 同时写和设置过期时间:setex name 5 test
    • setnx 表示 name不存在时才进行set:setnx name test
  3. 如果value的值是一个整数,name可以对它进行自增操作,如果整数超过Long的最大最小值,会报错
    • incr 表示自增1:incr age
    • incrby 表示自增指定值:incrby age 5

3. list(列表)

  1. Redis 列表相当于Java中的LinkedList,插入和删除非常快,时间复杂度是O(1),但是查找数据很慢,时间复杂度为O(n)。
  2. 可以用来做异步队列:将需要延后处理的任务序列化成字符串,存到 Redis 的 list 列表中,另一个线程从这个列表中轮询数据进行处理。
    • rpush 表示右边进,lpop 表示左边出,构成的是一个队列。
    • rpush 表示右边进,rpop表示右边出,构成的是一个栈。
  3. list底层结构:
    • Redis 底层存储的并不是一个简单的LinkedList,而是快速链表quickList。
    • 在列表元素较少时,使用一块连续的内存来储存,这个结构叫做zipList,也就是压缩链表,它将所有的元素紧挨着一起存储。
    • 当数据量较多时,改为quickList,这么做的原因是:普通的链表需要附加指针会占用空间,而且加重了内存的碎片化。所以Redis将链表和zipList结合组成了quickList,它将多个zipList通过双向指针串起来,这样既能满足快速插入删除性能,又不会出现严重的空间碎片化。

4. set(集合)

  1. Redis 的集合相当于Java 中的HashSet,它的内部实现相当于一个特殊的字典,字典中所有的value都是NULL。
  2. set集合中的元素时无序且唯一的,可以用来存储活动中奖的用户id,可以保证同一个用户不会中奖两次。
  3. 操作:
    • sadd book java(添加元素)
    • spop book(弹出一个元素)
    • scard book(获取长度,类似count)
    • smembers book(查看所有元素)

5. hash(字典)

  1. Redis的字典相当于Java中的HashMap。
  2. Redis的字典和HashMap的异同点
    • 相同点:无序,采用数组+链表结构。数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
    • 不同点:
      • Redis 字典的值只能是字符串,而HashMap的值可以是多种类型。
      • rehash的方式不一样,因为HashMap采用了一次性全部rehash。Redis为了高性能,不能阻塞服务,采用了渐进式rehash策略。渐进式 rehash 就是同时保留旧数组和新数组,在后续对 hash 的操作中渐渐的将旧数组中的数据迁移到新数组中,所以在操作处于 rehash 过程的字典时,需要同时访问新旧两个哈希表,如果在旧哈希表中找不到元素,就需要去新哈希表中查找。
  3. Redis中的每个字典都带有两个哈希表:ht[0]和ht[1],一个作为平时使用,一个在rehash时使用。
  4. Redis的哈希表示如何解决哈希冲突的:采用链地址法来解决键冲突问题,每个节点都有一个next指针指向另一个节点,通过指针将索引值相同的节点连接起来。
  5. rehash 当哈希表保存的键值对数量太多或者太少时,需要对哈希表的大小进行相应的扩展或者收缩。渐进式rehash步骤:
    • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个表。
    • 在字典中维持一个索引计数器rehashidx,值设为0,表示rehash工作正式开始。
    • 在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序就会顺带将ht[0]哈希表在rehashidx索引上所有的键值对rehash到ht[1],并将rehashidx的值加1。
    • 随着字典操作的不断执行,最终ht[0]的所有键值对都会被rehash到ht[1],这时程序将rehashidx的值设为-1,表示rehash操作已完成。然后将ht[0]的空间释放掉,将ht[1]设置为ht[0]。

6. zset(有序集合)

  1. 一方面它是一个set,保证了元素的唯一性。

  2. 另一方面它给每个元素赋予一个score,代表这个value的排序权重。可以根据score对元素进行排序。

  3. 使用场景:可以用来存粉丝列表(粉丝id,关注时间),存学术成绩(学生id,成绩)

  4. 操作:

    • zadd test 99 math
    • zrange test 60 100(区间查找60到100分的元素)
    • zrank test math 看排名
    • zcard test 相当于count
  5. zset底层数据结构

    • zset底层数据结构包括ziplist和skiplist,当有序集合保存的元素数量小于128个并且所有元素的长度都小于64字节时,使用ziplist,否则使用zskiplist和dict。
    • ziplist 作为zset底层数据结构时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。
    • zskiplist和dict作为zset底层存储结构时,dict保存key/value,便于通过key获取score;zskiplist保存有序的元素列表,便于按照分值对元素排序,便于执行range之类的命令。

跳跃表

  1. 跳跃表是一种链表加多层索引的结构,支持快速插入、删除、查找操作,平均查找复杂度是O(logN),空间复杂度是O(n)。

  2. 跳跃表由zskiplist和zskiplistNode两个结构构成。zskiplist结构用于保存跳跃节点相关的信息,比如节点的数量、表头表尾节点的指针等;zskiplistNode结构用于表示跳跃表节点。跳跃表节点中有一个level数组,代表层,数组中每个元素都包含一个指向其他节点的指针,可以通过这些层来加快访问其他节点的速度。节点中还保存了元素的引用和分值,跳跃表中所有的节点都按照分值从小到大排序。

  3. 为什么要使用跳表而不是红黑树来实现zset:

    • zset支持的操作有插入、删除、查找、有序输出所有元素、查找区间内所有元素。前面4项,两者效率差不多,但是最后一项,红黑树没有跳表效率高。
    • 在跳表中,要查找区间的元素,只需定位到两个区间端点在最底层的位置,然后顺序遍历元素即可,非常高效。而红黑树定位到端点后,每次都要查找后续节点,比较耗时。
  4. 为什么有序集合zset需要同时使用跳跃表和字典来实现

    • 如果只用字典实现有序集合,它可以以O(1)的复杂度查找元素的分值,但是对于zrank、zrange命令,都需要对所有元素排序,完成这种排序至少需要O(NlogN)的时间复杂度,以及O(N)的空间内存(因为要创建一个数组来保存排序后的元素)。
    • 如果只用跳跃表来实现有序集合的话,有高效执行范围操作的优点,但是根据成员查找分值这一操作的复杂度将从O(1)上升为O(N)。因此,为了让有序集合的查找分值和范围查找操作都尽可能快,redis选择同时使用字典和跳跃表来实现有序集合。两种数据结构通过指针共享元素,不会浪费内存
  5. Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是集群节点中用作内部数据结构。跳表是可以实现二分查找的有序链表(链表加上多级索引的结构,就是跳表)

  6. 跳跃表的时间复杂度为O(logn):标准化的跳表每两个元素提取出一个元素作为上一级的索引,也就是开始是1/2,然后1/4,1/8 … ,每一级索引减少上一级一半的元素,那么最后一级只有一个元素,也就是总元素个数 n/(2h) = 1 ,得到高度 h = logn,所以跳表的时间复杂度为 O(logn)。

    跳跃表的空间复杂度为O(n):如果每两个元素向上提取一个元素的索引,那么最后额外需要的空间是一个等比数列,为:n/2 + n/4 + n/8 … + 8 + 4 + 2 = n - 2 ,所以,跳表的空间复杂度为 O(n)。每三个节点抽取一个索引的话,额外需要的空间是n-1/2。

猜你喜欢

转载自blog.csdn.net/weixin_43338519/article/details/105514393