是數據結構而非類型redis
不少文章都會說,redis支持5種經常使用的數據類型,這實際上是存在很大的歧義。redis裏存的都是二進制數據,其實就是字節數組(byte[]),這些字節數據是沒有數據類型的,只有把它們按照合理的格式解碼後,能夠變成一個字符串,整數或對象,此時才具備數據類型。算法
這一點必需要記住。因此任何東西只要能轉化成字節數組(byte[])的,均可以存到redis裏。管你是字符串、數字、對象、圖片、聲音、視頻、仍是文件,只要變成byte數組。數組
所以redis裏的String指的並非字符串,它其實表示的是一種最簡單的數據結構,即一個key只能對應一個value。這裏的key和value都是byte數組,只不過key通常是由一個字符串轉換成的byte數組,value則根據實際須要而定。緩存
在特定狀況下,對value也會有一些要求,好比要進行自增或自減操做,那value對應的byte數組必需要能被解碼成一個數字才行,不然會報錯。服務器
那麼List這種數據結構,其實表示一個key能夠對應多個value,且value之間是有前後順序的,value值能夠重複。網絡
Set這種數據結構,表示一個key能夠對應多個value,且value之間是沒有前後順序的,value值也不能夠重複。數據結構
Hash這種數據結構,表示一個key能夠對應多個key-value對,此時這些key-value對之間的前後順序通常意義不大,這是一個按照名稱語義來訪問的數據結構,而非位置語義。多線程
Sorted Set這種數據結構,表示一個key能夠對應多個value,value之間是有大小排序的,value值不能夠重複。每一個value都和一個浮點數相關聯,該浮點數叫score。元素排序規則是:先按score排序,再按value排序。socket
相信如今你對這5種數據結構有了更清晰的認識,那它們的對應命令對你來講就是小case了。分佈式
集羣帶來的問題與解決思路
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
集羣帶來的好處是顯而易見的,好比容量增長、處理能力加強,還能夠按須要進行動態的擴容、縮容。但同時也會引入一些新的問題,至少會有下面這兩個。
一是數據分配:存數據時應該放到哪一個節點上,取數據時應該去哪一個節點上找。二是數據移動:集羣擴容,新增長節點時,該節點上的數據從何處來;集羣縮容,要剔除節點時,該節點上的數據往何處去。
上面這兩個問題有一個共同點就是,如何去描述和存儲數據與節點的映射關係。又由於數據的位置是由key決定的,因此問題就演變爲如何創建起各個key和集羣全部節點的關聯關係。
集羣的節點是相對固定和少數的,雖然有增長節點和剔除節點。但集羣裏存儲的key,則是徹底隨機、沒有規律、不可預測、數量龐多,還很是瑣碎。
這就比如一所大學和它的全部學生之間的關係。若是大學和學生直接掛鉤的話,必定會比較混亂。現實是它們之間又加入了好幾層,首先有院系,其次有專業,再者有年級,最後還有班級。通過這四層映射以後,關係就清爽不少了。
這實際上是一個很是重要的結論,這個世界上沒有什麼問題是不能經過加入一層來解決的。若是有,那就再加入一層。計算機裏也是這樣的。
redis在數據和節點之間又加入了一層,把這層稱爲槽(slot),因該槽主要和哈希有關,又叫哈希槽。
最後變成了,節點上放的是槽,槽裏放的是數據。槽解決的是粒度問題,至關於把粒度變大了,這樣便於數據移動。哈希解決的是映射問題,使用key的哈希值來計算所在的槽,便於數據分配。
能夠這樣來理解,你的學習桌子上堆滿了書,亂的很,想找到某本書很是困難。因而你買了幾個大的收納箱,把這些書按照書名的長度放入不一樣的收納箱,而後把這些收納箱放到桌子上。
這樣就變成了,桌子上是收納箱,收納箱裏是書籍。這樣書籍移動很方便,搬起一個箱子就走了。尋找書籍也很方便,只要數一數書名的長度,去對應的箱子裏找就好了。
其實咱們也沒作什麼,只是買了幾個箱子,按照某種規則把書裝入箱子。就這麼簡單的舉動,就完全改變了原來人心渙散的情況。是否是有點小小的神奇呢。
一個集羣只能有16384個槽,編號0-16383。這些槽會分配給集羣中的全部主節點,分配策略沒有要求。能夠指定哪些編號的槽分配給哪一個主節點。集羣會記錄節點和槽的對應關係。
接下來就須要對key求哈希值,而後對16384取餘,餘數是幾key就落入對應的槽裏。slot = CRC16(key) % 16384。
以槽爲單位移動數據,由於槽的數目是固定的,處理起來比較容易,這樣數據移動問題就解決了。
使用哈希函數計算出key的哈希值,這樣就能夠算出它對應的槽,而後利用集羣存儲的槽和節點的映射關係查詢出槽所在的節點,因而數據和節點就映射起來了,這樣數據分配問題就解決了。
我想說的是,通常的人只會去學習各類技術,高手更在意如何跳出技術,尋求一種解決方案或思路方向,順着這個方向走下去,八九不離十能找到你想要的答案。
集羣對命令操做的取捨
客戶端只要和集羣中的一個節點創建連接後,就能夠獲取到整個集羣的全部節點信息。此外還會獲取全部哈希槽和節點的對應關係信息,這些信息數據都會在客戶端緩存起來,由於這些信息至關有用。
客戶端能夠向任何節點發送請求,那麼拿到一個key後到底該向哪一個節點發請求呢?其實就是把集羣裏的那套key和節點的映射關係理論搬到客戶端來就好了。
因此客戶端須要實現一個和集羣端同樣的哈希函數,先計算出key的哈希值,而後再對16384取餘,這樣就找到了該key對應的哈希槽,利用客戶端緩存的槽和節點的對應關係信息,就能夠找到該key對應的節點了。
接下來發送請求就能夠了。還能夠把key和節點的映射關係緩存起來,下次再請求該key時,直接就拿到了它對應的節點,不用再計算一遍了。
理論和現實老是有差距的,集羣已經發生了變化,客戶端的緩存還沒來得及更新。確定會出現拿到一個key向對應的節點發請求,其實這個key已經不在那個節點上了。此時這個節點應該怎麼辦?
這個節點能夠去key實際所在的節點上拿到數據再返回給客戶端,也能夠直接告訴客戶端key已經不在我這裏了,同時附上key如今所在的節點信息,讓客戶端再去請求一次,相似於HTTP的302重定向。
這實際上是個選擇問題,也是個哲學問題。結果就是redis集羣選擇了後者。所以,節點只處理本身擁有的key,對於不擁有的key將返回重定向錯誤,即-MOVED key 127.0.0.1:6381,客戶端從新向這個新節點發送請求。
因此說選擇是一種哲學,也是個智慧。稍後再談這個問題。先來看看另外一個狀況,和這個問題有些相同點。
redis有一種命令能夠一次帶多個key,如MGET,我把這些稱爲多key命令。這個多key命令的請求被髮送到一個節點上,這裏有一個潛在的問題,不知道你們有沒有想到,就是這個命令裏的多個key必定都位於那同一個節點上嗎?
就分爲兩種狀況了,若是多個key不在同一個節點上,此時節點只能返回重定向錯誤了,可是多個key徹底可能位於多個不一樣的節點上,此時返回的重定向錯誤就會很是亂,因此redis集羣選擇不支持此種狀況。
若是多個key位於同一個節點上呢,理論上是沒有問題的,redis集羣是否支持就和redis的版本有關係了,具體使用時本身測試一下就好了。
在這個過程當中咱們發現了一件很有意義的事情,就是讓一組相關的key映射到同一個節點上是很是有必要的,這樣能夠提升效率,經過多key命令一次獲取多個值。
那麼問題來了,如何給這些key起名字才能讓他們落到同一個節點上,難不成都要先計算個哈希值,再取個餘數,太麻煩了吧。固然不是這樣了,redis已經幫咱們想好了。
能夠來簡單推理下,要想讓兩個key位於同一個節點上,它們的哈希值必需要同樣。要想哈希值同樣,傳入哈希函數的字符串必須同樣。那咱們只能傳進去兩個如出一轍的字符串了,那不就變成同一個key了,後面的會覆蓋前面的數據。
這裏的問題是咱們都是拿整個key去計算哈希值,這就致使key和參與計算哈希值的字符串耦合了,須要將它們解耦才行,就是key和參與計算哈希值的字符串有關可是又不同。
redis基於這個原理爲咱們提供了方案,叫作key哈希標籤。先看例子,{user1000}.following,{user1000}.followers,相信你已經看出了門道,就是僅使用Key中的位於{和}間的字符串參與計算哈希值。
這樣能夠保證哈希值相同,落到相同的節點上。可是key又是不一樣的,不會互相覆蓋。使用哈希標籤把一組相關的key關聯了起來,問題就這樣被輕鬆愉快地解決了。
相信你已經發現了,要解決問題靠的是巧妙的奇思妙想,而不是非要用牛逼的技術牛逼的算法。這就是小強,小而強大。
最後再來談選擇的哲學。redis的核心就是以最快的速度進行經常使用數據結構的key/value存取,以及圍繞這些數據結構的運算。對於與核心無關的或會拖累核心的都選擇弱化處理或不處理,這樣作是爲了保證核心的簡單、快速和穩定。
其實就是在廣度和深度面前,redis選擇了深度。因此節點不去處理本身不擁有的key,集羣不去支持多key命令。這樣一方面能夠快速地響應客戶端,另外一方面能夠避免在集羣內部有大量的數據傳輸與合併。
單線程模型
redis集羣的每一個節點裏只有一個線程負責接受和執行全部客戶端發送的請求。技術上使用多路複用I/O,使用Linux的epoll函數,這樣一個線程就能夠管理不少socket鏈接。
除此以外,選擇單線程還有如下這些緣由:
一、redis都是對內存的操做,速度極快(10W+QPS)
二、總體的時間主要都是消耗在了網絡的傳輸上
三、若是使用了多線程,則須要多線程同步,這樣實現起來會變的複雜
四、線程的加鎖時間甚至都超過了對內存操做的時間
五、多線程上下文頻繁的切換須要消耗更多的CPU時間
六、還有就是單線程自然支持原子操做,並且單線程的代碼寫起來更簡單
事務
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。
事務你們都知道,就是把多個操做捆綁在一塊兒,要麼都執行(成功了),要麼一個也不執行(回滾了)。redis也是支持事務的,但可能和你想要的不太同樣,一塊兒來看看吧。
redis的事務能夠分爲兩步,定義事務和執行事務。使用multi命令開啓一個事務,而後把要執行的全部命令都依次排上去。這就定義好了一個事務。此時使用exec命令來執行這個事務,或使用discard命令來放棄這個事務。
你可能但願在你的事務開始前,你關心的key不想被別人操做,那麼可使用watch命令來監視這些key,若是開始執行前這些key被其它命令操做了則會取消事務的。也可使用unwatch命令來取消對這些key的監視。
redis事務具備如下特色:
一、若是開始執行事務前出錯,則全部命令都不執行
二、一旦開始,則保證全部命令一次性按順序執行完而不被打斷
三、若是執行過程當中遇到錯誤,會繼續執行下去,不會中止的
四、對於執行過程當中遇到錯誤,是不會進行回滾的
看完這些,真想問一句話,你這能叫事務嗎?很顯然,這並非咱們一般認爲的事務,由於它連原子性都保證不了。保證不了原子性是由於redis不支持回滾,不過它也給出了不支持的理由。
不支持回滾的理由:
一、redis認爲,失敗都是由命令使用不當形成
二、redis這樣作,是爲了保持內部實現簡單快速
三、redis還認爲,回滾並不能解決全部問題
哈哈,這就是霸王條款,所以,好像使用redis事務的不太多
管道
客戶端和集羣的交互過程是串行化阻塞式的,即客戶端發送了一個命令後必須等到響應回來後才能發第二個命令,這一來一回就是一個往返時間。若是你有不少的命令,都這樣一個一個的來進行,會變得很慢。
redis提供了一種管道技術,可讓客戶端一次發送多個命令,期間不須要等待服務器端的響應,等全部的命令都發完了,再依次接收這些命令的所有響應。這就極大地節省了許多時間,提高了效率。
聰明的你是否是意識到了另一個問題,多個命令就是多個key啊,這不就是上面提到的多key操做嘛,那麼問題來了,你如何保證這多個key都是同一個節點上的啊,哈哈,redis集羣又放棄了對管道的支持。
不過能夠在客戶端模擬實現,就是使用多個鏈接往多個節點同時發送命令,而後等待全部的節點都返回了響應,再把它們按照發送命令的順序整理好,返回給用戶代碼。哎呀,好麻煩呀。
協議
簡單瞭解下redis的協議,知道redis的數據傳輸格式。
發送請求的協議:
*參數個數CRLF$參數1的字節數CRLF參數1的數據CRLF...$參數N的字節數CRLF參數N的數據CRLF
例如,SET name lixinjie,實際發送的數據是:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$8\r\nlixinjie\r\n
接受響應的協議:
單行回覆,第一個字節是+
錯誤消息,第一個字節是-
整型數字,第一個字節是:
批量回復,第一個字節是$
多個批量回復,第一個字節是*
例如,
+OK\r\n
-ERR Operation against\r\n
:1000\r\n
$6\r\nfoobar\r\n
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
可見redis的協議設計的很是簡單。
若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。