头条面试题:请谈谈Redis 9种数据结构以及它们的内部编码实现

转载自  头条面试题:请谈谈Redis 9种数据结构以及它们的内部编码实现

90%的人知道Redis 5种最基本的数据结构;

只有不到10%的人知道8种基本数据结构,5种基本+bitmap+GeoHash+HyperLogLog;

只有不到5%的人知道9种基本数据结构,5.0最新版本数据结构Streams;

只有不到1%的人掌握了所有9种基本数据结构以及8种内部编码;

掌握这篇文章的知识点,让你成为面试官眼中Redis方面最靓的仔

说明:本文基于Redis-3.2.11版本源码进行分析。

5种普通数据结构

这个没什么好说的,对Redis稍微有点了解的都知道5种最基本的数据结构:String,List,Hash,Set,Sorted Set。不过,需要注意的是,这里依然有几个高频面试题。

Set和Hash的关系

答案就是Set是一个特殊的value为空的Hash。Set类型操作的源码在tset.c中。以新增一个元素为例( intsetTypeAdd(robj*subject,sds value)),如果编码类型是OBJENCODING_HT,那么新增源码的源码如下,事实上就是对dict即Hash数据结构进行操作,并且dictSetVal时value是NULL:

dictEntry *de = dictAddRaw(ht,value,NULL);if (de) {dictSetKey(ht,de,sdsdup(value));dictSetVal(ht,de,NULL);return 1;}

 

同样的,我们在thash.c中看到Hash类型新增元素时,当判断编码类型是OBJENCODING_HT时,也是调用dict的方法:dictAdd(o->ptr,f,v),dictAdd最终也是调用dictSetVal()方法,只不过v即value不为NULL:

/* Add an element to the target hash table */int dictAdd(dict *d, void *key, void *val){dictEntry *entry = dictAddRaw(d,key,NULL);if (!entry) return DICT_ERR;dictSetVal(d, entry, val);return DICT_OK;}

所以,Redis中Set和Hash的关系就很清楚了,当编码是OBJENCODINGHT时,两者都是dict数据类型,只不过Set是value为NULL的特殊的dict。

谈谈你对Sorted Set的理解

Sorted Set的数据结构是一种跳表,即SkipList,如下图所示,红线是查找10的过程:

如何借助Sorted set实现多维排序

Sorted Set默认情况下只能根据一个因子score进行排序。如此一来,局限性就很大,举个栗子:热门排行榜需要按照下载量&最近更新时间排序,即类似数据库中的ORDER BY downloadcount, updatetime DESC。那这样的需求如果用Redis的Sorted Set实现呢?

事实上很简单,思路就是将涉及排序的多个维度的列通过一定的方式转换成一个特殊的列,即result = function(x, y, z),即x,y,z是三个排序因子,例如下载量、时间等,通过自定义函数function()计算得到result,将result作为Sorted Set中的score的值,就能实现任意维度的排序需求了。

Redis内部编码

我们常说的String,List,Hash,Set,Sorted Set只是对外的编码,实际上每种数据结构都有自己底层的内部编码实现,而且是多种实现,这样Redis可以在合适的场景选择更合适的内部编码。

如下图所示(图片纠正:intset编码,而不是inset编码),可以看到每种数据结构都有2种以上的内部编码实现,例如String数据结构就包含了raw、int和embstr三种内部编码。同时,有些内部编码可以作为多种外部数据结构的内部实现,例如ziplist就是hash、list和zset共有的内部编码,而set的内部编码可能是hashtable或者intset:

Redis这样设计有两个好处:

  • 可以偷偷的改进内部编码,而对外的数据结构和命令没有影响,这样一旦开发出更优秀的内部编码,无需改动对外数据结构和命令。

  • 多种内部编码实现可以在不同场景下发挥各自的优势。例如ziplist比较节省内存,但是在列表元素比较多的情况下,性能会有所下降。这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。

String的3种内部编码

