日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

bat 连续读取两行_Redis底层数据结构解析(BAT大厂必问)

發(fā)布時(shí)間:2024/7/23 55 豆豆
生活随笔 收集整理的這篇文章主要介紹了 bat 连续读取两行_Redis底层数据结构解析(BAT大厂必问) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

Redis是一個(gè)key-value存儲(chǔ)系統(tǒng),現(xiàn)在在各種系統(tǒng)中的使用越來越多,大部分情況下是因?yàn)槠涓咝阅艿奶匦?#xff0c;被當(dāng)做緩存使用。Redis由于其豐富的數(shù)據(jù)結(jié)構(gòu)也可以被應(yīng)用到其他場(chǎng)景。Redis是一個(gè)K-V的非關(guān)系型數(shù)據(jù)庫(kù)(NoSQL),常見的NoSQL數(shù)據(jù)庫(kù)有:K-V數(shù)據(jù)庫(kù)如Redis、Memcached,列式數(shù)據(jù)庫(kù)如大數(shù)據(jù)組件HBase,文檔數(shù)據(jù)庫(kù)如mogoDB。Redis應(yīng)用廣泛,尤其是被作為緩存使用。

Redis的具有很多優(yōu)勢(shì):

(1)讀寫性能高--100000次/s以上的讀速度,80000次/s以上的寫速度;

(2)K-V,value支持的數(shù)據(jù)類型很多:字符串(String),隊(duì)列(List),哈希(Hash),集合(Sets),有序集合(Sorted Sets)5種不同的數(shù)據(jù)類型。

(3)原子性,Redis的所有操作都是單線程原子性的。

(4)特性豐富--支持訂閱-發(fā)布模式,通知、設(shè)置key過期等特性。

(5)在Redis3.0 版本引入了Redis集群,可用于分布式部署。

有需要Redis大廠面試題的小伙伴點(diǎn)擊這里

IT架構(gòu)師luke:Redis面試題(BAT大廠真題)?zhuanlan.zhihu.com

Redis數(shù)據(jù)類型及其底層實(shí)現(xiàn)方式

Redis是由C語言編寫的。Redis支持5種數(shù)據(jù)類型,以K-V形式進(jìn)行存儲(chǔ),K是String類型的,V支持5種不同的數(shù)據(jù)類型,分別是:string,list,hash,set,sorted set,每一種數(shù)據(jù)結(jié)構(gòu)都有其特定的應(yīng)用場(chǎng)景。從內(nèi)部實(shí)現(xiàn)的角度來看是如何更好的實(shí)現(xiàn)這些數(shù)據(jù)類型。Redis底層數(shù)據(jù)結(jié)構(gòu)有以下數(shù)據(jù)類型:簡(jiǎn)單動(dòng)態(tài)字符串(SDS),鏈表,字典,跳躍表,整數(shù)集合,壓縮列表,對(duì)象。接下來,就探討一下Redis是怎么通過這些數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)value的5種類型的。

簡(jiǎn)單動(dòng)態(tài)字符串(simple dynamic string SDS)

String的數(shù)據(jù)類型是由SDS實(shí)現(xiàn)的。Redis并沒有采用C語言的字符串表示,而是自己構(gòu)建了一種名為SDS的抽象類型,并將SDS作為Redis的默認(rèn)字符串表示。

redis>SET msg "hello world"

OK

上邊設(shè)置key=msg,value=hello world的鍵值對(duì),它們的底層存儲(chǔ)是:鍵(key)是字符串類型,其底層實(shí)現(xiàn)是一個(gè)保存著“msg”的SDS。值(value)是字符串類型,其底層實(shí)現(xiàn)是一個(gè)保存著“hello world”的SDS。

注意:SDS除了用于實(shí)現(xiàn)字符串類型,還被用作AOF持久化時(shí)的緩沖區(qū)。

SDS的定義為:

/*

* 保存字符串對(duì)象的結(jié)構(gòu)

*/

struct sdshdr {

// buf 中已占用空間的長(zhǎng)度

int len;

// buf 中剩余可用空間的長(zhǎng)度

int free;

// 數(shù)據(jù)空間

char buf[];

};

為什么要使用SDS:

我們一定會(huì)思考,redis為什么不使用C語言的字符串而是費(fèi)事搞一個(gè)SDS呢,這是因?yàn)镃語言用N+1的字符數(shù)組來表示長(zhǎng)度為N的字符串,這樣做在獲取字符串長(zhǎng)度,字符串?dāng)U展等操作方面效率較低,并且無法滿足redis對(duì)字符串在安全性、效率以及功能方面的要求。

獲取字符串長(zhǎng)度(SDS O(1))

