接上篇 爲何要用Redis,今天來聊聊具體的Redis數據類型與命令。本篇是深刻理解Redis的一個重要基礎,請坐穩,前方 長文預警。redis
本系列內容基於:redis-3.2.12
文中不會介紹全部命令,主要是工做中常常遇到的。shell
平時咱們看的大部分資料,都是簡單粗暴的告訴咱們這個命令幹嗎,那個命令須要幾個參數。這種方式只會知其然不知其因此然,本文從命令的時間複雜度到用途,再到對應類型在Redis低層採用何種結構保存數據,但願讓你們認識的更深入,使用時內心更有底。緩存
應該講這是Redis中使用的最普遍的數據類型。該類型中的一些命令使用場景很是普遍。好比:服務器
注:表格中僅僅說明了String中的12個命令,使用場景也僅列舉了部分。
咱們時常被人說教 MSET/MGET 這類命令少用,由於他們的時間複雜度是O(n),但其實這裏注意,n表示的是本次設置或讀取的key個數,因此若是你批量讀取的key並非不少,每一個key的內容也不是很大,那麼使用批量操做命令反而可以節省網絡請求、傳輸的時間。網絡
String類型的數據最終是如何在Redis中保存的呢?若是要細究的話,得先從 SDS
這個結構提及,不過今天先按下不表這源碼部分的細節,只談其內部保存的數據結構。最終咱們設置的字符串都會以三種形式中的一種被存儲下來。數據結構
結合代碼來看看Redis對這三種數據結構是如何決策的。當咱們在客戶端使用命令 SET test hello,redis
時,客戶端會把命令保存到一個buf中,而後按照收到的命令前後順序依次執行。這其中有一個函數是:processMultibulkBuffer()
,它內部調用了 createStringObject()
函數:函數
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject(const char *ptr, size_t len) { // 檢查保存的字符串長度,選擇對應類型 if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); }
不懂C語言沒關係,這裏就是檢查咱們輸入的字符串 hello,redis
長度是否超過了 44 ,若是超過了用類型 raw
,沒有則選用 embstr
。實驗看看:性能
127.0.0.1:6379> SET test 12345678901234567890123456789012345678901234 // len=44 OK 127.0.0.1:6379> OBJECT encoding test "embstr" 127.0.0.1:6379> SET test 123456789012345678901234567890123456789012345 // len=45 OK 127.0.0.1:6379> OBJECT encoding test "raw"
能夠看到,一旦超過44,底層類型就變成了:raw
。等等,上面咱們不是還提到有一個 int
類型嗎?從函數裏邊徹底看不到它的蹤影啊?不急,當咱們輸入的這條命令真的要開始執行時,也就是調用函數 setCommand()
時,會觸發一個 tryObjectEncoding()
函數,這個函數的做用是試圖對輸入的字符串進行壓縮,繼續看看代碼:測試
robj *tryObjectEncoding(robj *o) { ... ... len = sdslen(s); // 長度小於等於20,而且可以轉成長整形 if(len <= 20 && string2l(s,len,&value)) { o->encoding = OBJ_ENCODING_INT; } ... ... }
這個函數被我大幅縮水了,可是簡單咱們可以看到它判斷長度是否小於等於20,而且嘗試轉化成整型,看看例子。網站
9223372036854775807 是8位字節可表示的最大整數,它的16進制形式是: 0x7fffffffffffffffL
127.0.0.1:6379> SET test 9223372036854775807 OK 127.0.0.1:6379> OBJECT encoding test "int" 127.0.0.1:6379> SET test 9223372036854775808 // 比上面大1 OK 127.0.0.1:6379> OBJECT encoding test "embstr"
至此,關於String的類型選擇流程完畢了。這對咱們的參考價值是,咱們在使用String類型保存數據時,要考慮到底層對應不一樣的類型,不一樣的類型在Redis內部會執行不一樣的流程,其所對應的執行效率、內存消耗都是不一樣的。
咱們常常用它來保存一個結構化的數據,好比與一個用戶相關的緩存信息。若是使用普通的String類型,須要對字符串進行序列化與反序列化,無疑增長額外開銷,而且每次讀取都只能所有讀取出來。
Hash類型保存的結構話數據,很是像MySQL中的一條記錄,咱們能夠方便修改某一個字段,可是它更具靈活性,每一個記錄可以含有不一樣的字段。
在內部Hash類型數據可能存在兩種類型的數據結構:
對於Hash,Redis 首先默認給它設置使用 ZipList
數據結構,後續根據條件進行判斷是否須要改變。
void hsetCommand(client *c) { int update; robj *o; if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; hashTypeTryConversion(o,c->argv,2,3);// 根據長度決策 ... ... update = hashTypeSet(o,c->argv[2],c->argv[3]);// 根據元素個數決策 addReply(c, update ? shared.czero : shared.cone); ... ... }
hashTypeLookupWriteOrCreate()
內部會調用 createHashObject()
建立Hash對象。
robj *createHashObject(void) { unsigned char *zl = ziplistNew(); robj *o = createObject(OBJ_HASH, zl); o->encoding = OBJ_ENCODING_ZIPLIST;// 設置編碼 ziplist return o; }
hashTypeTryConversion()
函數內部根據是否超過 hash_max_ziplist_value
限制的長度(64),來決定低層的數據結構。
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { int i; if (o->encoding != OBJ_ENCODING_ZIPLIST) return; for (i = start; i <= end; i++) { // 檢查 field 與 value 長度是否超長 if (sdsEncodedObject(argv[i]) && sdslen(argv[i]->ptr) > server.hash_max_ziplist_value) { hashTypeConvert(o, OBJ_ENCODING_HT); break; } } }
而後在函數 hashTypeSet()
中檢查field個數是否超過了 hash_max_ziplist_entries
的限制(512個)。
int hashTypeSet(robj *o, robj *field, robj *value) { int update = 0; if (o->encoding == OBJ_ENCODING_ZIPLIST) { ... ... // 檢查field個數是否超過512 if (hashTypeLength(o) > server.hash_max_ziplist_entries) hashTypeConvert(o, OBJ_ENCODING_HT); } else if (o->encoding == OBJ_ENCODING_HT) { ... ... } ... ... return update; }
來驗證一下上面的邏輯:
127.0.0.1:6379> HSET test name qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjsl (integer) 1 127.0.0.1:6379> HSTRLEN test name (integer) 64 127.0.0.1:6379> OBJECT encoding test "ziplist" 127.0.0.1:6379> HSET test name qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjslq (integer) 0 127.0.0.1:6379> HSTRLEN test name (integer) 65 127.0.0.1:6379> OBJECT encoding test "hashtable"
關於key設置超過64,以及field個數超過512的限制狀況,你們可自行測試。
List類型的用途也是很是普遍,主要歸納下經常使用場景:
List 的數據類型在低層實現有如下幾種:
網絡上有些文章說 LinkedList
在 Redis 4.0
以後的版本沒有再被使用,實際上我發現 Redis 3.2.12
版本中也沒有再使用該結構(不直接作爲數據存儲結構),包括 ZipList
在 3.2.12
版本中都沒有再被直接用來存儲數據了。
咱們作個實驗來驗證下,咱們設置一個List中有 1000 個元素,每一個元素value長度都超過 64 個字符。
127.0.0.1:6379> LLEN test (integer) 1000 127.0.0.1:6379> OBJECT encoding test "quicklist" 127.0.0.1:6379> LINDEX test 0 "qweqweqwkejkksdjfslfldsjfkldjslkfqweqweqwkejkksdjfslfldsjfkldjslq" // 65個字符
不管咱們是改變列表元素的個數以及元素值的長度,其結構都是 QuickList
。還不信的話,咱們來看看代碼:
void pushGenericCommand(client *c, int where) { int j, waiting = 0, pushed = 0; robj *lobj = lookupKeyWrite(c->db,c->argv[1]); ... ... for (j = 2; j < c->argc; j++) { c->argv[j] = tryObjectEncoding(c->argv[j]); if (!lobj) { // 建立 quick list lobj = createQuicklistObject(); quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size, server.list_compress_depth); dbAdd(c->db,c->argv[1],lobj); } listTypePush(lobj,c->argv[j],where); pushed++; } ... ... }
初始話時,調用 createQuicklistObject()
設置其低層數據結構是:quick list
。後續流程中沒有地方再對該結構進行轉化。
Set 類型的重要特性之一是能夠去重、無序。它集合的性質在社交上能夠有普遍的使用。
Set低層實現採用了兩種數據結構:
該命令的代碼以下,其中重要的兩個關於決定類型的調用是:setTypeCreate()
和 setTypeAdd()
。
void saddCommand(client *c) { robj *set; ... ... if (set == NULL) { // 初始化 set = setTypeCreate(c->argv[2]); } else { ... ... } for (j = 2; j < c->argc; j++) { // 內部會檢查元素個數是否擴充到須要改變低層結構 if (setTypeAdd(set,c->argv[j])) added++; } ... ... }
來看下 Set 結構對象的初始建立代碼:
robj *setTypeCreate(robj *value) { if (isObjectRepresentableAsLongLong(value,NULL) == C_OK) return createIntsetObject(); // 使用IntSet return createSetObject(); // 使用HashTable }
isObjectRepresentableAsLongLong()
內部判斷其整數範圍,若是是整數且沒有超過最大整數就會使用 IntSet
來保存。不然使用 HashTable
。接着會檢查元素的個數。
int setTypeAdd(robj *subject, robj *value) { long long llval; if (subject->encoding == OBJ_ENCODING_HT) { ... ... } else if (subject->encoding == OBJ_ENCODING_INTSET) { if (isObjectRepresentableAsLongLong(value,&llval) == C_OK) { uint8_t success = 0; subject->ptr = intsetAdd(subject->ptr,llval,&success); if (success) { /* Convert to regular set when the intset contains * too many entries. */ if (intsetLen(subject->ptr) > server.set_max_intset_entries) setTypeConvert(subject,OBJ_ENCODING_HT); return 1; } } else { /* Failed to get integer from object, convert to regular set. */ setTypeConvert(subject,OBJ_ENCODING_HT); ... ... return 1; } } ... ... return 0; }
看看例子,這裏以最大整數臨界值爲例:
127.0.0.1:6379> SADD test 9223372036854775807 (integer) 1 127.0.0.1:6379> OBJECT encoding test "intset" 127.0.0.1:6379> SADD test 9223372036854775808 (integer) 1 127.0.0.1:6379> OBJECT encoding test "hashtable"
關於集合個數的測試,請自行完成觀察。
如今的應用,都有一些排行榜之類的功能,好比投資網站顯示投資金額排行,購物網站顯示消費排行等。SortSet很是適合作這件事。經常使用來解決如下問題:
雖然有序集合也是集合,可是低層的數據結構卻與Set不同,它也有兩種數據結構,分別是:
這個轉變成過程以下:
void zaddGenericCommand(client *c, int flags) { if (zobj == NULL) { if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */ if (server.zset_max_ziplist_entries == 0 || server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr)) { zobj = createZsetObject();// skip list } else { zobj = createZsetZiplistObject();// zip list } dbAdd(c->db,key,zobj); } else { ... ... } ... ... if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries) zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);// 根據個數轉化編碼 if (sdslen(ele->ptr) > server.zset_max_ziplist_value) zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);// 根據長度轉化編碼 } }
這裏以member長度超過64舉例:
127.0.0.1:6379> ZADD test 77 qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwer // member長度是 64 (integer) 1 127.0.0.1:6379> OBJECT encoding test "ziplist" 127.0.0.1:6379> ZADD test 77 qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwerq // member長度是65 (integer) 1 127.0.0.1:6379> OBJECT encoding test "skiplist"
當咱們member 超過64位長度時,低層的數據結構由 ZipList
轉變成了 SkipList
。剩下的元素個數的測試,動動手試試看。
對於全局命令,無論對應的key是什麼類型的數據,都是能夠進行操做的。其中須要注意 KEYS 這個命令,不能用於線上,由於Redis單線程機制,若是內存中數據太多,會操做嚴重的阻塞,致使整個Redis服務都沒法響應。
第一篇講了爲何要用Redis,本文又講了絕大部分命令吧,以及Redis源碼中對它們的一些實現,後續開始關注具體實踐中的一些操做。但願對你們有幫助,期待任何形式的批評與鼓勵。