吐血整理:Redis的基本数据类型,你懂多少?

Redis的基本数据类型,你真的懂了吗?

    • 前言
    • Redis的基本数据类型
      • 1、string(字符串)
      • 2、list(有序列表)
      • 3、hash(字典)
      • 4、set(集合)
      • 5、zset(有序集合)
      • 其他高级用法
    • 文章最后

前言

之前项目有使用过redis做缓存,对redis的五种基本类型只是一知半解,懂得如何去使用,但是没有深入探究。最近在读老钱的《redis深度历险:核心原理与应用实践》这本书,觉得书上对redis的探索还是比较深入的,同学们有兴趣可以去了解一下。话不多说,直接上干货。


Redis的基本数据类型

redis有5种基本的数据类型:

  • string(字符串)
  • list(有序列表)
  • hash(字典)
  • set(集合)
  • zset(有序集合)

1、string(字符串)

  • 数据结构:内部是一个字符数组,通过预分配的冗余空间的方式减少内存的频繁分配,类似 Java 的 ArrayList。
  • 扩容机制:当字符串小于1MB的时候,扩容是加倍现有的空间。当字符串长度超过1MB的时候,每次只会扩容1MB的空间,最大的长度为512MB。
  • 应用场景
    1. 简单的 key-value 缓存
    2. 作为系统的实时计数器,支持 incr(自增)操作
    3. 共享用户 session

2、list(有序列表)

  • 数据结构:它是一个双向链表,类似于Java的LinkedList,这就意味着list的插入和删除是很快的,但是查询会比较慢。
  • 应用场景
    1. 消息队列:使用左进右出的命令设计队列,用于消息排队和异步处理逻辑
    2. 栈:左进左出
    3. 列表分页展示:当我们的数据量越来越大的时候,往往需要分页展示,该数据结构有序,并且支持范围获取元素,可以完美地完成分页查询的功能。

深入 list 的底层存储结构

  • 列表元素在较少的情况下,会使用一块连续的内存存储,所有元素彼此紧挨着一块存储,这个结构是 ziplist(压缩列表)

  因为普通链表需要附加的指针空间太大,会造成空间浪费,也会加重内存的碎片化。例如我们的链表中存的是int型(4字节)的数据,如果用普通链表的话,结构上需要额外的 prev 和 next 指针(每个指针占8个字节)。造成不必要的浪费。

  • 当数据量多的情况下,会改为 quicklist(快速链表)

  快速链表是 ziplist(压缩列表)和 linkedlist(双端链表)的结合,也就是将多个 ziplist 使用双向指针串起来使用。

探索“压缩列表”的内部

  • < zlbytes >: 32bit,表示 ziplist 占用的字节总数(也包括< zlbytes >本身占用的4个字节)。
  • < zltail_offset >: 32bit,表示ziplist表中最后一项(entry)在 ziplist 中的偏移字节数。(为了快速定位最后一项的位置,在末尾进行新增或删除)
  • < zllen >: 16bit,表示 ziplist 中数据项(entry)的个数。(可以表达的最大值为2^16-1,如果数据项个数超过了16bit能表达的最大值,ziplist仍然可以来表示。如果< zllen >16bit全为1的情况,那么< zllen >就不表示数据项个数了,这时候要想知道 ziplist 中数据项总数,那么必须对 ziplist 从头到尾遍历各个数据项,才能计算出来。)
  • < entry >: 表示真正存放数据的数据项,长度不定。(内部有一个 prevlen 字段,表示前一个 entry 的字节长度,通过这个字段快速定位到上一个元素的起始位置,当字符串长度小于254时,使用1个字节表示;当超过254的时候,使用5个字节表示,第一个字节是0xfe,区分结束标记)
  • < zlend >: ziplist 最后1个字节,是一个结束标记,值固定等于255。

探索“快速链表”的内部

快速链表是 ziplist(压缩列表)和 linkedlist(双端链表)的结合,也就是将多个 ziplist 使用双向指针串起来使用。
吐血整理:Redis的基本数据类型,你懂多少?_第1张图片

为什么要这样设计“快速链表”?

  总结起来,大概又是一个空间和时间的折中:

  > 双向链表便于在表的两端进行 push 和 pop 操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。

  > ziplist 由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的 realloc。特别是当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝,进一步降低性能。

  quicklist 内部默认单个 ziplist 长度为8KB。


