<b>2.1 預備</b>
在正式介紹5種資料結構之前,了解一下redis的一些全局指令、資料結構和内部編碼、單線程指令處理機制是十分有必要的,它們能為後面内容的學習打下一個好的基礎,主要展現在兩個方面:第一、redis的指令有上百個,如果純靠死記硬背比較困難,但是如果了解redis的一些機制,會發現這些指令有很強的通用性。第二、redis不是萬金油,有些資料結構和指令必須在特定場景下使用,一旦使用不當可能對redis本身或者應用本身造成緻命傷害。
<b>2.1.1 全局指令</b>
redis有5種資料結構,它們是鍵值對中的值,對于鍵來說有一些通用的指令。
1.?檢視所有鍵
keys *
下面插入了3對字元串類型的鍵值對:
127.0.0.1:6379> set hello world
ok
127.0.0.1:6379> set java jedis
127.0.0.1:6379> set python redis-py
keys *指令會将所有的鍵輸出:
127.0.0.1:6379> keys *
1) "python"
2) "java"
3) "hello"
2.?鍵總數
dbsize
下面插入一個清單類型的鍵值對(值是多個元素組成):
127.0.0.1:6379> rpush mylist a b c d e f
g
(integer) 7
dbsize指令會傳回目前資料庫中鍵的總數。例如目前資料庫有4個鍵,分别是hello、java、python、mylist,是以dbsize的結果是4:
127.0.0.1:6379> dbsize
(integer) 4
dbsize指令在計算鍵總數時不會周遊所有鍵,而是直接擷取redis内置的鍵總數變量,是以dbsize指令的時間複雜度是o(1)。而keys指令會周遊所有鍵,是以它的時間複雜度是o(n),當redis儲存了大量鍵時,線上環境禁止使用。
3.?檢查鍵是否存在
exists key
如果鍵存在則傳回1,不存在則傳回0:
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exist_key
(integer) 0
4.?删除鍵
del key [key ...]
del是一個通用指令,無論值是什麼資料結構類型,del指令都可以将其删除,例如下面将字元串類型的鍵java和清單類型的鍵mylist分别删除:
127.0.0.1:6379> del java
127.0.0.1:6379> del mylist
127.0.0.1:6379> exists mylist
傳回結果為成功删除鍵的個數,假設删除一個不存在的鍵,就會傳回0:
127.0.0.1:6379> del not_exist_key
同時del指令可以支援删除多個鍵:
127.0.0.1:6379> set a 1
127.0.0.1:6379> set b 2
127.0.0.1:6379> set c 3
127.0.0.1:6379> del a b c
(integer) 3
5.?鍵過期
expire key seconds
redis支援對鍵添加過期時間,當超過過期時間後,會自動删除鍵,例如為鍵hello設定了10秒過期時間:
127.0.0.1:6379> expire hello 10
ttl指令會傳回鍵的剩餘過期時間,它有3種傳回值:
大于等于0的整數:鍵剩餘的過期時間。
-1:鍵沒設定過期時間。
-2:鍵不存在
可以通過ttl指令觀察鍵hello的剩餘過期時間:
#還剩7秒
127.0.0.1:6379> ttl hello
...
#還剩1秒
#傳回結果為-2,說明鍵hello已經被删除
(integer) -2
127.0.0.1:6379> get hello
(nil)
有關鍵過期更為詳細的使用以及原理會在2.7節介紹。
6.?鍵的資料結構類型
type key
例如鍵hello是字元串類型,傳回結果為string。鍵mylist是清單類型,傳回結果為list:
127.0.0.1:6379> set a b
127.0.0.1:6379> type a
string
127.0.0.1:6379> type mylist
list
如果鍵不存在,則傳回none:
127.0.0.1:6379> type not_exsit_key
none
本小節隻是抛磚引玉,給出幾個通用的指令,為5種資料結構的使用做一個熱身,2.7節将對鍵管理做一個更為詳細的介紹。
<b>2.1.2 資料結構和内部編碼</b>
type指令實際傳回的就是目前鍵的資料結構類型,它們分别是:string(字元串)、hash(哈希)、list(清單)、set(集合)、zset(有序集合),但這些隻是redis對外的資料結構,如圖2-1所示。
實際上每種資料結構都有自己底層的内部編碼實作,而且是多種實作,這樣redis會在合适的場景選擇合适的内部編碼,如圖2-2所示。
可以看到每種資料結構都有兩種以上的内部編碼實作,例如list資料結構包含了linkedlist和ziplist兩種内部編碼。同時有些内部編碼,例如ziplist,可以作為多種外部資料結構的内部實作,可以通過object encoding指令查詢内部編碼:
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"ziplist"
圖2-1 redis的5種資料結構 圖2-2 redis資料結構和内部編碼
可以看到鍵hello對應值的内部編碼是embstr,鍵mylist對應值的内部編碼是ziplist。
redis這樣設計有兩個好處:第一,可以改進内部編碼,而對外的資料結構和指令沒有影響,這樣一旦開發出更優秀的内部編碼,無需改動外部資料結構和指令,例如redis 3.2提供了quicklist,結合了ziplist和linkedlist兩者的優勢,為清單類型提供了一種更為優秀的内部編碼實作,而對外部使用者來說基本感覺不到。第二,多種内部編碼實作可以在不同場景下發揮各自的優勢,例如ziplist比較節省記憶體,但是在清單元素比較多的情況下,性能會有所下降,這時候redis會根據配置選項将清單類型的内部實作轉換為linkedlist。
<b>2.1.3 單線程架構</b>
redis使用了單線程架構和i/o多路複用模型來實作高性能的記憶體資料庫服務,本節首先通過多個用戶端指令調用的例子說明redis單線程指令處理機制,接着分析redis單線程模型為什麼性能如此之高,最終給出為什麼了解單線程模型是使用和運維redis的關鍵。
1.?引出單線程模型
現在開啟了三個redis-cli用戶端同時執行指令。
用戶端1設定一個字元串鍵值對:
用戶端2對counter做自增操作:
127.0.0.1:6379> incr counter
用戶端3對counter做自增操作:
redis用戶端與服務端的模型可以簡化成圖2-3,每次用戶端調用都經曆了發送指令、執行指令、傳回結果三個過程。
其中第2步是重點要讨論的,因為redis是單線程來處理指令的,是以一條指令從用戶端達到服務端不會立刻被執行,所有指令都會進入一個隊列中,然後逐個被執行。是以上面3個用戶端指令的執行順序是不确定的(如圖2-4所示),但是可以确定不會有兩條指令被同時執行(如圖2-5所示),是以兩條incr指令無論怎麼執行最終結果都是2,不會産生并發問題,這就是redis單線程的基本模型。但是像發送指令、傳回結果、指令排隊肯定不像描述的這麼簡單,redis使用了i/o多路複用技術來解決i/o
的問題,下一節将進行介紹。
2.?為什麼單線程還能這麼快
通常來講,單線程處理能力要比多線程差,例如有10?000斤貨物,每輛車的運載能力是每次200斤,那麼要50次才能完成,但是如果有50輛車,隻要安排合理,隻需要一次就可以完成任務。那麼為什麼redis使用單線程模型會達到每秒萬級别的處理能力呢?可以将其歸結為三點:
第一,純記憶體通路,redis将所有資料放在記憶體中,記憶體的響應時長大約為100納秒,這是redis達到每秒萬級别通路的重要基礎。
第二,非阻塞i/o,redis使用epoll作為i/o多路複用技術的實作,再加上redis自身的事件處理模型将epoll中的連接配接、讀寫、關閉都轉換為事件,不在網絡i/o上浪費過多的時間,如圖2-6所示。
第三,單線程避免了線程切換和競态産生的消耗。
既然采用單線程就能達到如此高的性能,那麼也不失為一種不錯的選擇,因為單線程能帶來幾個好處:第一,單線程可以簡化資料結構和算法的實作。如果對進階程式設計語言熟悉的讀者應該了解并發資料結構實作不但困難而且開發測試比較麻煩。第二,單線程避免了線程切換和競态産生的消耗,對于服務端開發來說,鎖和線程切換通常是性能殺手。
但是單線程會有一個問題:對于每個指令的執行時間是有要求的。如果某個指令執行過長,會造成其他指令的阻塞,對于redis這種高性能的服務來說是緻命的,是以redis是面向快速執行場景的資料庫。
單線程機制很容易被初學者忽視,但筆者認為redis單線程機制是開發和運維人員使用和了解redis的核心之一,随着後面的學習,相信讀者會逐漸了解。