Redis基础数据结构内部实现简单介绍
5種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
- Redis有5種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),分別是:String(字符串),list(列表),hash(字典),set(集合),zset(有序集合),這五種是我們開發(fā)種經(jīng)常用的到的,是Redis種最基礎(chǔ),最重要的部分。
string(字符串)
- 字符串string是Redis最簡單的一個(gè)數(shù)據(jù)結(jié)構(gòu),他的內(nèi)部表示是一個(gè)字符數(shù)組,Redis所有的數(shù)據(jù)結(jié)構(gòu)都以唯一的key字符串作為名稱,然后通過這個(gè)唯一的key獲取對應(yīng)的value數(shù)據(jù),不同類型數(shù)據(jù)結(jié)構(gòu)差異其實(shí)就在value上而已。如下圖對應(yīng)String類型字符串示意圖:
- Redis的字符串底層實(shí)現(xiàn)其實(shí)是動(dòng)態(tài)的字符串,是可以修改的字符串,內(nèi)部結(jié)構(gòu)實(shí)現(xiàn)類似java種的ArrayList,采用預(yù)分配的方式冗余了一部分空的內(nèi)存空間來減少頻繁的內(nèi)存重新分配的環(huán)節(jié),如下圖,內(nèi)部為當(dāng)前字符串分配的實(shí)際空間Capacity,一般比我們字符串長度len更多,當(dāng)字符串長度小于1M時(shí)候,擴(kuò)容都是加倍現(xiàn)有空間,如果字符串長度超過1M,擴(kuò)容的時(shí)候,只會(huì)擴(kuò)容1M內(nèi)存(生產(chǎn)上一個(gè)1M的字符也比較大了,不建議這么搞)。注意,字符串最大長度512M。
- 如果這個(gè)string類型的數(shù)據(jù)value是一個(gè)整數(shù),Redis還提供來一種自增的操作,他的自增范圍是signed long的最大值最小值之間,超過這個(gè)范圍,Redis會(huì)報(bào)錯(cuò)。
list(列表)
- Redis的list相當(dāng)于Java中的LinkedList,他是鏈表而不是數(shù)組。這個(gè)意味著list的插入和刪除是很快的,事件復(fù)雜度是O(1),但是下標(biāo)索引定位慢,事件復(fù)雜度O(n),Redis中實(shí)現(xiàn)是一個(gè)雙向鏈表結(jié)構(gòu)的保證順序性,串起來可以同時(shí)支持前后的方向便利,當(dāng)列表彈出最后一個(gè)元素,該數(shù)據(jù)結(jié)構(gòu)被自動(dòng)刪除,內(nèi)存回收。如下圖
- Redis的列表結(jié)構(gòu)可以用來做簡單的異步隊(duì)列使用,將需要延后處理的任務(wù)結(jié)構(gòu)化為一個(gè)字符串,放到Redis列表中,另一個(gè)線程在列表中逐個(gè)彈出。
右近左出:隊(duì)列
- 隊(duì)列是先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu),用于消費(fèi)隊(duì)列和異步的邏輯處理,他會(huì)確保元素的訪問順序性。如下圖
右邊進(jìn)右邊出:棧
- 棧是先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),跟隊(duì)列正好相反,我們用Redis的列表數(shù)據(jù)結(jié)構(gòu)來做棧使用如下:
警惕慢操作
- lindex相當(dāng)于java鏈表的get方法,他需要遍歷整個(gè)鏈表,性能O(n),隨著參數(shù)index的增加而增加
- ltrim 與字面上意思不一致,保留一段期間內(nèi)的值,刪掉區(qū)間以外的,可以通過這個(gè)來獲取一個(gè)定長列表,index可以是負(fù)數(shù),-1是倒數(shù)第一個(gè),-2 倒數(shù)第二個(gè)
內(nèi)部存儲(chǔ):快速列表
- Redis的底層存儲(chǔ)的不是一個(gè)簡單的LinkedList,而是一個(gè)快速列表的一個(gè)結(jié)構(gòu)
- 首先元素較少的時(shí)候,會(huì)使用一塊連續(xù)的內(nèi)存存儲(chǔ),這個(gè)結(jié)構(gòu)叫做ziplist,壓縮列表。他所有的元素彼此緊挨在一起存儲(chǔ),分配的是一塊連續(xù)的內(nèi)存(可以看作一個(gè)數(shù)組)
- 當(dāng)數(shù)據(jù)量比較多的時(shí)候才會(huì)改成quicklist。因?yàn)槠胀斜硇枰母郊又羔樥加每臻g太大比如我們list中都是int類型的數(shù)據(jù),那么每一個(gè)節(jié)點(diǎn)還需要維護(hù)兩個(gè)指針prev,next,指針?biāo)加玫膬?nèi)存比實(shí)際數(shù)據(jù)還要多
- 所以Redis將鏈表和ziplist結(jié)合使用,組成quicklist,也就是將多個(gè)ziplist用雙向指針串起來,就可以節(jié)省很多指針空間。如下圖。
hash(字典)
- Redis的字典相當(dāng)于Java中的HashMap,,他是無順序的字典結(jié)構(gòu),內(nèi)存存儲(chǔ)多個(gè)key,value,
- 實(shí)現(xiàn)結(jié)構(gòu)與Java中HashMap也類似,都是數(shù)組+鏈表的而為結(jié)構(gòu),如下圖,第一緯度的hash結(jié)構(gòu)數(shù)組會(huì)發(fā)生重合的情況,也就是兩個(gè)key的hash值一樣,就會(huì)使用鏈表的形式將元素串起來。
- 不同地方Redis的字典值只能是字符串,另外他們的rehash的方式也不一樣,因?yàn)镴ava的HashMap字典很大時(shí)候,rehash的耗時(shí)很多,而且一次性全部完成,Redis為追求高性能,不能堵塞服務(wù),所以采用漸進(jìn)式rehash策略
- 漸進(jìn)式rehash會(huì)在rehash的同時(shí),保留新舊兩個(gè)hash結(jié)構(gòu),查詢時(shí)候同時(shí)查詢兩個(gè)字典結(jié)構(gòu),后續(xù)Redis會(huì)在定時(shí)任務(wù)以及hash操作指令中,逐漸的將舊的hash內(nèi)容一點(diǎn)點(diǎn)遷移到新的hash結(jié)構(gòu),完成后才用新的。(空間換時(shí)間)
- 當(dāng)hash結(jié)構(gòu)最后一個(gè)元素被移除,該數(shù)據(jù)結(jié)構(gòu)會(huì)自動(dòng)刪掉,內(nèi)存自動(dòng)回收。
- 同字符串一樣,hash結(jié)構(gòu)也有單個(gè)key的計(jì)數(shù)功能,他對于指令是hincrby,和incr使用方法基本一致
set(集合)
- Redis的集合相當(dāng)于java中的HashSet,內(nèi)部的鍵值對是無序的,唯一的。他的內(nèi)部實(shí)現(xiàn)相當(dāng)于一個(gè)特殊的字典,字典中的所有value都是一個(gè)NULL而已,同樣當(dāng)集合最后一個(gè)元素被移除也會(huì)被刪除,自動(dòng)回收內(nèi)存。
- set結(jié)構(gòu)可以用來存儲(chǔ)中獎(jiǎng)用戶id,因?yàn)橛腥ブ氐墓δ?#xff0c;保證唯一性。
zset(有序列表)
- zset是Redis提供的最有特色的一個(gè)數(shù)據(jù)結(jié)構(gòu),類似java的sortedSet和HashMap的結(jié)合體,應(yīng)為他有兩者特點(diǎn),首先他是一個(gè)set,所以保證內(nèi)部每個(gè)value的唯一性,另外他給每個(gè)value的排序賦予一個(gè)權(quán)重score,代表這個(gè)value的排序權(quán)重,他內(nèi)部實(shí)現(xiàn)是一個(gè)叫”跳躍表“的數(shù)據(jù)結(jié)構(gòu)。
跳躍列表
-
zset的內(nèi)部排序功能是通過跳躍表數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)的,他的結(jié)構(gòu)非常特殊,因?yàn)閦set既要支持隨機(jī)的插入和刪除,所以他不能用數(shù)組,數(shù)組需要遍歷
-
Zset要支持按照score排序,這意味著有新元素插入,需要定位特定的位置插入點(diǎn),這樣才可以保證有序性,通常我們用二分查找(平均時(shí)間復(fù)雜度最小),但是二分查找對象都是數(shù)組,鏈表無法做到這樣就兩個(gè)條件矛盾來用如下的結(jié)構(gòu)來解決這個(gè)問題:變異形態(tài)的鏈表
-
比如我們平時(shí)公司的結(jié)構(gòu)中,研發(fā)人員平等,都有一個(gè)組長,各個(gè)組長平等,組長上有l(wèi)eader,各個(gè)leader之間平等,leader上沒有總監(jiān),依次向上到最后的CEO,跳躍表就類似這樣的一個(gè)層級結(jié)構(gòu)
-
最下面的一層是所有元素都串起來,然后每隔幾個(gè)元素挑選出一個(gè)代表,再將這幾個(gè)代表用另外一個(gè)指針串聯(lián)。接著在這些代表中繼續(xù)挑選出二級代表,形成新的金字塔結(jié)構(gòu)。如下:
-
解釋:
- 類比二分法,當(dāng)我們用二分發(fā)時(shí)候,先找到middle位置,比較中間位置的value和將要insert進(jìn)去元素的value,如果小,則將0~middle作為新的鏈表,如果大middle-max作為新的鏈表重新以上步驟,來到第二層。
- 如上圖,我們定位插入點(diǎn)時(shí)候,先從頂層開始,我們將最頂層的一個(gè)節(jié)點(diǎn)就相當(dāng)于中間位置,只不過他是我們通過特殊的數(shù)據(jù)結(jié)構(gòu)暴露出來的一個(gè)中間位置,然后類比上面步驟,我們進(jìn)入到第二層中,比較中間位置的L2和將要insert進(jìn)去元素的value,如果小,將L1層級中中0~L2位置元素作為新的鏈表,我們通過這樣逐層的比較,直到最底層找到合適的位置,將新的元素insert進(jìn)去。
- 這種做法就相當(dāng)于將二分法的步驟拆開后,得到N個(gè)鏈表,每個(gè)鏈表都是我們二分法中每個(gè)步驟中需要查找的那個(gè)半個(gè)數(shù)組而已,通過比較每個(gè)數(shù)組中的中間value值來得出最后的位置。用冗余的方式來避免鏈表的遍歷(替換數(shù)組的下標(biāo)索引功能)
-
跳躍表中節(jié)點(diǎn)可以在多個(gè)層級有多個(gè)身份,跳躍表采用隨機(jī)策略決定新元素可以到第幾層,L0層概率是1 ,L1 只有50%對半分,L2 再次25%,L3只有12.5%,依次,一直隨機(jī)到頂層L31,每次對半,到頂層基本不會(huì)有很多數(shù)據(jù)。
容器類型數(shù)據(jù)結(jié)構(gòu)的通用規(guī)則
- list,set, hash, zset四種數(shù)據(jù)結(jié)構(gòu)是容器型數(shù)據(jù)結(jié)構(gòu),他們共享下面幾個(gè)規(guī)則:
- create if not exists:如果容器不存在,那就創(chuàng)建一個(gè),子啊進(jìn)行操作,比如rpush操作剛開始沒有列表,會(huì)新建一個(gè)列表,然后在rpush元素
- drop if no elements:如果容器中沒有元素,會(huì)立即刪除容器,釋放內(nèi)存。意味著lpop操作完最后一個(gè)元素后列表就會(huì)消失。
- 過期時(shí)間設(shè)置,到期自動(dòng)刪除,注意如果一個(gè)key設(shè)置來過期時(shí)間后調(diào)用set方法修改,過期時(shí)間會(huì)消失。
上一篇Redis數(shù)據(jù)結(jié)構(gòu)以及對應(yīng)存儲(chǔ)策略
下一篇Redis基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)之二
總結(jié)
以上是生活随笔為你收集整理的Redis基础数据结构内部实现简单介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 刮痧板厚的好还是薄的好
- 下一篇: Redis高级数据结构原理解析-bitm