由上图可知,String的3种内部编码分别是:int、embstr、raw。int类型很好理解,当一个key的value是整型时,Redis就将其编码为int类型(另外还有一个条件:把这个value当作字符串来看,它的长度不能超过20)。如下所示。这种编码类型为了节省内存。Redis默认会缓存10000个整型值(#define OBJSHAREDINTEGERS 10000),这就意味着,如果有10个不同的KEY,其value都是10000以内的值,事实上全部都是共享同一个对象:

127.0.0.1:6379> set number "7890"OK127.0.0.1:6379> object encoding number"int"

接下来就是ebmstr和raw两种内部编码的长度界限,请看下面的源码:

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44robj *createStringObject(const char *ptr, size_t len) {if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)return createEmbeddedStringObject(ptr,len);elsereturn createRawStringObject(ptr,len);}

也就是说,embstr和raw编码的长度界限是44,我们可以做如下验证。长度超过44以后,就是raw编码类型,不会有任何优化,是多长,就要消耗多少内存:

127.0.0.1:6379> set name "a1234567890123456789012345678901234567890123"OK127.0.0.1:6379> object encoding name"embstr"127.0.0.1:6379> set name "a12345678901234567890123456789012345678901234"OK127.0.0.1:6379> object encoding name"raw"

那么为什么有embstr编码呢?它相比raw的优势在哪里?embstr编码将创建字符串对象所需的空间分配的次数从raw编码的两次降低为一次。因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能更好地利用缓存带来的优势。并且释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码对象的字符串对象需要调用两次内存释放函数。如下图所示,左边是embstr编码,右边是raw编码:

ziplist

由前面的图可知,List,Hash,Sorted Set三种对外结构,在特殊情况下的内部编码都是ziplist,那么这个ziplist有什么神奇之处呢?

以Hash为例,我们首先看一下什么条件下它的内部编码是ziplist:

  • 当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个);

  • 所有值都小于hash-max-ziplist-value配置(默认64个字节);

如果是sorted set的话,同样需要满足两个条件:

  • 元素个数小于zset-max-ziplist-entries配置,默认128;

  • 所有值都小于zset-max-ziplist-value配置,默认64。

实际上,ziplist充分体现了Redis对于存储效率的追求。一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。

ziplist的源码在ziplist.c这个文件中,其中有一段这样的描述 -- The general layout of the ziplist is as follows::

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>
  • zlbytes:表示这个ziplist占用了多少空间,或者说占了多少字节,这其中包括了zlbytes本身占用的4个字节;

  • zltail:表示到ziplist中最后一个元素的偏移量,有了这个值,pop操作的时间复杂度就是O(1)了,即不需要遍历整个ziplist;

  • zllen:表示ziplist中有多少个entry,即保存了多少个元素。由于这个字段占用16个字节,所以最大值是2^16-1,也就意味着,如果entry的数量超过2^16-1时,需要遍历整个ziplist才知道entry的数量;

  • entry:真正保存的数据,有它自己的编码;

  • zlend:专门用来表示ziplist尾部的特殊字符,占用8个字节,值固定为255,即8个字节每一位都是1。

如下就是一个真实的ziplist编码,包含了2和5两个元素:

[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]| | | | | |zlbytes zltail entries "2" "5" end

linkedlist

这是List的一种编码数据结构非常简单,就是我们非常熟悉的双向链表,对应Java中的LinkedList。

skiplist

这个前面也已经提及,就是经典的跳表数据结构。

hashtable

这个也很容易,对应Java中的HashMap。

intset

Set特殊内部编码,当满足下面的条件时Set的内部编码就是intset而不是hashtable:

  • Set集合中必须是64位有符号的十进制整型;

  • 元素个数不能超过set-max-intset-entries配置,默认512;

验证如下:

127.0.0.1:6379> sadd scores 135(integer) 0127.0.0.1:6379> sadd scores 128(integer) 1127.0.0.1:6379> object encoding scores"intset"

那么intset编码到底是个什么东西呢?看它的源码定义如下,很明显,就是整型数组,并且是一个有序的整型数组。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数采取了不同的编码,尽量对内存的使用进行了优化。这样的数据结构,如果执行SISMEMBER命令,即查看某个元素是否在集合中时,事实上使用的是二分查找法:

typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[];} intset;// intset编码查找方法源码(人为简化),标准的二分查找法:static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;int64_t cur = -1;while(max >= min) {mid = ((unsigned int)min + (unsigned int)max) >> 1;cur = _intsetGet(is,mid);if (value > cur) {min = mid+1;} else if (value < cur) {max = mid-1;} else {break;}}if (value == cur) {if (pos) *pos = mid;return 1;} else {if (pos) *pos = min;return 0;}}#define INTSET_ENC_INT16 (sizeof(int16_t))#define INTSET_ENC_INT32 (sizeof(int32_t))#define INTSET_ENC_INT64 (sizeof(int64_t))

3种高级数据结构

Redis中3种高级数据结构分别是bitmap、GEO、HyperLogLog,针对这3种数据结构,笔者之前也有文章介绍过。其中,最重要的就是bitmap

bitmap

这个就是Redis实现的BloomFilter,BloomFilter非常简单,如下图所示,假设已经有3个元素a、b和c,分别通过3个hash算法h1()、h2()和h2()计算然后对一个bit进行赋值,接下来假设需要判断d是否已经存在,那么也需要使用3个hash算法h1()、h2()和h2()对d进行计算,然后得到3个bit的值,恰好这3个bit的值为1,这就能够说明:d可能存在集合中。再判断e,由于h1(e)算出来的bit之前的值是0,那么说明:e一定不存在集合中

需要说明的是,bitmap并不是一种真实的数据结构,它本质上是String数据结构,只不过操作的粒度变成了位,即bit。因为String类型最大长度为512MB,所以bitmap最多可以存储2^32个bit。

GEO

GEO数据结构可以在Redis中存储地理坐标,并且坐标有限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:

  • 有效的经度从-180度到180度。

  • 有效的纬度从-85.05112878度到85.05112878度。

当坐标位置超出上述指定范围时,该命令将会返回一个错误。添加地理位置命令如下:

redis> GEOADD city 114.031040 22.324386 "shenzhen" 112.572154 22.267832 "guangzhou"(integer) 2redis> GEODIST city shenzhen guangzhou"150265.8106"

但是,需要说明的是,Geo本身不是一种数据结构,它本质上还是借助于Sorted Set(ZSET),并且使用GeoHash技术进行填充。Redis中将经纬度使用52位的整数进行编码,放进zset中,score就是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实就是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。

总之,Redis中处理这些地理位置坐标点的思想是:二维平面坐标点 --> 一维整数编码值 --> zset(score为编码值) --> zrangebyrank(获取score相近的元素)、zrangebyscore --> 通过score(整数编码值)反解坐标点 --> 附近点的地理位置坐标。

GEOHASH原理

使用wiki上的例子,纬度为42.6,经度为-5.6的点,转化为base32的话要如何转呢?首先拿纬度来进行说明,纬度的范围为-90到90,将这个范围划为两段,则为[-90,0]、[0,90],然后看给定的纬度在哪个范围,在前面的范围的话,就设当前位为0,后面的话值便为1.然后继续将确定的范围1分为2,继续以确定值在前段还是后段来确定bit的值。就这样慢慢的缩小范围,一般最多缩小13次就可以了(经纬度的二进制位相加最多25位,经度13位,纬度12位)。这时的中间值,将跟给定的值最相近。如下图所示:

第1行,纬度42.6位于[0, 90]之间,所以bit=1;第2行,纬度42.6位于[0, 45]之间,所以bit=0;第3行,纬度42.6位于[22.5, 45]之间,所以bit=1,以此类推。这样,取出图中的bit位:1011 1100 1001,同样的方法,将经度(范围-180到180)算出来为 :0111 1100 0000 0。结果对其如下:

# 经度0111 1100 0000 0# 纬度1011 1100 1001

得到了经纬度的二进制位后,下面需要将两者进行结合:从经度、纬度的循环,每次取其二进制的一位(不足位取0),合并为新的二进制数:01101111 11110000 01000001 0。每5位为一个十进制数,结合base32对应表映射为base32值为:ezs42。这样就完成了encode的过程。

Streams