3、hash(字典)

  • 数据结构:使用“数组+链表”的二维结构,相当于Java的HashMap,是一个无序的字典,内部存储的是键值对。
  • 应用场景:一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。

  优点:可以对信息进行部分获取,减少网络流量的浪费。
  缺点:hash 结构的存储消耗要高于单个字符串,而且我们现在很多的对象都是比较复杂的,不适应于嵌套对象的场景。

  • 渐进式 rehash 策略

      字典结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的,但是在字典扩容或者缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。

      在 rehash 过程中,查询会同时查询两个 hash 结构。

      待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。

吐血整理:Redis的基本数据类型,你懂多少?_第2张图片

4、set(集合)

  • 数据结构:内部的键值对是无序的、唯一的。内部实现相当于一个特殊的字典(hash),字典中所有的 value 都是 NULL。类似于 Java 中的 HashSet。
  • 应用场景
    1. 数据去重
    2. 将两个set做交集、并集、差集等操作:
      • 交集:sinterstore set12 set1 set2
      • 并集:sunion set1 set2
      • 差集:sdiff set1 set2

5、zset(有序集合)

  • 数据结构:一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每一个 value 赋予一个 score,代表这个 value 的排序权重,内部使用“跳跃列表”这种数据结构。即内部实现为一个 hash 字典加一个跳跃列表(skiplist)
  • 应用场景
    1. 排行榜;
    2. 用 zset 来做带权重的队列,比如普通消息的 score 为1,重要消息的 score 为2,然后工作线程可以选择按 score 的倒序来获取工作任务。让重要的任务优先执行。

探究“跳跃列表”的底层

普通链表查询“50”这个节点,需要由根节点遍历链表,时间复杂度为 O(n):


这时,如果我们给链表加一层索引,可以看到我们遍历的节点数变少了:


redis的跳跃列表最多有64层,容纳2^64个元素应该不成问题。

吐血整理:Redis的基本数据类型,你懂多少?_第3张图片

跳跃列表——查找过程:从最高层的索引开始遍历,找到第一个节点(最后一个比“我”小的元素),然后从这个节点开始,降一层再遍历,找到第二个节点(最后一个比“我”小的元素),以此类推,降到最底层进行遍历,就可以找到我们期望的节点。

  我们将中间经过的一系列节点称之为“搜索路径”。

吐血整理:Redis的基本数据类型,你懂多少?_第4张图片

跳跃列表——插入节点

  1. 定位到最底层的数据链表要插入的位置
  2. 插入数据
  3. 调整索引

随机判断:对每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数,直观上期望的目标是50%的概率被分配到第一层,25%的概率分配到第二层,以此类推,2^(-63)的概率被分配到最顶层。

  注意的是,如果要插入第n层的索引的时候,同时也要插入1-(n-1)层索引。


跳跃列表——删除节点:和插入过程类似,首先把搜索路径查出来,定位到数据的位置,删除数据节点,如果有索引的话,要同时删除每一层的索引节点。


跳跃列表——更新过程:当我们调用 zadd 方法时,如果对应的 value 不存在,那就是插入过程。如果这个 value 已经存在了,只是调整了 score 的值,就需要走一个更新流程。假设这个 score 不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了。否则就对位置进行调整。(简单的策略就是,先删除这个元素,再插入这个元素)

这里就会有一个常见的面试题:为什么 zset 使用跳表而不使用红黑树呢?

  1. 跳表在代码上实现起来比红黑树简单;
  2. 跳表的区间查询效率会比较高,只需要定位左边界,然后遍历就行了。而红黑树区间查询需要通过中序遍历,效率相对慢;
  3. 红黑树要通过左右旋的方式左右子树的平衡,而跳表只需要通过随机函数就能维护这种平衡性。

跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。跳表是一种动态数据
结构,支持快速的插入、删除、查找操作,时间复杂度都是O(logn)。


其他高级用法

  1. Bitmap(位图):支持按 bit 位来存储信息,可以用来实现布隆过滤器(BloomFilter);
  2. HyperLogLog:提供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV(单位时间内有多少个访问者来到相应的页面);
  3. Geospatial:可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。

文章最后

如果你看到最后,相信你也是收获满满。可能有些小伙伴会有疑惑,我不了解底层结构,依然可以很容易上手 redis,那我了解其底层的意义是什么呢?

  我看过这么一篇推文,推文标题为:
《 redis探秘:选择合适的数据结构,减少80%的内存占用,这些点你get到了吗?》

  有兴趣的小伙伴可以去看看这篇文章:https://mp.weixin.qq.com/s/uzuz7rqctQ-bjdRcf1tO9g

  最后就是:数据结构没有最好的,只有最合适的,这就是为什么要了解其数据结构的原因!

你可能感兴趣的:(redis,java)