日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

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

發布時間:2023/12/3 数据库 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 头条面试题:请谈谈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:

?

總結

以上是生活随笔為你收集整理的头条面试题:请谈谈Redis 9种数据结构以及它们的内部编码实现的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。