这是Redis5.0引入的全新数据结构,用一句话概括Streams就是Redis实现的内存版kafka。而且,Streams也有Consumer Groups的概念。通过Redis源码中对stream的定义我们可知,streams底层的数据结构是radix tree

typedef struct stream {rax *rax; /* The radix tree holding the stream. */uint64_t length; /* Number of elements inside this stream. */streamID last_id; /* Zero if there are yet no items. */rax *cgroups; /* Consumer groups dictionary: name -> streamCG */} stream;

那么这个radix tree长啥样呢?在Redis源码的rax.h文件中有一段这样的描述,这样看起来是不是就比较直观了:

* (f) ""* /* (i o) "f"* / \* "firs" ("rst") (o) "fo"* / \* "first" [] [t b] "foo"* / \* "foot" ("er") ("ar") "foob"* / \* "footer" [] [] "foobar"

Radix Tree(基数树) 事实上就几乎相同是传统的二叉树。仅仅是在寻找方式上,以一个unsigned int类型数为例,利用这个数的每个比特位作为树节点的推断。能够这样说,比方一个数10001010101010110101010,那么依照Radix 树的插入就是在根节点,假设遇到0,就指向左节点,假设遇到1就指向右节点,在插入过程中构造树节点,在删除过程中删除树节点。如下是一个保存了7个单词的Radix Tree:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327027.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【初码干货】关于.NET玩爬虫这些事

这几天在微信群里又聊到.NET可以救中国但是案例太少不深的问题&#xff0c;我说.NET玩爬虫简直就是宇宙第一&#xff0c;于是大神朱永光说&#xff0c;你为何不来写一篇总结一下&#xff1f; 那么今天就全面的来总结一下&#xff0c;在.NET生态下&#xff0c;如何玩爬虫 关于爬…

使用ueditor实现多图片上传案例——Service层(IShoppingService)

/** * Title: IShoppingService.java * Package org.service * Description: TODO该方法的主要作用&#xff1a; * author A18ccms A18ccms_gmail_com * date 2017-9-30 下午10:13:23 * version V1.0 */ package org.service;import org.dao.IShoppingDao;/** * * 项…

数组,List,Set相互转化

https://blog.csdn.net/kye055947/article/details/80561633 订阅专栏 转载&#xff1a;https://blog.csdn.net/my_precious/article/details/53010232 1. 数组转化为List&#xff1a; String[] strArray new String[]{"Tom", "Bob", "Jane"};…

【JVM】浅谈双亲委派和破坏双亲委派

转载自 【JVM】浅谈双亲委派和破坏双亲委派 一、前言 笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书&#xff0c;阅读完后自以为对jvm有了一定的了解&#xff0c;然而当真正碰到问题的时候&#xff0c;才发现自己读的有多粗糙&#xff0c;也体会到只有实践才能加深理…

使用ueditor实现多图片上传案例——ServiceImpl层(ShoppingServiceImpl)

/** * Title: ShoppingServiceImpl.java * Package org.service.impl * Description: TODO该方法的主要作用&#xff1a; * author A18ccms A18ccms_gmail_com * date 2017-9-30 下午10:17:54 * version V1.0 */ package org.service.impl;import org.dao.IShoppingDao; …

开源OSS.Social微信项目解析

前言&#xff1a;OSS.Social是个开源的社交网站接口集成项目&#xff0c;当前也有很多其他不错的项目&#xff0c;不过始终没有我想要的那种简单清晰&#xff0c;只能撸起袖子&#xff0c;从头打造一个。当前正在进行的是对微信项目的开发&#xff0c;这里把对接口的整理&#…

查询空值中的注意事项

在数据表创建之初&#xff0c;创建者可以指定某个字段是否为空值NULL。注意了&#xff0c;这个NULL既不代表0&#xff0c;也不代表空字符&#xff0c;而是代表一种未知的状态&#xff0c;比如不适用或者放着等将来有合适数据了再添加进去。 语法规则为&#xff1a; SELECT 字段…

Mysql字符串截取 mysql将字符串字段转为数字排序或比大小

SELECT * FROM Student WHERE 1 1 ORDER BY -ID DESC ; SELECT * FROM Student WHERE 1 1 ORDER BY (ID 1); mysql将字符串字段转为数字排序或比大小 2017年09月17日 01:36:31 阅读数&#xff1a;6566 版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得…

