1、对象的类型与编码

Redis使用五大数据类型来表示键和值

string list hash set zset

每次在Redis中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redisObject 结构来表示:

  1. typedef struct redisObject{
  2. //类型
  3. unsigned type:4;
  4. //编码
  5. unsigned encoding:4;
  6. //指向底层数据结构的指针
  7. void *ptr;
  8. //引用计数
  9. int refcount;
  10. //记录最后一次被程序访问的时间
  11. unsigned lru:22;
  12. }robj

1、type属性

对象的type属性记录了对象的类型,这个类型就是前面讲的五大数据类型:
image.png
可以通过如下命令来判断对象类型:
image.png

在Redis中,键总是一个字符串对象,而值可以是字符串、列表、集合等对象,所以我们通常说的键为字符串键,表示的是这个键对应的值为字符串对象,我们说一个键为集合键时,表示的是这个键对应的值为集合对象。

2、encoding属性和*prt指针

对象的 prt 指针指向对象底层的数据结构,而数据结构由 encoding 属性来决定。
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图3
  而每种类型的对象都至少使用了两种不同的编码:
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图4
  可以通过如下命令查看值对象的编码:

OBJECT ENCODING key

  比如 string 类型:(可以是 embstr编码的简单字符串或者是 int 整数值实现)
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图5

字符串对象

字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。
  ①、编码
  字符串对象的编码可以是int,raw或者embstr。
  1、int 编码:保存的是可以用 long 类型表示的整数值。
  2、raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
  3、embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图6

  由上可以看出,int 编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存短字符串。其实 embstr 编码是专门用来保存短字符串的一种优化编码,raw 和 embstr 的区别:
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图7
  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图8
  embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间,而raw需要分配两次内存空间。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
  ps:Redis中对于浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型。
  ②、编码的转换
  当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。
  对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

3、列表对象

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。
  ①、编码
  列表对象的编码可以是 ziplist(压缩列表) 和 linkedlist(双端链表)。
  比如我们执行以下命令,创建一个 key = ‘numbers’,value = ‘1 three 5’ 的三个值的列表。

