【面試】吃透了這些Redis知識點彪笼,面試官一定覺得你很NB(干貨 | 建議珍藏)]
是數(shù)據(jù)結(jié)構(gòu)而非類型
很多文章都會說,redis支持5種常用的數(shù)據(jù)類型蚂且,這其實是存在很大的歧義配猫。redis里存的都是二進(jìn)制數(shù)據(jù),其實就是字節(jié)數(shù)組(byte[])杏死,這些字節(jié)數(shù)據(jù)是沒有數(shù)據(jù)類型的泵肄,只有把它們按照合理的格式解碼后,可以變成一個字符串淑翼,整數(shù)或?qū)ο蟾玻藭r才具有數(shù)據(jù)類型。
這一點必須要記住玄括。所以任何東西只要能轉(zhuǎn)化成字節(jié)數(shù)組(byte[])的冯丙,都可以存到redis里。管你是字符串遭京、數(shù)字胃惜、對象、圖片哪雕、聲音船殉、視頻、還是文件热监,只要變成byte數(shù)組捺弦。
因此redis里的String指的并不是字符串,它其實表示的是一種最簡單的數(shù)據(jù)結(jié)構(gòu),即一個key只能對應(yīng)一個value列吼。這里的key和value都是byte數(shù)組幽崩,只不過key一般是由一個字符串轉(zhuǎn)換成的byte數(shù)組,value則根據(jù)實際需要而定寞钥。
在特定情況下慌申,對value也會有一些要求,比如要進(jìn)行自增或自減操作理郑,那value對應(yīng)的byte數(shù)組必須要能被解碼成一個數(shù)字才行蹄溉,否則會報錯。
那么List這種數(shù)據(jù)結(jié)構(gòu)您炉,其實表示一個key可以對應(yīng)多個value柒爵,且value之間是有先后順序的,value值可以重復(fù)赚爵。
Set這種數(shù)據(jù)結(jié)構(gòu)棉胀,表示一個key可以對應(yīng)多個value,且value之間是沒有先后順序的冀膝,value值也不可以重復(fù)唁奢。
Hash這種數(shù)據(jù)結(jié)構(gòu),表示一個key可以對應(yīng)多個key-value對窝剖,此時這些key-value對之間的先后順序一般意義不大麻掸,這是一個按照名稱語義來訪問的數(shù)據(jù)結(jié)構(gòu),而非位置語義赐纱。
Sorted Set這種數(shù)據(jù)結(jié)構(gòu)脊奋,表示一個key可以對應(yīng)多個value,value之間是有大小排序的千所,value值不可以重復(fù)狂魔。每個value都和一個浮點數(shù)相關(guān)聯(lián),該浮點數(shù)叫score淫痰。元素排序規(guī)則是:先按score排序最楷,再按value排序。
相信現(xiàn)在你對這5種數(shù)據(jù)結(jié)構(gòu)有了更清晰的認(rèn)識待错,那它們的對應(yīng)命令對你來說就是小case了籽孙。
集群帶來的問題與解決思路
集群帶來的好處是顯而易見的,比如容量增加火俄、處理能力增強(qiáng)犯建,還可以按需要進(jìn)行動態(tài)的擴(kuò)容、縮容瓜客。但同時也會引入一些新的問題适瓦,至少會有下面這兩個竿开。
一是數(shù)據(jù)分配:存數(shù)據(jù)時應(yīng)該放到哪個節(jié)點上,取數(shù)據(jù)時應(yīng)該去哪個節(jié)點上找玻熙。二是數(shù)據(jù)移動:集群擴(kuò)容否彩,新增加節(jié)點時,該節(jié)點上的數(shù)據(jù)從何處來嗦随;集群縮容列荔,要剔除節(jié)點時,該節(jié)點上的數(shù)據(jù)往何處去枚尼。
上面這兩個問題有一個共同點就是贴浙,如何去描述和存儲數(shù)據(jù)與節(jié)點的映射關(guān)系。又因為數(shù)據(jù)的位置是由key決定的署恍,所以問題就演變?yōu)槿绾谓⑵鸶鱾€key和集群所有節(jié)點的關(guān)聯(lián)關(guān)系崎溃。
集群的節(jié)點是相對固定和少數(shù)的霎奢,雖然有增加節(jié)點和剔除節(jié)點另玖。但集群里存儲的key,則是完全隨機(jī)梳毙、沒有規(guī)律唤殴、不可預(yù)測、數(shù)量龐多到腥,還非扯涫牛瑣碎。
這就好比一所大學(xué)和它的所有學(xué)生之間的關(guān)系乡范。如果大學(xué)和學(xué)生直接掛鉤的話配名,一定會比較混亂。現(xiàn)實是它們之間又加入了好幾層晋辆,首先有院系渠脉,其次有專業(yè),再者有年級瓶佳,最后還有班級芋膘。經(jīng)過這四層映射之后,關(guān)系就清爽很多了霸饲。
這其實是一個非常重要的結(jié)論为朋,這個世界上沒有什么問題是不能通過加入一層來解決的。如果有厚脉,那就再加入一層习寸。計算機(jī)里也是這樣的。
redis在數(shù)據(jù)和節(jié)點之間又加入了一層傻工,把這層稱為槽(slot)霞溪,因該槽主要和哈希有關(guān)孵滞,又叫哈希槽。
最后變成了鸯匹,節(jié)點上放的是槽坊饶,槽里放的是數(shù)據(jù)。槽解決的是粒度問題忽你,相當(dāng)于把粒度變大了幼东,這樣便于數(shù)據(jù)移動。哈希解決的是映射問題科雳,使用key的哈希值來計算所在的槽根蟹,便于數(shù)據(jù)分配。
可以這樣來理解糟秘,你的學(xué)習(xí)桌子上堆滿了書简逮,亂的很,想找到某本書非常困難尿赚。于是你買了幾個大的收納箱散庶,把這些書按照書名的長度放入不同的收納箱,然后把這些收納箱放到桌子上凌净。
這樣就變成了悲龟,桌子上是收納箱,收納箱里是書籍冰寻。這樣書籍移動很方便须教,搬起一個箱子就走了。尋找書籍也很方便斩芭,只要數(shù)一數(shù)書名的長度轻腺,去對應(yīng)的箱子里找就行了。
其實我們也沒做什么划乖,只是買了幾個箱子贬养,按照某種規(guī)則把書裝入箱子。就這么簡單的舉動琴庵,就徹底改變了原來一盤散沙的狀況误算。是不是有點小小的神奇呢。
一個集群只能有16384個槽迷殿,編號0-16383尉桩。這些槽會分配給集群中的所有主節(jié)點,分配策略沒有要求贪庙≈├纾可以指定哪些編號的槽分配給哪個主節(jié)點。集群會記錄節(jié)點和槽的對應(yīng)關(guān)系止邮。
接下來就需要對key求哈希值这橙,然后對16384取余奏窑,余數(shù)是幾key就落入對應(yīng)的槽里。slot = CRC16(key) % 16384屈扎。
以槽為單位移動數(shù)據(jù)埃唯,因為槽的數(shù)目是固定的,處理起來比較容易鹰晨,這樣數(shù)據(jù)移動問題就解決了墨叛。
使用哈希函數(shù)計算出key的哈希值,這樣就可以算出它對應(yīng)的槽模蜡,然后利用集群存儲的槽和節(jié)點的映射關(guān)系查詢出槽所在的節(jié)點漠趁,于是數(shù)據(jù)和節(jié)點就映射起來了,這樣數(shù)據(jù)分配問題就解決了忍疾。
我想說的是闯传,一般的人只會去學(xué)習(xí)各種技術(shù),高手更在乎如何跳出技術(shù)卤妒,尋求一種解決方案或思路方向甥绿,順著這個方向走下去,八九不離十能找到你想要的答案则披。
集群對命令操作的取舍
客戶端只要和集群中的一個節(jié)點建立鏈接后共缕,就可以獲取到整個集群的所有節(jié)點信息。此外還會獲取所有哈希槽和節(jié)點的對應(yīng)關(guān)系信息士复,這些信息數(shù)據(jù)都會在客戶端緩存起來骄呼,因為這些信息相當(dāng)有用。
客戶端可以向任何節(jié)點發(fā)送請求判没,那么拿到一個key后到底該向哪個節(jié)點發(fā)請求呢?其實就是把集群里的那套key和節(jié)點的映射關(guān)系理論搬到客戶端來就行了隅茎。
所以客戶端需要實現(xiàn)一個和集群端一樣的哈希函數(shù)澄峰,先計算出key的哈希值,然后再對16384取余辟犀,這樣就找到了該key對應(yīng)的哈希槽俏竞,利用客戶端緩存的槽和節(jié)點的對應(yīng)關(guān)系信息,就可以找到該key對應(yīng)的節(jié)點了堂竟。
接下來發(fā)送請求就可以了魂毁。還可以把key和節(jié)點的映射關(guān)系緩存起來,下次再請求該key時出嘹,直接就拿到了它對應(yīng)的節(jié)點席楚,不用再計算一遍了。
理論和現(xiàn)實總是有差距的税稼,集群已經(jīng)發(fā)生了變化烦秩,客戶端的緩存還沒來得及更新垮斯。肯定會出現(xiàn)拿到一個key向?qū)?yīng)的節(jié)點發(fā)請求只祠,其實這個key已經(jīng)不在那個節(jié)點上了兜蠕。此時這個節(jié)點應(yīng)該怎么辦?
這個節(jié)點可以去key實際所在的節(jié)點上拿到數(shù)據(jù)再返回給客戶端抛寝,也可以直接告訴客戶端key已經(jīng)不在我這里了熊杨,同時附上key現(xiàn)在所在的節(jié)點信息,讓客戶端再去請求一次盗舰,類似于HTTP的302重定向晶府。
這其實是個選擇問題,也是個哲學(xué)問題岭皂。結(jié)果就是redis集群選擇了后者郊霎。因此,節(jié)點只處理自己擁有的key爷绘,對于不擁有的key將返回重定向錯誤书劝,即-MOVED key 127.0.0.1:6381,客戶端重新向這個新節(jié)點發(fā)送請求土至。
所以說選擇是一種哲學(xué)购对,也是個智慧。稍后再談這個問題陶因。先來看看另一個情況骡苞,和這個問題有些相同點。
redis有一種命令可以一次帶多個key楷扬,如MGET解幽,我把這些稱為多key命令。這個多key命令的請求被發(fā)送到一個節(jié)點上烘苹,這里有一個潛在的問題躲株,不知道大家有沒有想到,就是這個命令里的多個key一定都位于那同一個節(jié)點上嗎镣衡?
就分為兩種情況了霜定,如果多個key不在同一個節(jié)點上,此時節(jié)點只能返回重定向錯誤了廊鸥,但是多個key完全可能位于多個不同的節(jié)點上望浩,此時返回的重定向錯誤就會非常亂,所以redis集群選擇不支持此種情況惰说。
如果多個key位于同一個節(jié)點上呢磨德,理論上是沒有問題的,redis集群是否支持就和redis的版本有關(guān)系了吆视,具體使用時自己測試一下就行了剖张。
在這個過程中我們發(fā)現(xiàn)了一件頗有意義的事情切诀,就是讓一組相關(guān)的key映射到同一個節(jié)點上是非常有必要的,這樣可以提高效率搔弄,通過多key命令一次獲取多個值幅虑。
那么問題來了,如何給這些key起名字才能讓他們落到同一個節(jié)點上顾犹,難不成都要先計算個哈希值倒庵,再取個余數(shù),太麻煩了吧炫刷。當(dāng)然不是這樣了擎宝,redis已經(jīng)幫我們想好了。
可以來簡單推理下浑玛,要想讓兩個key位于同一個節(jié)點上绍申,它們的哈希值必須要一樣。要想哈希值一樣顾彰,傳入哈希函數(shù)的字符串必須一樣极阅。那我們只能傳進(jìn)去兩個一模一樣的字符串了,那不就變成同一個key了涨享,后面的會覆蓋前面的數(shù)據(jù)筋搏。
這里的問題是我們都是拿整個key去計算哈希值,這就導(dǎo)致key和參與計算哈希值的字符串耦合了厕隧,需要將它們解耦才行奔脐,就是key和參與計算哈希值的字符串有關(guān)但是又不一樣。
redis基于這個原理為我們提供了方案吁讨,叫做key哈希標(biāo)簽髓迎。先看例子,{user1000}.following建丧,{user1000}.followers排龄,相信你已經(jīng)看出了門道,就是僅使用Key中的位于{和}間的字符串參與計算哈希值茶鹃。
這樣可以保證哈希值相同,落到相同的節(jié)點上艰亮。但是key又是不同的闭翩,不會互相覆蓋。使用哈希標(biāo)簽把一組相關(guān)的key關(guān)聯(lián)了起來迄埃,問題就這樣被輕松愉快地解決了疗韵。
相信你已經(jīng)發(fā)現(xiàn)了,要解決問題靠的是巧妙的奇思妙想侄非,而不是非要用牛逼的技術(shù)牛逼的算法蕉汪。這就是小強(qiáng)流译,小而強(qiáng)大。
最后再來談選擇的哲學(xué)者疤。redis的核心就是以最快的速度進(jìn)行常用數(shù)據(jù)結(jié)構(gòu)的key/value存取福澡,以及圍繞這些數(shù)據(jù)結(jié)構(gòu)的運(yùn)算。對于與核心無關(guān)的或會拖累核心的都選擇弱化處理或不處理驹马,這樣做是為了保證核心的簡單革砸、快速和穩(wěn)定。
其實就是在廣度和深度面前糯累,redis選擇了深度算利。所以節(jié)點不去處理自己不擁有的key,集群不去支持多key命令泳姐。這樣一方面可以快速地響應(yīng)客戶端效拭,另一方面可以避免在集群內(nèi)部有大量的數(shù)據(jù)傳輸與合并。
單線程模型
redis集群的每個節(jié)點里只有一個線程負(fù)責(zé)接受和執(zhí)行所有客戶端發(fā)送的請求胖秒。技術(shù)上使用多路復(fù)用I/O缎患,使用Linux的epoll函數(shù),這樣一個線程就可以管理很多socket連接扒怖。
除此之外较锡,選擇單線程還有以下這些原因:
1、redis都是對內(nèi)存的操作盗痒,速度極快(10W+QPS)
2蚂蕴、整體的時間主要都是消耗在了網(wǎng)絡(luò)的傳輸上
3、如果使用了多線程俯邓,則需要多線程同步骡楼,這樣實現(xiàn)起來會變的復(fù)雜
4、線程的加鎖時間甚至都超過了對內(nèi)存操作的時間
5稽鞭、多線程上下文頻繁的切換需要消耗更多的CPU時間
6鸟整、還有就是單線程天然支持原子操作,而且單線程的代碼寫起來更簡單
事務(wù)
事務(wù)大家都知道朦蕴,就是把多個操作捆綁在一起篮条,要么都執(zhí)行(成功了),要么一個也不執(zhí)行(回滾了)吩抓。redis也是支持事務(wù)的涉茧,但可能和你想要的不太一樣,一起來看看吧疹娶。
redis的事務(wù)可以分為兩步伴栓,定義事務(wù)和執(zhí)行事務(wù)。使用multi命令開啟一個事務(wù),然后把要執(zhí)行的所有命令都依次排上去钳垮。這就定義好了一個事務(wù)惑淳。此時使用exec命令來執(zhí)行這個事務(wù),或使用discard命令來放棄這個事務(wù)饺窿。
你可能希望在你的事務(wù)開始前歧焦,你關(guān)心的key不想被別人操作,那么可以使用watch命令來監(jiān)視這些key短荐,如果開始執(zhí)行前這些key被其它命令操作了則會取消事務(wù)的倚舀。也可以使用unwatch命令來取消對這些key的監(jiān)視。
redis事務(wù)具有以下特點:
1忍宋、如果開始執(zhí)行事務(wù)前出錯痕貌,則所有命令都不執(zhí)行
2、一旦開始糠排,則保證所有命令一次性按順序執(zhí)行完而不被打斷
3舵稠、如果執(zhí)行過程中遇到錯誤,會繼續(xù)執(zhí)行下去入宦,不會停止的
4哺徊、對于執(zhí)行過程中遇到錯誤,是不會進(jìn)行回滾的
看完這些乾闰,真想問一句話落追,你這能叫事務(wù)嗎?很顯然涯肩,這并不是我們通常認(rèn)為的事務(wù)轿钠,因為它連原子性都保證不了。保證不了原子性是因為redis不支持回滾病苗,不過它也給出了不支持的理由疗垛。
不支持回滾的理由:
1、redis認(rèn)為硫朦,失敗都是由命令使用不當(dāng)造成
2贷腕、redis這樣做,是為了保持內(nèi)部實現(xiàn)簡單快速
3咬展、redis還認(rèn)為泽裳,回滾并不能解決所有問題
哈哈,這就是霸王條款破婆,因此涮总,好像使用redis事務(wù)的不太多
管道
客戶端和集群的交互過程是串行化阻塞式的,即客戶端發(fā)送了一個命令后必須等到響應(yīng)回來后才能發(fā)第二個命令荠割,這一來一回就是一個往返時間妹卿。如果你有很多的命令,都這樣一個一個的來進(jìn)行蔑鹦,會變得很慢夺克。
redis提供了一種管道技術(shù),可以讓客戶端一次發(fā)送多個命令嚎朽,期間不需要等待服務(wù)器端的響應(yīng)铺纽,等所有的命令都發(fā)完了,再依次接收這些命令的全部響應(yīng)哟忍。這就極大地節(jié)省了許多時間狡门,提升了效率。
聰明的你是不是意識到了另外一個問題锅很,多個命令就是多個key啊其馏,這不就是上面提到的多key操作嘛,那么問題來了爆安,你如何保證這多個key都是同一個節(jié)點上的啊叛复,哈哈,redis集群又放棄了對管道的支持扔仓。
不過可以在客戶端模擬實現(xiàn)褐奥,就是使用多個連接往多個節(jié)點同時發(fā)送命令,然后等待所有的節(jié)點都返回了響應(yīng)翘簇,再把它們按照發(fā)送命令的順序整理好撬码,返回給用戶代碼。哎呀版保,好麻煩呀呜笑。
協(xié)議
簡單了解下redis的協(xié)議,知道redis的數(shù)據(jù)傳輸格式找筝。
發(fā)送請求的協(xié)議:
*參數(shù)個數(shù)CRLF參數(shù)N的字節(jié)數(shù)CRLF參數(shù)N的數(shù)據(jù)CRLF
例如蹈垢,SET name lixinjie,實際發(fā)送的數(shù)據(jù)是:
*3\r\n4\r\nname\r\n$8\r\nlixinjie\r\n
接受響應(yīng)的協(xié)議:
單行回復(fù)袖裕,第一個字節(jié)是+
錯誤消息曹抬,第一個字節(jié)是-
整型數(shù)字,第一個字節(jié)是:
批量回復(fù)急鳄,第一個字節(jié)是$
多個批量回復(fù)谤民,第一個字節(jié)是*
例如,
+OK\r\n
-ERR Operation against\r\n
:1000\r\n
$6\r\nfoobar\r\n
*2\r\n3\r\nbar\r\n
可見redis的協(xié)議設(shè)計的非常簡單疾宏。
Memo
本文轉(zhuǎn)載至 https://www.cnblogs.com/lixinjie/p/a-key-point-of-redis-in-interview.html