"Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker." —— Redis是一個開放源代碼(BSD許可)的內存中數據結構存儲,用做數據庫,緩存和消息代理。 (摘自官網)html
Redis 是一個開源,高級的鍵值存儲和一個適用的解決方案,用於構建高性能,可擴展的 Web 應用程序。Redis 也被做者戲稱爲 數據結構服務器 ,這意味着使用者能夠經過一些命令,基於帶有 TCP 套接字的簡單 服務器-客戶端 協議來訪問一組 可變數據結構 。(在 Redis 中都採用鍵值對的方式,只不過對應的數據結構不同罷了)java
如下是 Redis 的一些優勢:python
這一步比較簡單,你能夠在網上搜到許多滿意的教程,這裏就再也不贅述。git
給一個菜鳥教程的安裝教程用做參考:https://www.runoob.com/redis/redis-install.html程序員
當你安裝完成以後,你能夠先執行 redis-server
讓 Redis 啓動起來,而後運行命令 redis-benchmark -n 100000 -q
來檢測本地同時執行 10 萬個請求時的性能:github
固然不一樣電腦之間因爲各方面的緣由會存在性能差距,這個測試您能夠權當是一種 「樂趣」 就好。golang
Redis 有 5 種基礎數據結構,它們分別是:string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)。這 5 種是 Redis 相關知識中最基礎、最重要的部分,下面咱們結合源碼以及一些實踐來給你們分別講解一下。web
Redis 中的字符串是一種 動態字符串,這意味着使用者能夠修改,它的底層實現有點相似於 Java 中的 ArrayList,有一個字符數組,從源碼的 sds.h/sdshdr 文件 中能夠看到 Redis 底層對於字符串的定義 SDS,即 Simple Dynamic String 結構:redis
/* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
你會發現一樣一組結構 Redis 使用泛型定義了好屢次,爲何不直接使用 int 類型呢?數據庫
由於當字符串比較短的時候,len 和 alloc 可使用 byte 和 short 來表示,Redis 爲了對內存作極致的優化,不一樣長度的字符串使用不一樣的結構體來表示。
爲何不考慮直接使用 C 語言的字符串呢?由於 C 語言這種簡單的字符串表示方式 不符合 Redis 對字符串在安全性、效率以及功能方面的要求。咱們知道,C 語言使用了一個長度爲 N+1 的字符數組來表示長度爲 N 的字符串,而且字符數組最後一個元素老是 '\0'
。(下圖就展現了 C 語言中值爲 "Redis" 的一個字符數組)
這樣簡單的數據結構可能會形成如下一些問題:
'\0'
可能會被斷定爲提早結束的字符串而識別不了;咱們以追加字符串的操做舉例,Redis 源碼以下:
/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the * end of the specified sds string 's'. * * After the call, the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdscatlen(sds s, const void *t, size_t len) { // 獲取原字符串的長度 size_t curlen = sdslen(s); // 按需調整空間,若是容量不夠容納追加的內容,就會從新分配字節數組並複製原字符串的內容到新數組中 s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; // 內存不足 memcpy(s+curlen, t, len); // 追加目標字符串到字節數組中 sdssetlen(s, curlen+len); // 設置追加後的長度 s[curlen+len] = '\0'; // 讓字符串以 \0 結尾,便於調試打印 return s; }
安裝好 Redis,咱們可使用 redis-cli
來對 Redis 進行命令行的操做,固然 Redis 官方也提供了在線的調試器,你也能夠在裏面敲入命令進行操做:http://try.redis.io/#run
> SET key value OK > GET key "value"
正如你看到的,咱們一般使用 SET
和 GET
來設置和獲取字符串值。
值能夠是任何種類的字符串(包括二進制數據),例如你能夠在一個鍵下保存一張 .jpeg
圖片,只須要注意不要超過 512 MB 的最大限度就行了。
當 key 存在時,SET
命令會覆蓋掉你上一次設置的值:
> SET key newValue OK > GET key "newValue"
另外你還可使用 EXISTS
和 DEL
關鍵字來查詢是否存在和刪除鍵值對:
> EXISTS key (integer) 1 > DEL key (integer) 1 > GET key (nil)
> SET key1 value1 OK > SET key2 value2 OK > MGET key1 key2 key3 # 返回一個列表 1) "value1" 2) "value2" 3) (nil) > MSET key1 value1 key2 value2 > MGET key1 key2 1) "value1" 2) "value2"
能夠對 key 設置過時時間,到時間會被自動刪除,這個功能經常使用來控制緩存的失效時間。(過時能夠是任意數據結構)
> SET key value1 > GET key "value1" > EXPIRE name 5 # 5s 後過時 ... # 等待 5s > GET key (nil)
等價於 SET
+ EXPIRE
的 SETNX
命令:
> SETNX key value1 ... # 等待 5s 後獲取 > GET key (nil) > SETNX key value1 # 若是 key 不存在則 SET 成功 (integer) 1 > SETNX key value1 # 若是 key 存在則 SET 失敗 (integer) 0 > GET key "value" # 沒有改變
若是 value 是一個整數,還能夠對它使用 INCR
命令進行 原子性 的自增操做,這意味着及時多個客戶端對同一個 key 進行操做,也決不會致使競爭的狀況:
> SET counter 100 > INCR count (interger) 101 > INCRBY counter 50 (integer) 151
對字符串,還有一個 GETSET
比較讓人以爲有意思,它的功能跟它名字同樣:爲 key 設置一個值並返回原值:
> SET key value > GETSET key value1 "value"
這能夠對於某一些須要隔一段時間就統計的 key 很方便的設置和查看,例如:系統每當由用戶進入的時候你就是用 INCR
命令操做一個 key,當須要統計時候你就把這個 key 使用 GETSET
命令從新賦值爲 0,這樣就達到了統計的目的。
Redis 的列表至關於 Java 語言中的 LinkedList,注意它是鏈表而不是數組。這意味着 list 的插入和刪除操做很是快,時間複雜度爲 O(1),可是索引定位很慢,時間複雜度爲 O(n)。
咱們能夠從源碼的 adlist.h/listNode
來看到對其的定義:
/* Node, List, and Iterator are the only data structures used currently. */ typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct listIter { listNode *next; int direction; } listIter; typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len; } list;
能夠看到,多個 listNode 能夠經過 prev
和 next
指針組成雙向鏈表:
雖然僅僅使用多個 listNode 結構就能夠組成鏈表,可是使用 adlist.h/list
結構來持有鏈表的話,操做起來會更加方便:
LPUSH
和 RPUSH
分別能夠向 list 的左邊(頭部)和右邊(尾部)添加一個新元素;LRANGE
命令能夠從 list 中取出必定範圍的元素;LINDEX
命令能夠從 list 中取出指定下表的元素,至關於 Java 鏈表操做中的 get(int index)
操做;示範:
> rpush mylist A (integer) 1 > rpush mylist B (integer) 2 > lpush mylist first (integer) 3 > lrange mylist 0 -1 # -1 表示倒數第一個元素, 這裏表示從第一個元素到最後一個元素,即全部 1) "first" 2) "A" 3) "B"
隊列是先進先出的數據結構,經常使用於消息排隊和異步邏輯處理,它會確保元素的訪問順序:
> RPUSH books python java golang (integer) 3 > LPOP books "python" > LPOP books "java" > LPOP books "golang" > LPOP books (nil)
棧是先進後出的數據結構,跟隊列正好相反:
> RPUSH books python java golang > RPOP books "golang" > RPOP books "java" > RPOP books "python" > RPOP books (nil)
Redis 中的字典至關於 Java 中的 HashMap,內部實現也差很少相似,都是經過 "數組 + 鏈表" 的鏈地址法來解決部分 哈希衝突,同時這樣的結構也吸取了兩種不一樣數據結構的優勢。源碼定義如 dict.h/dictht
定義:
typedef struct dictht { // 哈希表數組 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩碼,用於計算索引值,老是等於 size - 1 unsigned long sizemask; // 該哈希表已有節點的數量 unsigned long used; } dictht; typedef struct dict { dictType *type; void *privdata; // 內部有兩個 dictht 結構 dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict;
table
屬性是一個數組,數組中的每一個元素都是一個指向 dict.h/dictEntry
結構的指針,而每一個 dictEntry
結構保存着一個鍵值對:
typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; double d; } v; // 指向下個哈希表節點,造成鏈表 struct dictEntry *next; } dictEntry;
能夠從上面的源碼中看到,實際上字典結構的內部包含兩個 hashtable,一般狀況下只有一個 hashtable 是有值的,可是在字典擴容縮容時,須要分配新的 hashtable,而後進行 漸進式搬遷 (下面說緣由)。
大字典的擴容是比較耗時間的,須要從新申請新的數組,而後將舊字典全部鏈表中的元素從新掛接到新的數組下面,這是一個 O(n) 級別的操做,做爲單線程的 Redis 很難承受這樣耗時的過程,因此 Redis 使用 漸進式 rehash 小步搬遷:
漸進式 rehash 會在 rehash 的同時,保留新舊兩個 hash 結構,如上圖所示,查詢時會同時查詢兩個 hash 結構,而後在後續的定時任務以及 hash 操做指令中,按部就班的把舊字典的內容遷移到新字典中。當搬遷完成了,就會使用新的 hash 結構取而代之。
正常狀況下,當 hash 表中 元素的個數等於第一維數組的長度時,就會開始擴容,擴容的新數組是 原數組大小的 2 倍。不過若是 Redis 正在作 bgsave(持久化命令)
,爲了減小內存也得過多分離,Redis 儘可能不去擴容,可是若是 hash 表很是滿了,達到了第一維數組長度的 5 倍了,這個時候就會 強制擴容。
當 hash 表由於元素逐漸被刪除變得愈來愈稀疏時,Redis 會對 hash 表進行縮容來減小 hash 表的第一維數組空間佔用。所用的條件是 元素個數低於數組長度的 10%,縮容不會考慮 Redis 是否在作 bgsave
。
hash 也有缺點,hash 結構的存儲消耗要高於單個字符串,因此到底該使用 hash 仍是字符串,須要根據實際狀況再三權衡:
> HSET books java "think in java" # 命令行的字符串若是包含空格則須要使用引號包裹 (integer) 1 > HSET books python "python cookbook" (integer) 1 > HGETALL books # key 和 value 間隔出現 1) "java" 2) "think in java" 3) "python" 4) "python cookbook" > HGET books java "think in java" > HSET books java "head first java" (integer) 0 # 由於是更新操做,因此返回 0 > HMSET books java "effetive java" python "learning python" # 批量操做 OK
Redis 的集合至關於 Java 語言中的 HashSet,它內部的鍵值對是無序、惟一的。它的內部實現至關於一個特殊的字典,字典中全部的 value 都是一個值 NULL。
因爲該結構比較簡單,咱們直接來看看是如何使用的:
> SADD books java (integer) 1 > SADD books java # 重複 (integer) 0 > SADD books python golang (integer) 2 > SMEMBERS books # 注意順序,set 是無序的 1) "java" 2) "python" 3) "golang" > SISMEMBER books java # 查詢某個 value 是否存在,至關於 contains (integer) 1 > SCARD books # 獲取長度 (integer) 3 > SPOP books # 彈出一個 "java"
這可能使 Redis 最具特點的一個數據結構了,它相似於 Java 中 SortedSet 和 HashMap 的結合體,一方面它是一個 set,保證了內部 value 的惟一性,另外一方面它能夠爲每一個 value 賦予一個 score 值,用來表明排序的權重。
它的內部實現用的是一種叫作 「跳躍表」 的數據結構,因爲比較複雜,因此在這裏簡單提一下原理就行了:
想象你是一家創業公司的老闆,剛開始只有幾我的,你們都分庭抗禮。後來隨着公司的發展,人數愈來愈多,團隊溝通成本逐漸增長,漸漸地引入了組長制,對團隊進行劃分,因而有一些人又是員工又有組長的身份。
再後來,公司規模進一步擴大,公司須要再進入一個層級:部門。因而每一個部門又會從組長中推舉一位選出部長。
跳躍表就相似於這樣的機制,最下面一層全部的元素都會串起來,都是員工,而後每隔幾個元素就會挑選出一個表明,再把這幾個表明使用另一級指針串起來。而後再在這些表明裏面挑出二級表明,再串起來。最終造成了一個金字塔的結構。
想一下你目前所在的地理位置:亞洲 > 中國 > 某省 > 某市 > ....,就是這樣一個結構!
> ZADD books 9.0 "think in java" > ZADD books 8.9 "java concurrency" > ZADD books 8.6 "java cookbook" > ZRANGE books 0 -1 # 按 score 排序列出,參數區間爲排名範圍 1) "java cookbook" 2) "java concurrency" 3) "think in java" > ZREVRANGE books 0 -1 # 按 score 逆序列出,參數區間爲排名範圍 1) "think in java" 2) "java concurrency" 3) "java cookbook" > ZCARD books # 至關於 count() (integer) 3 > ZSCORE books "java concurrency" # 獲取指定 value 的 score "8.9000000000000004" # 內部 score 使用 double 類型進行存儲,因此存在小數點精度問題 > ZRANK books "java concurrency" # 排名 (integer) 1 > ZRANGEBYSCORE books 0 8.91 # 根據分值區間遍歷 zset 1) "java cookbook" 2) "java concurrency" > ZRANGEBYSCORE books -inf 8.91 withscores # 根據分值區間 (-∞, 8.91] 遍歷 zset,同時返回分值。inf 表明 infinite,無窮大的意思。 1) "java cookbook" 2) "8.5999999999999996" 3) "java concurrency" 4) "8.9000000000000004" > ZREM books "java concurrency" # 刪除 value (integer) 1 > ZRANGE books 0 -1 1) "java cookbook" 2) "think in java"
- 本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
- 我的公衆號 :wmyskxz,堅持原創輸出,下方掃碼關注,2020,與您共同成長!
很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!
創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!