Apdex(Application Performance Index)量化应用性能

“道琼斯指数帮助人们衡量股市行情变化&#xff0c;Apdex 指数帮助您衡量用户心情变化。“ 一.为什么需要 Apdex 性能指数&#xff0c;Apdex(Application Performance Index)是一个国际通用标准&#xff0c;Apdex 是用户对应用性能满意度的量化值。它提供了一个统一的测量和报告…

使用ueditor实现多图片上传案例——截取字符串层Util(SubString_text)

/** * Title: ConfigManager.java * Package org.util * Description: TODO该方法的主要作用&#xff1a; * author A18ccms A18ccms_gmail_com * date 2017-9-29 上午8:01:39 * version V1.0 */ package org.util;import org.entity.Shopping;/** * * 项目名称&…

关于Java类加载双亲委派机制的思考(附面试题)

转载自 关于Java类加载双亲委派机制的思考&#xff08;附面试题&#xff09; 预定义类加载器和双亲委派机制 JVM预定义的三种类型类加载器&#xff1a; 启动&#xff08;Bootstrap&#xff09;类加载器&#xff1a;是用本地代码实现的类装入器&#xff0c;它负责将 <Java_R…

线性结构VS非线性结构

线性结构 线性结构作为最常用的数据结构&#xff0c;其特点是数据元素之间存在一对一的线性关系 线性结构有两种不同的存储结构&#xff0c;即顺序存储结构和链式存储结构。顺序存储的线性表称为顺序表&#xff0c;顺序表中的存储元素是连续的 链式存储的线性表称为链表&#…

ES报错:Connection reset by peer 解决经历

http://nicethemes.cn/news/txtlist_i28391v.html 这次来分享一下ES报错&#xff1a;java.io.IOException: Connection reset by peer 的解决经历 问题描述 本人最近负责了定时获取Prometheus Metrics并发送到ES做持久化存储的任务。然而在Metrics采集粒度从3分钟变为1小时后…

云计算设计模式(一)缓存预留模式

云带来的改变是显而易见的&#xff0c;云计算是一种按使用量付费的模式&#xff0c;这种模式提供可用的、便捷的、按需的网络访问&#xff0c; 进入可配置的计算资源共享池&#xff08;资源包括网络&#xff0c;服务器&#xff0c;存储&#xff0c;应用软件&#xff0c;服务&am…

Thread.currentThread().getContextClassLoader() 和 Class.getClassLoader()区别

转载自 Thread.currentThread().getContextClassLoader() 和 Class.getClassLoader()区别 查了一些资料也不是太明白两个的区别&#xff0c;但是前者是最安全的用法 打个简单的比方&#xff0c;你一个WEB程序&#xff0c;发布到Tomcat里面运行。 首先是执行Tomcat org.apache.c…

批量删除文件

git bash 运行 rm -rf *.class 删除当前文件夹下所有的.class文件

稀疏数组与二维数组相互转化

图示 二维数组转稀疏数组的思路 遍历 原始的二维数组&#xff0c;得到有效数据的个数 sum根据sum 就可以创建 稀疏数组 sparseArr int[sum 1] [3]将二维数组的有效数据数据存入到 稀疏数组 稀疏数组转原始的二维数组的思路 先读取稀疏数组的第一行&#xff0c;根据第一行的…

云计算设计模式(二)——断路器模式

云带来的改变是显而易见的&#xff0c;云计算是一种按使用量付费的模式&#xff0c;这种模式提供可用的、便捷的、按需的网络访问&#xff0c; 进入可配置的计算资源共享池&#xff08;资源包括网络&#xff0c;服务器&#xff0c;存储&#xff0c;应用软件&#xff0c;服务&am…

Class.forName()和ClassLoader.getSystemClassLoader().loadClass()区别

转载自 Class.forName&#xff08;&#xff09;和ClassLoader.getSystemClassLoader().loadClass&#xff08;&#xff09;区别 class A {static {System.out.println("Class A is Loading now");}public A(){System.out.println("A new Class A instance is cr…