在C語言字符串中,為了獲取一個(gè)字符串的長(zhǎng)度,必須遍歷整個(gè)字符串,時(shí)間復(fù)雜度為O(1),而SDS中,有專門用于保存字符串長(zhǎng)度的變量,所以可以在O(1)時(shí)間內(nèi)獲得。

防止緩沖區(qū)溢出

C字符串,容易導(dǎo)致緩沖區(qū)溢出,假設(shè)在程序中存在內(nèi)存緊鄰的字符串s1和s2,s1保存redis,s2保存MongoDB,如下圖:

如果我們現(xiàn)在將s1 的內(nèi)容修改為redis cluster,但是又忘了重新為s1 分配足夠的空間,這時(shí)候就會(huì)出現(xiàn)以下問題:

因?yàn)閟1和s2是緊鄰的,所以原本s2 中的內(nèi)容已經(jīng)被S1的內(nèi)容給占領(lǐng)了,s2 現(xiàn)在為 cluster,而不是“Mongodb”。而Redis中的SDS就杜絕了發(fā)生緩沖區(qū)溢出的可能性。

當(dāng)我們需要對(duì)一個(gè)SDS 進(jìn)行修改的時(shí)候,redis 會(huì)在執(zhí)行拼接操作之前,預(yù)先檢查給定SDS 空間是否足夠(free記錄了剩余可用的數(shù)據(jù)長(zhǎng)度),如果不夠,會(huì)先拓展SDS 的空間,然后再執(zhí)行拼接操作。

減少擴(kuò)展或收縮字符串帶來的內(nèi)存重分配次數(shù)

當(dāng)字符串進(jìn)行擴(kuò)展或收縮時(shí),都會(huì)對(duì)內(nèi)存空間進(jìn)行重新分配。

1. 字符串拼接會(huì)產(chǎn)生字符串的內(nèi)存空間的擴(kuò)充,在拼接的過程中,原來的字符串的大小很可能小于拼接后的字符串的大小,那么這樣的話,就會(huì)導(dǎo)致一旦忘記申請(qǐng)分配空間,就會(huì)導(dǎo)致內(nèi)存的溢出。

2. 字符串在進(jìn)行收縮的時(shí)候,內(nèi)存空間會(huì)相應(yīng)的收縮,而如果在進(jìn)行字符串的切割的時(shí)候,沒有對(duì)內(nèi)存的空間進(jìn)行一個(gè)重新分配,那么這部分多出來的空間就成為了內(nèi)存泄露。

比如:字符串"redis",當(dāng)進(jìn)行字符串拼接時(shí),將redis+cluster=13,會(huì)將SDS的長(zhǎng)度修改為13,同時(shí)將free也改為13,這意味著進(jìn)行預(yù)分配了,將buffer大小變?yōu)榱?6。這是為了如果再次執(zhí)行字符串拼接操作,如果拼接的字符串長(zhǎng)度<13,就不需要重新進(jìn)行內(nèi)存分配了。

通過這種預(yù)分配策略,SDS將連續(xù)增長(zhǎng)N次字符串所需的內(nèi)存重分配次數(shù)從必定N次降低為最多N次。通過惰性空間釋放,SDS 避免了縮短字符串時(shí)所需的內(nèi)存重分配操作,并未將來可能有的增長(zhǎng)操作提供了優(yōu)化。

二進(jìn)制安全

C 字符串中的字符必須符合某種編碼,并且除了字符串的末尾之外,字符串里面不能包含空字符,否則最先被程序讀入的空字符將被誤認(rèn)為是字符串結(jié)尾,這些限制使得C字符串只能保存文本數(shù)據(jù),而不能保存想圖片,音頻,視頻,壓縮文件這樣的二進(jìn)制數(shù)據(jù)。

 但是在Redis中,不是靠空字符來判斷字符串的結(jié)束的,而是通過len這個(gè)屬性。那么,即便是中間出現(xiàn)了空字符對(duì)于SDS來說,讀取該字符仍然是可以的。

但是,SDS依然可以兼容部分C字符串函數(shù)。

鏈表

鏈表是list的實(shí)現(xiàn)方式之一。當(dāng)list包含了數(shù)量較多的元素,或者列表中包含的元素都是比較長(zhǎng)的字符串時(shí),Redis會(huì)使用鏈表作為實(shí)現(xiàn)List的底層實(shí)現(xiàn)。此鏈表是雙向鏈表:

typedef struct listNode{

struct listNode *prev;

struct listNode * next;

void * value;

}

一般我們通過操作list來操作鏈表:

typedef struct list{

//表頭節(jié)點(diǎn)

listNode * head;

//表尾節(jié)點(diǎn)

listNode * tail;

//鏈表長(zhǎng)度

unsigned long len;

//節(jié)點(diǎn)值復(fù)制函數(shù)

void *(*dup) (void *ptr);

//節(jié)點(diǎn)值釋放函數(shù)

void (*free) (void *ptr);

//節(jié)點(diǎn)值對(duì)比函數(shù)

int (*match)(void *ptr, void *key);

}

鏈表結(jié)構(gòu)的特點(diǎn)是可以快速的在表頭和表尾插入和刪除元素,但查找復(fù)雜度高,是列表的底層實(shí)現(xiàn)之一,也因此列表沒有提供判斷某一元素是否在列表中的借口,因?yàn)樵阪湵碇胁檎覐?fù)雜度高。

字典

字典,又稱為符號(hào)表(symbol table)、關(guān)聯(lián)數(shù)組(associative array)或映射(map),是一種用于保存鍵值對(duì)的抽象數(shù)據(jù)結(jié)構(gòu)。 

 在字典中,一個(gè)鍵(key)可以和一個(gè)值(value)進(jìn)行關(guān)聯(lián),字典中的每個(gè)鍵都是獨(dú)一無二的。在C語言中,并沒有這種數(shù)據(jù)結(jié)構(gòu),但是Redis 中構(gòu)建了自己的字典實(shí)現(xiàn)。

redis > SET msg "hello world"

OK

Redis本身的K-V存儲(chǔ)就是利用字典這種數(shù)據(jù)結(jié)構(gòu)的,另外value類型的哈希表也是通過這個(gè)實(shí)現(xiàn)的。

哈希表dicy的定義為:

typedef struct dictht {

//哈希表數(shù)組

dictEntry **table;

//哈希表大小

unsigned long size;

//哈希表大小掩碼,用于計(jì)算索引值

unsigned long sizemask;

//該哈希表已有節(jié)點(diǎn)的數(shù)量

unsigned long used;

}

我們可以想到對(duì)比Java hashMap的實(shí)現(xiàn)方式,在dictht中,table數(shù)組的類型是:

typeof struct dictEntry{

//鍵

void *key;

//值

union{

void *val;

uint64_tu64;

int64_ts64;

}

struct dictEntry *next;

}

我們存入里面的key 并不是直接的字符串,而是一個(gè)hash 值,通過hash 算法,將字符串轉(zhuǎn)換成對(duì)應(yīng)的hash 值,然后在dictEntry 中找到對(duì)應(yīng)的位置。

這時(shí)候我們會(huì)發(fā)現(xiàn)一個(gè)問題,如果出現(xiàn)hash 值相同的情況怎么辦?Redis 采用了鏈地址法來解決hash沖突。這與hashmap的實(shí)現(xiàn)類似。

注意:Redis又在dictht的基礎(chǔ)上,又抽象了一層字典dict,其定義為:

typedef struct dict {

// 類型特定函數(shù)

dictType *type;

// 私有數(shù)據(jù)

void *privedata;

// 哈希表

dictht ht[2];

// rehash 索引

in trehashidx;

}

type 屬性 和privdata 屬性是針對(duì)不同類型的鍵值對(duì),為創(chuàng)建多態(tài)字典而設(shè)置的。

ht 屬性是一個(gè)包含兩個(gè)項(xiàng)(兩個(gè)哈希表)的數(shù)組,如圖:

解決hash沖突:采用鏈地址法來實(shí)現(xiàn)。

擴(kuò)充Rehash: 隨著對(duì)哈希表的不斷操作,哈希表保存的鍵值對(duì)會(huì)逐漸的發(fā)生改變,為了讓哈希表的負(fù)載因子維持在一個(gè)合理的范圍之內(nèi),我們需要對(duì)哈希表的大小進(jìn)行相應(yīng)的擴(kuò)展或者壓縮,這時(shí)候,我們可以通過 rehash(重新散列)操作來完成。其實(shí)現(xiàn)方式和hashmap略有不同,因?yàn)閐ict有兩個(gè)hash表dictht,所以它是通過這兩個(gè)dictht互相進(jìn)行轉(zhuǎn)移的。

比如:

上圖這種情況,代表要進(jìn)行擴(kuò)容了,所以就要將ht[0]的數(shù)據(jù)轉(zhuǎn)移到ht[1]中。ht[1]創(chuàng)建為2*ht[0].size大小的,如下圖:

將ht[0]釋放,然后將ht[1]設(shè)置成ht[0],最后為ht[1]分配一個(gè)空白哈希表:

