是數(shù)據(jù)結構而非類型
很多文章都會說誊垢,redis支持5種常用的?數(shù)據(jù)類型?掉弛,這其實是存在很大的歧義。redis里存的都是二進制數(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ù)結構澳泵,即一個key只能對應一個value实愚。這里的key和value都是byte數(shù)組,只不過key一般是由一個字符串轉(zhuǎn)換成的byte數(shù)組兔辅,value則根據(jù)實際需要而定腊敲。
在特定情況下,對value也會有一些要求维苔,比如要進行自增或自減操作碰辅,那value對應的byte數(shù)組必須要能被解碼成一個數(shù)字才行,否則會報錯介时。
那么List這種數(shù)據(jù)結構没宾,其實表示一個key可以對應多個value,且value之間是有先后順序的沸柔,value值可以重復循衰。
Set這種數(shù)據(jù)結構,表示一個key可以對應多個value褐澎,且value之間是沒有先后順序的会钝,value值也不可以重復。
Hash這種數(shù)據(jù)結構乱凿,表示一個key可以對應多個key-value對顽素,此時這些key-value對之間的先后順序一般意義不大,這是一個按照名稱語義來訪問的數(shù)據(jù)結構徒蟆,而非位置語義胁出。
Sorted Set這種數(shù)據(jù)結構,表示一個key可以對應多個value段审,value之間是有大小排序的全蝶,value值不可以重復。每個value都和一個浮點數(shù)相關聯(lián),該浮點數(shù)叫score抑淫。元素排序規(guī)則是:先按score排序绷落,再按value排序。
相信現(xiàn)在你對這5種數(shù)據(jù)結構有了更清晰的認識始苇,那它們的對應命令對你來說就是小case了砌烁。
集群帶來的問題與解決思路
集群帶來的好處是顯而易見的,比如容量增加催式、處理能力增強函喉,還可以按需要進行動態(tài)的擴容、縮容荣月。但同時也會引入一些新的問題管呵,至少會有下面這兩個。
一是數(shù)據(jù)分配:存數(shù)據(jù)時應該放到哪個節(jié)點上哺窄,取數(shù)據(jù)時應該去哪個節(jié)點上找捐下。二是數(shù)據(jù)移動:集群擴容,新增加節(jié)點時萌业,該節(jié)點上的數(shù)據(jù)從何處來坷襟;集群縮容,要剔除節(jié)點時生年,該節(jié)點上的數(shù)據(jù)往何處去啤握。
上面這兩個問題有一個共同點就是,如何去描述和存儲數(shù)據(jù)與節(jié)點的映射關系晶框。又因為數(shù)據(jù)的位置是由key決定的,所以問題就演變?yōu)槿绾谓⑵鸶鱾€key和集群所有節(jié)點的關聯(lián)關系懂从。
集群的節(jié)點是相對固定和少數(shù)的授段,雖然有增加節(jié)點和剔除節(jié)點。但集群里存儲的key番甩,則是完全隨機侵贵、沒有規(guī)律、不可預測缘薛、數(shù)量龐多窍育,還非常瑣碎宴胧。
這就好比一所大學和它的所有學生之間的關系漱抓。如果大學和學生直接掛鉤的話,一定會比較混亂∷∑耄現(xiàn)實是它們之間又加入了好幾層乞娄,首先有院系,其次有專業(yè),再者有年級仪或,最后還有班級确镊。經(jīng)過這四層映射之后,關系就清爽很多了范删。
這其實是一個非常重要的結論蕾域,這個世界上沒有什么問題是不能通過加入一層來解決的。如果有到旦,那就再加入一層旨巷。計算機里也是這樣的。
redis在數(shù)據(jù)和節(jié)點之間又加入了一層厢绝,把這層稱為槽(slot)契沫,因該槽主要和哈希有關,又叫哈希槽昔汉。
最后變成了懈万,節(jié)點上放的是槽,槽里放的是數(shù)據(jù)靶病。槽解決的是粒度問題会通,相當于把粒度變大了,這樣便于數(shù)據(jù)移動娄周。哈希解決的是映射問題涕侈,使用key的哈希值來計算所在的槽,便于數(shù)據(jù)分配煤辨。
可以這樣來理解裳涛,你的學習桌子上堆滿了書,亂的很众辨,想找到某本書非常困難端三。于是你買了幾個大的收納箱,把這些書按照書名的長度放入不同的收納箱鹃彻,然后把這些收納箱放到桌子上郊闯。
這樣就變成了,桌子上是收納箱蛛株,收納箱里是書籍团赁。這樣書籍移動很方便,搬起一個箱子就走了谨履。尋找書籍也很方便欢摄,只要數(shù)一數(shù)書名的長度,去對應的箱子里找就行了屉符。
其實我們也沒做什么剧浸,只是買了幾個箱子锹引,按照某種規(guī)則把書裝入箱子。就這么簡單的舉動唆香,就徹底改變了原來一盤散沙的狀況嫌变。是不是有點小小的神奇呢。
一個集群只能有16384個槽躬它,編號0-16383腾啥。這些槽會分配給集群中的所有主節(jié)點,分配策略沒有要求冯吓√却可以指定哪些編號的槽分配給哪個主節(jié)點。集群會記錄節(jié)點和槽的對應關系组贺。
接下來就需要對key求哈希值凸舵,然后對16384取余,余數(shù)是幾key就落入對應的槽里失尖。?slot = CRC16(key) % 16384?啊奄。
以槽為單位移動數(shù)據(jù),因為槽的數(shù)目是固定的掀潮,處理起來比較容易菇夸,這樣數(shù)據(jù)移動問題就解決了。
使用哈希函數(shù)計算出key的哈希值仪吧,這樣就可以算出它對應的槽庄新,然后利用集群存儲的槽和節(jié)點的映射關系查詢出槽所在的節(jié)點,于是數(shù)據(jù)和節(jié)點就映射起來了薯鼠,這樣數(shù)據(jù)分配問題就解決了择诈。
我想說的是,一般的人只會去學習各種技術出皇,高手更在乎如何跳出技術吭从,尋求一種解決方案或思路方向,順著這個方向走下去恶迈,八九不離十能找到你想要的答案。
集群對命令操作的取舍
客戶端只要和集群中的一個節(jié)點建立鏈接后谱醇,就可以獲取到整個集群的所有節(jié)點信息暇仲。此外還會獲取所有哈希槽和節(jié)點的對應關系信息,這些信息數(shù)據(jù)都會在客戶端緩存起來副渴,因為這些信息相當有用奈附。
客戶端可以向任何節(jié)點發(fā)送請求,那么拿到一個key后到底該向哪個節(jié)點發(fā)請求呢煮剧?其實就是把集群里的那套key和節(jié)點的映射關系理論搬到客戶端來就行了斥滤。
所以客戶端需要實現(xiàn)一個和集群端一樣的哈希函數(shù)将鸵,先計算出key的哈希值,然后再對16384取余佑颇,這樣就找到了該key對應的哈希槽顶掉,利用客戶端緩存的槽和節(jié)點的對應關系信息,就可以找到該key對應的節(jié)點了挑胸。
接下來發(fā)送請求就可以了痒筒。還可以把key和節(jié)點的映射關系緩存起來,下次再請求該key時茬贵,直接就拿到了它對應的節(jié)點簿透,不用再計算一遍了。
理論和現(xiàn)實總是有差距的解藻,集群已經(jīng)發(fā)生了變化老充,客戶端的緩存還沒來得及更新∶螅肯定會出現(xiàn)拿到一個key向?qū)墓?jié)點發(fā)請求啡浊,其實這個key已經(jīng)不在那個節(jié)點上了。此時這個節(jié)點應該怎么辦路狮?
這個節(jié)點可以去key實際所在的節(jié)點上拿到數(shù)據(jù)再返回給客戶端虫啥,也可以直接告訴客戶端key已經(jīng)不在我這里了,同時附上key現(xiàn)在所在的節(jié)點信息奄妨,讓客戶端再去請求一次涂籽,類似于HTTP的302重定向。
這其實是個選擇問題砸抛,也是個哲學問題评雌。結果就是redis集群選擇了后者。因此直焙,節(jié)點只處理自己擁有的key景东,對于不擁有的key將返回重定向錯誤,即?-MOVED key 127.0.0.1:6381?奔誓,客戶端重新向這個新節(jié)點發(fā)送請求斤吐。
所以說選擇是一種哲學,也是個智慧厨喂。稍后再談這個問題和措。先來看看另一個情況,和這個問題有些相同點蜕煌。
redis有一種命令可以一次帶多個key派阱,如MGET,我把這些稱為多key命令斜纪。這個多key命令的請求被發(fā)送到一個節(jié)點上贫母,這里有一個潛在的問題文兑,不知道大家有沒有想到,就是這個命令里的多個key一定都位于那同一個節(jié)點上嗎腺劣?
就分為兩種情況了绿贞,如果多個key不在同一個節(jié)點上,此時節(jié)點只能返回重定向錯誤了誓酒,但是多個key完全可能位于多個不同的節(jié)點上樟蠕,此時返回的重定向錯誤就會非常亂,所以redis集群選擇不支持此種情況靠柑。
如果多個key位于同一個節(jié)點上呢寨辩,理論上是沒有問題的,redis集群是否支持就和redis的版本有關系了歼冰,具體使用時自己測試一下就行了靡狞。
在這個過程中我們發(fā)現(xiàn)了一件頗有意義的事情,就是讓一組相關的key映射到同一個節(jié)點上是非常有必要的隔嫡,這樣可以提高效率甸怕,通過多key命令一次獲取多個值。
那么問題來了腮恩,如何給這些key起名字才能讓他們落到同一個節(jié)點上梢杭,難不成都要先計算個哈希值,再取個余數(shù)秸滴,太麻煩了吧武契。當然不是這樣了,redis已經(jīng)幫我們想好了荡含。
可以來簡單推理下咒唆,要想讓兩個key位于同一個節(jié)點上,它們的哈希值必須要一樣释液。要想哈希值一樣全释,傳入哈希函數(shù)的字符串必須一樣。那我們只能傳進去兩個一模一樣的字符串了误债,那不就變成同一個key了浸船,后面的會覆蓋前面的數(shù)據(jù)。
這里的問題是我們都是拿整個key去計算哈希值寝蹈,這就導致key和參與計算哈希值的字符串耦合了糟袁,需要將它們解耦才行,就是key和參與計算哈希值的字符串有關但是又不一樣躺盛。
redis基于這個原理為我們提供了方案,叫做key哈希標簽形帮。先看例子槽惫,{?user1000?}.following周叮,{user1000?}.followers,相信你已經(jīng)看出了門道界斜,就是僅使用Key中的位于?{?和?}?間的字符串參與計算哈希值仿耽。
這樣可以保證哈希值相同,落到相同的節(jié)點上各薇。但是key又是不同的项贺,不會互相覆蓋。使用哈希標簽把一組相關的key關聯(lián)了起來峭判,問題就這樣被輕松愉快地解決了开缎。
相信你已經(jīng)發(fā)現(xiàn)了,要解決問題靠的是巧妙的奇思妙想林螃,而不是非要用牛逼的技術牛逼的算法奕删。這就是小強,小而強大疗认。
最后再來談選擇的哲學完残。redis的核心就是以最快的速度進行常用數(shù)據(jù)結構的key/value存取,以及圍繞這些數(shù)據(jù)結構的運算横漏。對于與核心無關的或會拖累核心的都選擇弱化處理或不處理谨设,這樣做是為了保證核心的簡單、快速和穩(wěn)定缎浇。
其實就是在廣度和深度面前扎拣,redis選擇了深度。所以節(jié)點不去處理自己不擁有的key华畏,集群不去支持多key命令鹏秋。這樣一方面可以快速地響應客戶端,另一方面可以避免在集群內(nèi)部有大量的數(shù)據(jù)傳輸與合并亡笑。
單線程模型
redis集群的每個節(jié)點里只有一個線程負責接受和執(zhí)行所有客戶端發(fā)送的請求侣夷。技術上使用多路復用I/O,使用Linux的epoll函數(shù)仑乌,這樣一個線程就可以管理很多socket連接百拓。
除此之外,選擇單線程還有以下這些原因:
1晰甚、redis都是對內(nèi)存的操作衙传,速度極快(10W+QPS)
2、整體的時間主要都是消耗在了網(wǎng)絡的傳輸上
3厕九、如果使用了多線程蓖捶,則需要多線程同步,這樣實現(xiàn)起來會變的復雜
4扁远、線程的加鎖時間甚至都超過了對內(nèi)存操作的時間
5俊鱼、多線程上下文頻繁的切換需要消耗更多的CPU時間
6刻像、還有就是單線程天然支持原子操作,而且單線程的代碼寫起來更簡單
事務
事務大家都知道并闲,就是把多個操作捆綁在一起细睡,要么都執(zhí)行(成功了),要么一個也不執(zhí)行(回滾了)帝火。redis也是支持事務的溜徙,但可能和你想要的不太一樣,一起來看看吧犀填。
redis的事務可以分為兩步蠢壹,定義事務和執(zhí)行事務。使用multi命令開啟一個事務宏浩,然后把要執(zhí)行的所有命令都依次排上去知残。這就定義好了一個事務。此時使用exec命令來執(zhí)行這個事務比庄,或使用discard命令來放棄這個事務求妹。
你可能希望在你的事務開始前,你關心的key不想被別人操作佳窑,那么可以使用watch命令來監(jiān)視這些key制恍,如果開始執(zhí)行前這些key被其它命令操作了則會取消事務的。也可以使用unwatch命令來取消對這些key的監(jiān)視神凑。
redis事務具有以下特點:
1净神、如果開始執(zhí)行事務前出錯,則所有命令都不執(zhí)行
2溉委、一旦開始鹃唯,則保證所有命令一次性按順序執(zhí)行完而不被打斷
3、如果執(zhí)行過程中遇到錯誤瓣喊,會繼續(xù)執(zhí)行下去坡慌,不會停止的
4、對于執(zhí)行過程中遇到錯誤藻三,是不會進行回滾的
看完這些洪橘,真想問一句話,你這能叫事務嗎棵帽?很顯然熄求,這并不是我們通常認為的事務,因為它連原子性都保證不了逗概。保證不了原子性是因為redis不支持回滾弟晚,不過它也給出了不支持的理由。
不支持回滾的理由:
1、redis認為卿城,失敗都是由命令使用不當造成
2淑履、redis這樣做,是為了保持內(nèi)部實現(xiàn)簡單快速
3藻雪、redis還認為,回滾并不能解決所有問題
哈哈狸吞,這就是霸王條款勉耀,因此,好像使用redis事務的不太多
管道
客戶端和集群的交互過程是串行化阻塞式的蹋偏,即客戶端發(fā)送了一個命令后必須等到響應回來后才能發(fā)第二個命令便斥,這一來一回就是一個往返時間。?如果你有很多的命令威始,都這樣一個一個的來進行枢纠,會變得很慢。
redis提供了一種管道技術黎棠,可以讓客戶端一次發(fā)送多個命令晋渺,期間不需要等待服務器端的響應,等所有的命令都發(fā)完了脓斩,再依次接收這些命令的全部響應木西。?這就極大地節(jié)省了許多時間,提升了效率随静。
聰明的你是不是意識到了另外一個問題八千,多個命令就是多個key啊,這不就是上面提到的多key操作嘛燎猛,那么問題來了恋捆,你如何保證這多個key都是同一個節(jié)點上的啊,哈哈重绷,redis集群又放棄了對管道的支持沸停。
不過可以在客戶端模擬實現(xiàn),就是使用多個連接往多個節(jié)點同時發(fā)送命令论寨,然后等待所有的節(jié)點都返回了響應星立,再把它們按照發(fā)送命令的順序整理好,返回給用戶代碼葬凳。?哎呀绰垂,好麻煩呀。
協(xié)議
簡單了解下redis的協(xié)議火焰,知道redis的數(shù)據(jù)傳輸格式劲装。
發(fā)送請求的協(xié)議:
*?參數(shù)個數(shù)?CRLF?$?參數(shù)1的字節(jié)數(shù)?CRLF?參數(shù)1的數(shù)據(jù)?CRLF...?$?參數(shù)N的字節(jié)數(shù)?CRLF?參數(shù)N的數(shù)據(jù)?CRLF
例如,SET name lixinjie,實際發(fā)送的數(shù)據(jù)是:
*?3?\r\n?$?3?\r\n?SET?\r\n?$?4?\r\n?name?\r\n?$?8?\r\n?lixinjie?\r\n
接受響應的協(xié)議:
單行回復占业,第一個字節(jié)是
+
錯誤消息绒怨,第一個字節(jié)是?-
整型數(shù)字,第一個字節(jié)是?:
批量回復谦疾,第一個字節(jié)是?$
多個批量回復南蹂,第一個字節(jié)是?*
例如,
+?OK?\r\n
-?ERR Operation against?\r\n
:?1000?\r\n
$?6?\r\n?foobar?\r\n
*?2?\r\n?$?3?\r\n?foo?\r\n?$?3?\r\n?bar?\r\n
可見redis的協(xié)議設計的非常簡單念恍。
和大家的分享就到這吧六剥!有收獲,需要Java資料的可以關注小編峰伙。私信“架構”可以獲取本人整理的一份java架構進階資料疗疟。
?