rpush numbers 1 ``"three" 5

  1. ziplist 编码表示如下:<br />  ![](https://cdn.nlark.com/yuque/0/2020/png/2908168/1609143293247-208d3968-9cca-46a7-91f8-cc5239e4c257.png#align=left&display=inline&height=231&margin=%5Bobject%20Object%5D&originHeight=231&originWidth=777&size=0&status=done&style=none&width=777)<br />  **linkedlist表示如下:**<br />    ![](https://cdn.nlark.com/yuque/0/2020/png/2908168/1609143293404-7f9460d1-ceb3-4d29-a2a3-9c7b9c816333.png#align=left&display=inline&height=221&margin=%5Bobject%20Object%5D&originHeight=221&originWidth=831&size=0&status=done&style=none&width=831)<br />  **②、编码转换**<br />  当同时满足下面两个条件时,使用ziplist(压缩列表)编码:<br />  1、列表保存元素个数小于512个<br />  2、每个元素长度小于64字节<br />  不能满足这两个条件的时候使用 linkedlist 编码。

4、哈希对象

哈希对象的键是一个字符串类型,值是一个键值对集合。
  ①、编码
  哈希对象的编码可以是 ziplist 或者 hashtable。
  当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾。比如执行以下命令:

hset profile name ``"Tom" hset profile age 25 hset profile career ``"Programmer"

  1. 如果使用ziplistprofile 存储如下:<br />  ![](https://cdn.nlark.com/yuque/0/2020/png/2908168/1609143669107-26fbfe9f-17ea-4c46-bfc6-8b8c287c0415.png#align=left&display=inline&height=151&margin=%5Bobject%20Object%5D&originHeight=151&originWidth=830&size=0&status=done&style=none&width=830)<br />  当使用 hashtable 编码时,上面命令存储如下:<br />  ![](https://cdn.nlark.com/yuque/0/2020/png/2908168/1609143669088-f0055005-41e6-4072-8591-f3d4236a969e.png#align=left&display=inline&height=368&margin=%5Bobject%20Object%5D&originHeight=368&originWidth=526&size=0&status=done&style=none&width=526)<br />  hashtable 编码的哈希表对象底层使用字典数据结构,哈希对象中的每个键值对都使用一个字典键值对。<br />  **②、编码转换**<br />  和上面列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码:<br />  1、列表保存元素个数小于512个<br />  2、每个元素长度小于64字节

5、集合对象

集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。
  ①、编码
  集合对象的编码可以是 intset 或者 hashtable。
  intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。
  hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。

SADD numbers 1 3 5

  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图9

SADD Dfruits ``"apple" "banana" "cherry"

 Redis五大数据类型实现原理及Redis中hll算法的应用 - 图10
  ②、编码转换
  当集合同时满足以下两个条件时,使用 intset 编码:
  1、集合对象中所有元素都是整数
  2、集合对象所有元素数量不超过512
  不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。

6、有序集合

和上面的集合对象相比,有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
  ①、编码
  有序集合的编码可以是 ziplist 或者 skiplist。
  ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

ZADD price 8.5 apple 5.0 banana 6.0 cherry

 Redis五大数据类型实现原理及Redis中hll算法的应用 - 图11


  Redis五大数据类型实现原理及Redis中hll算法的应用 - 图12

  skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:
image.png
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。
  这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费
  ②、编码转换
  当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
  1、保存的元素数量小于128;
  2、保存的所有元素长度都小于64字节。

6、HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的

统计 APP或网页 的一个页面,每天有多少用户点击进入的次数。同一个用户的反复点击进入记为 1 次。

HyperLogLog

HyperLogLog,下面简称为HLL,它是 LogLog 算法的升级版,作用是能够提供不精确的去重计数。存在以下的特点:

  • 代码实现较难。
  • 能够使用极少的内存来统计巨量的数据,在 Redis 中实现的 HyperLogLog,只需要12K内存就能统计2^64个数据。
  • 计数存在一定的误差,误差率整体较低。标准误差为 0.81% 。
  • 误差可以被设置辅助计算因子进行降低。

    伯努利试验

    在认识为什么HyperLogLog能够使用极少的内存来统计巨量的数据之前,要先认识下伯努利试验

    伯努利试验是数学概率论中的一部分内容,它的典故来源于抛硬币

硬币拥有正反两面,一次的上抛至落下,最终出现正反面的概率都是50%。假设一直抛硬币,直到它出现正面为止,我们记录为一次完整的试验,间中可能抛了一次就出现了正面,也可能抛了4次才出现正面。无论抛了多少次,只要出现了正面,就记录为一次试验。这个试验就是伯努利试验
那么对于多次的伯努利试验,假设这个多次为n次。就意味着出现了n次的正面。假设每次伯努利试验所经历了的抛掷次数为k。第一次伯努利试验,次数设为k1,以此类推,第n次对应的是kn
其中,对于这n伯努利试验中,必然会有一个最大的抛掷次数k,例如抛了12次才出现正面,那么称这个为k_max,代表抛了最多的次数。
伯努利试验容易得出有以下结论:

  1. n 次伯努利过程的投掷次数都不大于 k_max。
  2. n 次伯努利过程,至少有一次投掷次数等于 k_max

最终结合极大似然估算的方法,发现在nk_max中存在估算关联:n = 2^(k_max) 。这种通过局部信息预估整体数据流特性的方法似乎有些超出我们的基本认知,需要用概率和统计的方法才能推导和验证这种关联关系。
例如下面的样子:

  1. 第一次试验: 抛了3次才出现正面,此时 k=3n=1
  2. 第二次试验: 抛了2次才出现正面,此时 k=2n=2
  3. 第三次试验: 抛了6次才出现正面,此时 k=6n=3
  4. n 次试验:抛了12次才出现正面,此时我们估算, n = 2^12

假设上面例子中实验组数共3组,那么 k_max = 6,最终 n=3,我们放进估算公式中去,明显: 3 ≠ 2^6 。也即是说,当试验次数很小的时候,这种估算方法的误差是很大的。

image.png