其實(shí)上邊的擴(kuò)容過程和Java 的HashMap具體的擴(kuò)容實(shí)現(xiàn)方式還是挺像的。

漸進(jìn)式rehash:在實(shí)際開發(fā)過程中,這個(gè)rehash 操作并不是一次性、集中式完成的,而是分多次、漸進(jìn)式地完成的。采用漸進(jìn)式rehash 的好處在于它采取分而治之的方式,避免了集中式rehash 帶來的龐大計(jì)算量。

漸進(jìn)式rehash 的詳細(xì)步驟:

1、為ht[1] 分配空間,讓字典同時(shí)持有ht[0]和ht[1]兩個(gè)哈希表

2、在幾點(diǎn)鐘維持一個(gè)索引計(jì)數(shù)器變量rehashidx,并將它的值設(shè)置為0,表示rehash 開始

3、在rehash 進(jìn)行期間,每次對(duì)字典執(zhí)行CRUD操作時(shí),程序除了執(zhí)行指定的操作以外,還會(huì)將ht[0]中的數(shù)據(jù)rehash 到ht[1]表中,并且將rehashidx加一

4、當(dāng)ht[0]中所有數(shù)據(jù)轉(zhuǎn)移到ht[1]中時(shí),將rehashidx 設(shè)置成-1,表示rehash 結(jié)束

跳躍表

Redis 只在兩個(gè)地方用到了跳躍表,一個(gè)是實(shí)現(xiàn)有序集合鍵(sorted Sets),另外一個(gè)是在集群節(jié)點(diǎn)中用作內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

其實(shí)跳表主要是來替代平衡二叉樹的,比起平衡樹來說,跳表的實(shí)現(xiàn)要簡(jiǎn)單直觀的多。

跳躍表(skiplist)是一種有序數(shù)據(jù)結(jié)構(gòu),它通過在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而達(dá)到快速查找訪問節(jié)點(diǎn)的目的。跳躍表是一種隨機(jī)化的數(shù)據(jù),跳躍表以有序的方式在層次化的鏈表中保存元素,效率和平衡樹媲美 ——查找、刪除、添加等操作都可以在O(logn)期望時(shí)間下完成。

Redis 的跳躍表 主要由兩部分組成:zskiplist(鏈表)和zskiplistNode (節(jié)點(diǎn)):

typedef struct zskiplistNode{

//層

struct zskiplistLevel{

//前進(jìn)指針

struct zskiplistNode *forward;

//跨度

unsigned int span;

} level[];

//后退指針

struct zskiplistNode *backward;

//分值

double score;

//成員對(duì)象

robj *obj;

}

1、層:level 數(shù)組可以包含多個(gè)元素,每個(gè)元素都包含一個(gè)指向其他節(jié)點(diǎn)的指針。level數(shù)組的每個(gè)元素都包含:前進(jìn)指針:用于指向表尾方向的前進(jìn)指針,跨度:用于記錄兩個(gè)節(jié)點(diǎn)之間的距離

2、后退指針:用于從表尾向表頭方向訪問節(jié)點(diǎn)

3、分值和成員:跳躍表中的所有節(jié)點(diǎn)都按分值從小到大排序(按照這個(gè)進(jìn)行排序的,也就是平衡二叉樹(搜索樹的)的節(jié)點(diǎn)大小)。成員對(duì)象指向一個(gè)字符串,這個(gè)字符串對(duì)象保存著一個(gè)SDS值(實(shí)際存儲(chǔ)的值)

typedef struct zskiplist {

//表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)

structz skiplistNode *header,*tail;

//表中節(jié)點(diǎn)數(shù)量

unsigned long length;

//表中層數(shù)最大的節(jié)點(diǎn)的層數(shù)

int level;

}zskiplist;

從結(jié)構(gòu)圖中我們可以清晰的看到,header,tail分別指向跳躍表的頭結(jié)點(diǎn)和尾節(jié)點(diǎn)。level 用于記錄最大的層數(shù),length 用于記錄我們的節(jié)點(diǎn)數(shù)量。

 跳躍表是有序集合的底層實(shí)現(xiàn)之一

主要有zskiplist 和zskiplistNode兩個(gè)結(jié)構(gòu)組成

每個(gè)跳躍表節(jié)點(diǎn)的層高都是1至32之間的隨機(jī)數(shù)

在同一個(gè)跳躍表中,多個(gè)節(jié)點(diǎn)可以包含相同的分值,但每個(gè)節(jié)點(diǎn)的對(duì)象必須是唯一的

節(jié)點(diǎn)按照分值的大小從大到小排序,如果分值相同,則按成員對(duì)象大小排序

怎么使用跳表來實(shí)現(xiàn)O(logn)的增刪改查??

其實(shí)跳表的實(shí)現(xiàn)原理,我們可以結(jié)合二分法來看。

比如上圖,我們要查找55,如果通過遍歷,則必須得從頭遍歷到最后一個(gè)才能找到,所以在數(shù)組實(shí)現(xiàn)中,我們可以使用二分法來實(shí)現(xiàn),但是在鏈表中,我們沒辦法直接通過下標(biāo)來訪問元素,所以一般我們可以用二叉搜索樹,平衡樹來存儲(chǔ)元素,我們知道跳表就是來替代平衡樹的,那么跳表是如何快速查詢呢?看下圖:

從上圖我們可以看到,我們通過第4層,只需一步便可找到55,另外最耗時(shí)的訪問46需要6次查詢。即L4訪問55,L3訪問21、55,L2訪問37、55,L1訪問46。我們直覺上認(rèn)為,這樣的結(jié)構(gòu)會(huì)讓查詢有序鏈表的某個(gè)元素更快。這種實(shí)現(xiàn)方式跟二分很相似,其時(shí)間復(fù)雜度就是O(logn)。其插入,刪除都是O(logn)。

我們可以看到,redis正是通過定義這種結(jié)構(gòu)來實(shí)現(xiàn)上邊的過程,其層數(shù)最高為32層,也就是他可以存儲(chǔ)2^32次方的數(shù)據(jù),其查找過程與上圖很類似。

整數(shù)集合(Intset)

《Redis 設(shè)計(jì)與實(shí)現(xiàn)》 中這樣定義整數(shù)集合:“整數(shù)集合是集合建(sets)的底層實(shí)現(xiàn)之一,當(dāng)一個(gè)集合中只包含整數(shù),且這個(gè)集合中的元素?cái)?shù)量不多時(shí),redis就會(huì)使用整數(shù)集合intset作為集合的底層實(shí)現(xiàn)。”

我們可以這樣理解整數(shù)集合,他其實(shí)就是一個(gè)特殊的集合,里面存儲(chǔ)的數(shù)據(jù)只能夠是整數(shù),并且數(shù)據(jù)量不能過大。

typedef struct intset{

//編碼方式

uint32_t enconding;

// 集合包含的元素?cái)?shù)量

uint32_t length;

//保存元素的數(shù)組

int8_t contents[];

}

整數(shù)集合是集合建的底層實(shí)現(xiàn)之一.

整數(shù)集合的底層實(shí)現(xiàn)為數(shù)組,這個(gè)數(shù)組以有序,無重復(fù)的范式保存集合元素,在有需要時(shí),程序會(huì)根據(jù)新添加的元素類型改變這個(gè)數(shù)組的類型.

壓縮列表

壓縮列表是列表鍵(list)和哈希鍵(hash)的底層實(shí)現(xiàn)之一。當(dāng)一個(gè)列表鍵只有少量列表項(xiàng),并且每個(gè)列表項(xiàng)要么就是小整數(shù),要么就是長(zhǎng)度比較短的字符串,那么Redis 就會(huì)使用壓縮列表來做列表鍵的底層實(shí)現(xiàn)。

1、zlbytes:用于記錄整個(gè)壓縮列表占用的內(nèi)存字節(jié)數(shù)

2、zltail:記錄要列表尾節(jié)點(diǎn)距離壓縮列表的起始地址有多少字節(jié)

3、zllen:記錄了壓縮列表包含的節(jié)點(diǎn)數(shù)量。

4、entryX:要說列表包含的各個(gè)節(jié)點(diǎn)

5、zlend:用于標(biāo)記壓縮列表的末端

壓縮列表是一種為了節(jié)約內(nèi)存而開發(fā)的順序型數(shù)據(jù)結(jié)構(gòu)

壓縮列表被用作列表鍵和哈希鍵的底層實(shí)現(xiàn)之一

壓縮列表可以包含多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者整數(shù)值

添加新節(jié)點(diǎn)到壓縮列表,可能會(huì)引發(fā)連鎖更新操作。

如果你喜歡我寫的技術(shù)文章以及面試總結(jié),歡迎關(guān)注收看我的視頻,并且點(diǎn)贊、收藏、關(guān)注我哦。

我是luke,感謝你的關(guān)注!

也可以加入到我的圈子一起學(xué)習(xí)成長(zhǎng)哦【架構(gòu)師之路】點(diǎn)擊鏈接申請(qǐng)加入圈子

架構(gòu)師之路 - 知乎?www.zhihu.com

總結(jié)

以上是生活随笔為你收集整理的bat 连续读取两行_Redis底层数据结构解析(BAT大厂必问)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。