萬字長文入門 Redis 命令、事務、鎖、訂閱、效能測試

痴者工良發表於2021-12-20

作者:痴者工良

Redis 基本資料型別

Redis 中,常用的資料型別有以下幾種:

  • String:字串型別,二進位制安全字串
  • Hash:雜湊表;
  • List 列表:連結串列結構,按照插入順序排序的字串元素的集合;
  • Set:集合,元素具有唯一性,未排序的字串元素集合;
  • Sorted Set:有序集合;
  • Bit arrays:二進位制資料;

Redis 的 Key

Redis 的鍵是二進位制安全的,意味著無論 Key 中的內容是什麼,在 Redis 都可以正常使用,例如 Key 中可以包含空格、\r\n$ 等特殊字元,因為它們都會被轉為二進位制儲存,它們不再是具有意義的字串,而是一堆 01 組成的內容。你可以使用普通字串做 Key ,也可以使用一張圖片做 Key,只要 Key 小於 512MB 即可

Redis Key 命名

Redis Key 的命名一般都是小寫,命名主要以便於閱讀為主,同時考慮縮短 Key,減少記憶體佔用,例如 user:1000:followers 便於閱讀,而 u1000flw 很短可以減少記憶體佔用,但可讀性不高。

Key 可以如果要表達有層次結構,則可以使用 : 組合 ,如要表達 租戶(id1)=>技術部(id5)=>後端(id1)=>工號006,每層物件在資料庫中都有一個表儲存,且每個物件都有一個 Id,則可以使用 tenant:01:department:05:group:01:user:006 做 Key,在某些工具下,Key 會被以樹的形式顯示,便於查詢,其顯示如下圖所示:

如果 Key 某一個部分是多詞欄位,則可以使用 .- 連線,如 comment:1234:reply.tocomment:1234:reply-to,表示 Id 為 1234 的評論被回覆的資訊列表。

由於 Redis 的 Key 是很寬鬆的,因此命名規則不必限定,可以根據團隊內部討論覺得使用何種分隔符分割層次、使用長命名還是縮短命名習慣等。

一般更加建議是使用 {物件名稱}:{物件標識/id}:{物件屬性} 表示,如 使用者1 的訊息列表表示為 user:1:messages,如果是租戶隔離,還可以表示使用者相關資訊為 tenant:1:user:1:messagestenant:1:user:1.messages

設定 Key 過期時間

Redis 的過期時間設定有四種形式:

  • EXPIRE 秒——設定指定的過期時間(秒),表示的是時間間隔。
  • PEXPIRE 毫秒——設定指定的過期時間,以毫秒為單位,表示的是時間間隔。
  • EXPIREAT 時間戳-秒——設定指定的 Key 過期的 Unix 時間,單位為秒,表示的是時間/時刻。
  • PEXPIREAT 時間戳-毫秒——設定指定的 Key 到期的 Unix 時間,以毫秒為單位,表示的是時間/時刻。

如設定一個 Key 在 5s 後過期:

127.0.0.1:6379> expire value 5
(integer) 1

設定 Key 在 2021年11月26日22時過期:

# 2021-11-26 22:00:00
127.0.0.1:6379> expireat value 1637935200
(integer) 1

有些型別本身或型別的元素的命令引數可以設定過期時間,如 string 型別,可以不使用 expire 等相關命令。

使用 ttl 命令,可以檢視一個 Key 的過期時間,返回剩餘存活秒數。

# 2021-11-26 22:00:00
127.0.0.1:6379> expireat value 1637935200
(integer) 1
127.0.0.1:6379> ttl value
(integer) 4760

Redis 7.0 後 expire 命令有以下引數可用:

  • NX ——只有當金鑰沒有過期時才設定過期
  • XX -- 僅當鍵具有現有的過期時才設定過期
  • GT 僅當新的有效期大於當前有效期時才設定有效期
  • LT ——只有當新的有效期小於當前有效期時才設定有效期

筆者在編寫這篇文章時,使用的 redis:latest 映象,其版本是 6.2.6,因此暫未使用這些引數,你可以使用 info 命令檢視 Redis 資訊:

127.0.0.1:6379> info
# Server
redis_version:6.2.6
... ...

Redis 使用 expires 字典儲存了所有 Key 的過期時間。

判斷鍵是否存在

exist 命令可以判斷一個 Key 是否存在,如果存在則返回 1,否則返回 0 。

redis> exist key1
(integer) 1
redis> exist nosuchkey
(integer) 0

搜尋 Key

keys 命令可以搜尋符合條件的 Key,如 keys * 則返回全部 key。

# 搜尋以 t 開頭的所有 key
keys t*
# 搜尋包含 test 的 key
keys *test*

使用 dbsize 命令可以知道 Key 的數量:

127.0.0.1:6379> dbsize
(integer) 5

scan 則可以指定搜尋多少條符合條件的 key:

# 返回一條以 t 開頭的 key
scan 0 match t* count 1

可參考: https://redis.io/commands/scan

can 命令格式入下

scan cursor [MATCH pattern] [COUNT count] [TYPE type]

cursor 是一個遊標值,scan 每次結果的是在上一次迭代,表示開始位置,如果不注意,可能會導致查詢結果與需要的不一樣。如 Redis 中有三個 user:{id} Key,我想搜尋符合條件的這三個值:

127.0.0.1:6379> scan 0 match  "user:*" count 5
1) "10"
2) 1) "user:3"
   2) "user:1"

搜尋結果一直很奇怪。

這是因為是當前遊標是 1) "10" 在 10;而且scan 只會返回部分數量的 Key,不會返回所有數量。所以如果要使用 scan 命令,我們要注意以下步驟。

筆者的完整 Key 如下:

 1) "test"
 2) "user:1"
 3) "key"
 4) "user:3"
 5) "Sicily"
 6) "test1"
 7) "zset"
 8) "deck"
 9) "h"
10) "year"
11) "user:2"

注意 user:{id} 的位置。

重置遊標:

127.0.0.1:6379> scan 0
1) "7"
2)  1) "Sicily"
    2) "user:3"
    3) "deck"
    4) "test"
    5) "user:1"
    6) "year"
    7) "h"
    8) "key"
    9) "test1"
   10) "zset"

注意,此時遊標位置在 7,這個是 Redis 分配的,具有不確定性

搜尋:

127.0.0.1:6379> scan 0 match  "user:*" count 100
1) "0"
2) 1) "user:3"
   2) "user:1"
   3) "user:2"

首先,當我們使用 scan 0 時,遊標重新在 0 開始,因為沒有設定值,因此 Redis 分配到了 7。另外遊標的意思並不是下次從 7 開始搜尋,而是指當前遊標識別了 0-7 中的 Key,你下次搜尋的結果將會在 0-7 中搜尋!因此筆者給其設定了 count 100,這樣遊標會一直往下走,直至找到符合數量的 Key 或 Key 已經檢索完畢。

要注意,SCAN 命令並不保證每次執行都返回某個給定數量的元素

如下所示,重置遊標後,它自動檢索到 7,預設最大 10,此時我們的關鍵字在 0-7中搜尋,不加 count 預設只會找到兩個 Key,那麼我將 count 數量改成 3,那他不就可以找到三個元素了?這裡我們直接設定為 5 試試:

127.0.0.1:6379> scan 0
1) "7"
2)  1) "Sicily"
    2) "user:3"
    3) "deck"
    4) "test"
    5) "user:1"
    6) "year"
    7) "h"
    8) "key"
    9) "test1"
   10) "zset"
127.0.0.1:6379> scan 0 match  "user:*" count 5
1) "10"
2) 1) "user:3"
   2) "user:1"

結果事與願違,遊標只走到 10 ,並且結果只有兩個,而不是 3 個。

如果你把 count 設定大一點,可能便可以搜尋到需要的 3 個 Key 了:

127.0.0.1:6379> scan 0 match  "user:*" count 11
1) "0"
2) 1) "user:3"
   2) "user:1"
   3) "user:2"

注意,scan cursorscan cursor match ... 命令不一樣,前者是重置遊標檢索位置,將範圍內的 Key 當搜尋結果蒐集起來;而 scan cursor match ... 指從哪個位置開始搜尋。

在 Redis 的很多型別中,如列表、集合,都支援搜尋,它們的命令格式中有個 pattern 欄位,其支援 glob 風格的萬用字元區配格式,也使用這種風格區配 Key。其規則或說明如下:

符號 說明
? 表示一個任意字元,如 tes?test 符合結果;
* 區配任意數量的字元,如 * 表示所有;t* 表示以 t 開頭;
[] 區配方括號間的任一個字元,可以使用 - 表示一個範圍,與正規表示式類似;如 t[a-d]* ,以 t 開頭,第二個字元是 a,b,cd 中的一個;
\x x 表示一個字元;用於將前面三個符號轉義,使其失去特殊意義,如 \?\*

另外 Redis 的命令關鍵字不區分大小寫。

判斷鍵型別

type 命令可以獲取一個 Key 的 Value 的型別:

127.0.0.1:6379> set value 123455
OK
127.0.0.1:6379> type value
string
127.0.0.1:6379> rpush value2 abc
(integer) 1
127.0.0.1:6379> type value2
list

刪除鍵

del 命令可以刪除一個 Key:

127.0.0.1:6379> del value2
(integer) 1
127.0.0.1:6379> del value2 value
(integer) 1

返回刪除的 Key 數量;如果 Key 不存在,del 命令不會報錯,只會返回受影響的數量;

前面提到, Redis 支援模糊搜尋 Key,可以很容易查詢符合條件的 Key,但是 Redis 不支援模糊刪除 Key。

RESP 協議

RESP 協議用於編寫 Redis 客戶端,它定義了 Redis 請求和響應的格式內容,當我們使用 redis-cli 工具連線 Redis 並執行命令時,返回的資料格式跟 RESP 有關係,這裡簡單說一下,Redis 響應的格式主要有:

  • 對於簡單字串,回覆的第一個位元組是“+”
  • 對於錯誤,回覆的第一個位元組是“-”
  • 對於整數,回覆的第一個位元組是“:”
  • 對於批量字串,回覆的第一個位元組是“$”
  • 對於陣列,回覆的第一個位元組是“ *

在 redis-cli 或別的工具中,第一個符號可能不會顯示,例如 "+ok",在工具中只給使用者顯示 "ok" ;響應包含 ok 則說明命令執行成功;nil 表示 Null Bulk String,也 nil 可以表達為 ok 的反義,即失敗,但不代表發生錯誤,不同的程式語言客戶端應將 nil 表示為其語言的相關空型別,例如 go 語言返回 nil,C# 返回 null,C 語言返回 NULL 等,在 redis-cli 中顯示為 (nil)。

以上符號只是對響應內容進行初步解析,具體含義要根據傳送的命令以及程式語言特點做二次處理。如 del bar 命令,刪除一個 Key,響應內容:

127.0.0.1:6379> del bar 
(integer) 0

redis-cli 工具中看到的不是原始的訊息內容,如果直接接收 TCP 訊息,其內容應該是 :0,客戶端可以通過前面的 : 符號瞭解到後面的是數字,於是吧 0 擷取處理,轉為數字;但是這個數字結合 del 命令才有意義,這部分則要看程式語言的特點做處理。

這裡不必深入瞭解 RESP 協議,只需要大概瞭解使用 redis-cli 等工具執行 Redis 命令時,響應結果代表什麼意義即可。

字串型別

Redis 的字串型別也是二進位制安全的,二進位制安全並不是指執行緒安全,而是指無論你儲存什麼內容都可以,Redis string 最大可以儲存 512 MB,可以往裡面塞一些小姐姐視訊、圖片、網頁、檔案等都沒問題。

面試題:Redis 相比 memcached 有哪些優勢

  • memcached 只支援簡單的字串型別,而 Redis 支援多種型別;
  • Redis 速度更加快;
  • Redis 的資料可以持久化;

下面介紹一些 string 常用的指令。

使用 setget 對單個 Key 進行寫或讀,使用 msetmget 對多個 Key 批量寫或讀。

> set mykey somevalue
OK
> get mykey
"somevalue"
> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

預設情況下,當 Key 已存在時,setmset 會替換其值;當 Key 不存在時,setmset 會建立新的 Key,而 Redis 提供了 NX、XX 兩個引數,可以改變這種替換或新建立行為。如果一個 Key 存在並具有過期時間等屬性時,如果使用 set 等命令替換 Key 時,過期時間等屬性會自動消除。

NX:當 Key 不存在時才生效。

127.0.0.1:6379> set key1 666 nx
"OK"
127.0.0.1:6379> set key1 666 nx
(nil)

Key 不存在時,set 命令正常;當 Key 不存在時,set 命令報 nil。

如果響應的資訊以 - 開頭,則表示一個錯誤。

XX:當 Key 存在時才生效。

127.0.0.1:6379> set key1 666 xx
OK
127.0.0.1:6379> del key1	# 刪除鍵
(integer) 1
127.0.0.1:6379> set key1 666 xx
(nil)

Key 存在時,set 命令正常;當 Key 不存在時,set 命令報 nil。

完整的 set 命令定義如下:

set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]

可參考 https://redis.io/commands/set

下面介紹一下這些引數:

  • EX 秒——設定指定的過期時間(秒),表示的是時間間隔。
  • PX 毫秒——設定指定的過期時間,以毫秒為單位,表示的是時間間隔。
  • EXAT 時間戳-秒——設定指定的 Key 過期的 Unix 時間,單位為秒,表示的是時間/時刻。
  • PXAT 時間戳-毫秒——設定指定的 Key 到期的 Unix 時間,以毫秒為單位,表示的是時間/時刻。
  • NX ——當 Key 不存在時才設定值。
  • XX ——當 Key 存在時才設定值。
  • KEEPTTL ——保留設定前指定鍵的生存時間,即替換 Key 時,保留 Key 的過期時間設定。
  • GET ——如果 Key 已存在,使用 set 命令會替換 Key,加上 get 可以取得替換之前的值;如果 Key 不存在,則返回 nil。

EX、PX、EXAT、PXAT 都是設定時間的,其中 EX、PX 都是表示時間間隔,即自設定起還有多久此 Key 過期;而 EXAT 、PXAT 都是表示過期時刻,即什麼時候過期,EXAT 是 10 位時間戳,如設定 2021-11-25 22:33:48 此 Key 過期,則時間戳為 1637850828;而 PXAT 是十三位的時間戳。

KEEPTTL 引數可以讓 Key 繼承舊 Key 的過期時間,如果一個 Key 設定了 100 秒後過期,那麼當 set 命令替換 Key 前還有 90 秒過期,當替換後,新的 Key 會在 90 秒後過期。

示例:

127.0.0.1:6379> set key1 666 EX 10  get
"666"
127.0.0.1:6379> set key1 666 KEEPTTL get
"666"

string 型別也可以使用原子操作,相當於 C# 的 Interlocked.Increment、Java 的 AtomicInteger、Go 的 atomic,在 Redis 中稱為 atomic increment(原子增量)。

原子操作主要有 INCR、INCRBY、DECR、DECRBY 四種,前兩種是增量,後兩種是減量。

127.0.0.1:6379> set value 100
OK
127.0.0.1:6379> incr value		# 自加 1
(integer) 101
127.0.0.1:6379> incrby value 5  # 指定加量
(integer) 106

INCR 可以用作統計訪問量、註冊賬號遞增的 ID 等。Redis 的原子操作對所有客戶端生效,避免此客戶端操作時,值被另一個客戶端操作覆蓋。

原子增量是雙精確度型別,你可以使用 incrby value 5.0 甚至 incrby value 5E+4 加值。

string 型別具有以下列出的命令,有部分命令可能已經失效或在將來的版本中去除,本文只列舉部分常用的命令,讀者可參考官網文件說明。

Redis 命令有上百個,即使是常用的 Linux 命令也沒有這麼多,沒必要強硬記住這些命令。

  • APPEND :追加字串;
  • DECR:原子操作,減 1;
  • DECRBY:原子操作,減指定值;
  • GET:獲取字串值;
  • GETDEL:獲取字串值後刪除 Key;
  • GETEX:獲取字串並設定過期時間,單位秒;
  • GETRANGE:獲取字串中的一部分字元;
  • GETSET:設定字串值並返回舊字串值;
  • INCR:原子操作,加 1;
  • INCRBY:原子操作,加指定值;
  • INCRBYFLOAT:原子操作,浮點數加指定值;
  • MGET:獲取多個字串 key;
  • MSET:同時設定多個字串 ;
  • MSETNX:對多個字串進行原子級別的設定值,這些 key 同時改變值;
  • PSETEX:獲取字串並設定過期時間,單位毫秒;
  • SET:設定字串值;
  • SETEX:設定字串並設定過期時間,單位秒;
  • SETNX:字串不存在時才設定值;
  • SETRANGE:覆蓋字串的部分值,從偏移量 offset 設定的位置開始替換為新的字串;
  • STRALGO:STRALGO LCS,不知道是什麼東西;
  • STRLEN:獲取字串的字元數量;

位操作

點陣圖不是實際的資料型別,而是在 String 型別上定義的一組面向位的操作,當然,從邏輯上也可以說 Bit 型別,前面提到過字串是二進位制安全的,它們的最大長度為 512 MB,使用二進位制儲存,因此有,因此它們適合設定為232個不同的位。

Redis 的字串實現叫 簡單動態字串(Simple dynamic string),簡稱 SDS,按照儲存空間的大小拆分成為了 sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64,其中 5、8、16、32、64 表示位數,例如 32 位,最大可以表示為 4GB。但是 Redis 中 Key 字串值預設最大為 512MB,因此 sdshdr64 並沒有實際使用到,sdshdr32 也是 ”殘血“ 狀態。

位操作主要有以下五個命令:

所述SETBIT命令採用作為第一個引數的位元數,和作為第二個引數的值以設定所述位,其為1或0的命令自動放大字串,如果定址位是當前字串長度之外。

1,SETBIT:設定指定位的值;

2,GETBIT:僅返回指定索引處的位值,超出範圍的位不會報錯,會顯示 0;

3,BITOP:在不同的字串之間執行按位運算,提供的操作是 AND、OR、XOR 和 NOT。

4,BITCOUNT: 統計字串的二進位制位中 1 的個數;

5,BITPOS:返回字串中設定為 1 或 0 的第一位的位置。

要注意,Redis 使用 C 語言編寫,使用 char* 型別儲存字串,而在 C 語言中,char 是一個位元組,而其他語言可能是兩個位元組;字串儲存的數字是字串,以 ASCII 表示,因此,每位字元使用一個 char 表示,每個 char 8 位程度;但是中文等字元,不能按照此規則,例如 Unicode 使用4位元組表示,UTF-8 使用3位元組表示,那麼中文的 字,使用 UTF8 表示,其二進位制為 11100101 10111000 10000101

Redis 的字串是二進位制安全的,當我們使用 C# 或 Go 語言編寫時,需將字串轉為二進位制資料,此時由程式語言編寫的客戶端決定了 Redis 中要儲存的二進位制資料,然後通過 TCP 傳送二進位制資料到 Redis 中,讀者可參考 教你寫個簡單的 Redis Client 框架

首先,在字串中,儲存 1 這個字串:

127.0.0.1:6379> set a1 1
OK

在工具中檢視 a1 的 16 進製表示,在 ASCII 中使用 31 表示 "1",二進位制表示為 0011 0001

所以,使用 BITCOUNT 命令時,返回結果是 3:

# 127.0.0.1:6379> bitcount a1
127.0.0.1:6379> bitcount a1 0 -1
(integer) 3

獲取 1,3,7 位的值:

127.0.0.1:6379> getbit a1 0
(integer) 1
127.0.0.1:6379> getbit a1 1
(integer) 1
127.0.0.1:6379> getbit a1 2
(integer) 1
127.0.0.1:6379> getbit a1 7
(integer) 1

BITOP 可以讓多個值之間進行位運算,即 與(&)或(|)異或(^)非(~) 四個基本操作,多個字串值的二進位制位數可以不相等。

BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP NOT destkey srckey

示例:

127.0.0.1:6379> set a1 1
OK
127.0.0.1:6379> set a2 2
OK
127.0.0.1:6379> set a2 帥
OK
127.0.0.1:6379> bitop and a1 a2
(integer) 3
127.0.0.1:6379> get a1
"\xe5\xb8\x85"

# 11100101 10111000 10000101 帥
# AND
#                   00110001 1(ASCII 31)
# 00100001 00000000 00000000 !

# 11100101 10111000 10000101 帥
# XOR
#                   00110001 1(ASCII 31)
# 11010100 10111000 10000101 (無對於中文)

BITFIELD 也是一個很有用的命令,可以指定在某些位置填充字元。

BITFIELD 命令格式如下:

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

在 Redis 中,整型可以使用 i8、i16 等表示,其中 i8 表示 8 位二進位制組成的數字,值在 0-127 之間;而無符號使用 u8、u16 等表示。使用 BITFIELD 命令時,會返回上一次的值。

127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 200
1) (integer) 0
2) (integer) 0
127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 200
1) (integer) 100
2) (integer) -56

# 後面的數字表示位元組偏移量, SET i8 #0 100 表示將第一個位元組設定為值為 i8 表示的 100。由於 i8 範圍在 0-127,因此 200 使用 u8 表示,發生溢位,結果為 -56。但是不代表有問題,因為儲存的時候 i8 和 u8 表示 200 都是 11001000(0xc8),儲存二進位制時不會區分正負,但是當你設定了 i8,則它在返回舊值的時候,按照給定的資料型別轉換,因此 11001000 會顯示 -56,但是正負不影響存入結果

如果值過大,則會發生溢位。如:

...  ...
127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 257
1) (integer) 100
2) (integer) 1

BITFIELD 還有個好玩的地方是可以在某一位上使用原子增量,格式示例 incrby i8 0 1

127.0.0.1:6379> BITFIELD mystring SET i8 #0 1 SET i8 #1 2
1) (integer) 1
2) (integer) 2
127.0.0.1:6379> BITFIELD mystring  incrby i8 0 1
1) (integer) 2

incrby 引數後面可以帶上溢位控制,避免自增後的數溢位,有 WRAP、SAT、FAIL,預設是 WRAP 模式,溢位了也沒問題。

而 WRAP 會從負數到正數範圍內取值,如 i8 則為 -127~+128;而 SAT 模式在遞增時,如果即將發生溢位,那麼他不會執行此操作,將值一直保持為 127。

# WRAP
127.0.0.1:6379> BITFIELD mystring SET i8 #0 127
1) (integer) 127
127.0.0.1:6379> BITFIELD mystring  incrby i8 0 1
1) (integer) -128
127.0.0.1:6379> BITFIELD mystring  incrby i8 0 257
1) (integer) -127
# SAT
127.0.0.1:6379> BITFIELD mystring SET i8 #0 127
1) (integer) 127
127.0.0.1:6379> BITFIELD mystring overflow sat incrby i8 #0 1
1) (integer) 127
127.0.0.1:6379> BITFIELD mystring overflow sat incrby i8 #0 1
1) (integer) 127

如果溢位控制模式為 FAIL,會對檢測到的上溢或下溢執行任何操作。相應的返回值設定為 NULL 以向呼叫者發出條件訊號:

127.0.0.1:6379> BITFIELD mystring overflow fail incrby i8 #0 128
1) (nil)

列表型別

Redis 的 list 型別是連結串列,區別與一些語言的 List 型別,例如 C# 的 List 、Go 的切片型別內部使用陣列實現。因為 Redis list 是連結串列,所以 list 型別最平常的操作是頭部或尾部新增/移除元素,頭部或尾部的操作速度和時間跟元素數量不相關,1百萬個元素和1千萬個元素的操作速度是相同的。當然 list 的查詢速度比陣列慢。一個 list 最多可以包含 232- 1 個元素(4294967295)。

list 使用 lpush 和 rpush 命令在頭部或尾部插入元素,使用 lpop 和 rpop 命令在頭部或尾部移除元素:

127.0.0.1:6379> lpush list a b c d
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
127.0.0.1:6379> rpush list 1 2 3 4
(integer) 8
127.0.0.1:6379> lrange list 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
5) "1"
6) "2"
7) "3"
8) "4"

lpop 和 rpop 移除元素時,可以指定彈出的元素數量,如果不指定,預設數量是 1:

127.0.0.1:6379> lpop list 2
1) "d"
2) "c"

要注意,lpush list 1 2 3,結果是 3 2 1,而不是 1 2 3,因為每一個元素都會從左邊插入,相當於跑過第一,就是你第一。

插入過程:

1
---
2
1
---
3
2
1

除了 LPOP、RPOP,還有其它彈出頭部和尾部的命令。

BLPOP、BRPOP:從多個鍵的頭部或尾部彈出一個元素;

LMPOP(Redis 7.0 後可用):在多個鍵中彈出多個元素,示例:LMPOP 2 mylist mylist2 right count 3

BLMPOP:阻塞版本的 LMPOP;

lrange 表示從 list 的頭部取一定範圍的元素,其格式是 lrange {key} start stopstart stop 表示元素下標範圍,如取下標為 0-5 的六個元素:

127.0.0.1:6379> lrange list 0 5
1) "d"
2) "c"
3) "b"
4) "a"
5) "1"
6) "2"

如果要獲取全部元素,stop 取值為 -1:

127.0.0.1:6379> lrange list 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
5) "1"
6) "2"
7) "3"
8) "4"

lset 可以通過指定索引設定元素的值:

127.0.0.1:6379> lrange list 0 -1
 1) "a"
 2) "1"
127.0.0.1:6379> lset list 0 b
OK
127.0.0.1:6379> lrange list 0 -1
 1) "b"
 2) "1"

可以使用 llen 獲取 list 的元素數量:

127.0.0.1:6379> llen list
(integer) 8

lindex 可以獲取指定索引下標的元素的值:

1) "d"
2) "c"
3) "b"
4) "a"
5) "1"
6) "2"
7) "3"
8) "4"
127.0.0.1:6379> lindex list 0
"d"

lrem 命令可以從 list 的左邊或右邊開始掃描,移除 N 個值為 value 的元素:

127.0.0.1:6379> lrem list 2 b
(integer) 1

lrem 的命令格式為 lrem {key} [count] {value} ,如果 count 為 0 ,則表示移除全部值 {value} 的元素;如果 count > 0,則從左邊開始掃描,移除對應數量的元素;如果 count < 0,則從右邊開始掃描,移除 |count| 個對應的元素。

linsert 可以在指定元素值前或後插入一個新的值,其命令格式如下:

linsert  key BEFORE|AFTER pivot element
# privot 元素值
127.0.0.1:6379> lpush list 3 2 1 1 2 3 3 2 2 1 1
(integer) 11
127.0.0.1:6379> lrange list 0 -1
 1) "1"
 2) "1"
 3) "2"
 4) "2"
 5) "3"
 6) "3"
 7) "2"
 8) "1"
 9) "1"
10) "2"
11) "3"
127.0.0.1:6379> linsert list before 1 a
(integer) 12
127.0.0.1:6379> lrange list 0 -1
 1) "a"
 2) "1"
 3) "1"
 4) "2"
 5) "2"
 6) "3"
 7) "3"
 8) "2"
 9) "1"
10) "1"
11) "2"
12) "3"

lmove 命令可以從一個 list 中彈出頭部或尾部,然後壓入另一個 list 中,其格式:LMOVE source destination LEFT|RIGHT LEFT|RIGHT

127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
127.0.0.1:6379> lrange test 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> lmove list test right right
"d"
127.0.0.1:6379> lrange list 0 -1
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> lrange test 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "d"

示例中,由於兩個位置引數都是 right,因此只處理 "list" 中的尾部元素,並壓入到 "testt" 中。

list: a b c d
test: 1 2 3 4
---
list: a b c
test: 1 2 3 4 d

而 blmove 命令是 lmove 的阻塞版本,類似 Go 語言的 chan,如果 "list" 沒有元素,那麼會被阻塞;當 "list" 有元素後,馬上移除並壓入到 "test" 中。

其命令示例如下:

# list 中沒有元素時;5 是阻塞超時時間
127.0.0.1:6379> blmove list test right right 5
(nil)
(5.10s)

blmove 命令只會阻塞一次,不會一直阻塞,如果 "list" 壓入了任一元素,則會馬上處理。

如果只插入了一個元素,即使是 left right,也會馬上處理。

可以利用 Redis 製作訊息佇列,使用 rpop 命令從 未處理列表 的尾部插入元素,而使用 lmove 或 blmove 命令從 list 左側移除訊息並放到移除已處理列表中。

list 型別具有以下列出的命令,有部分命令可能已經失效或在將來的版本中去除,本文只列舉部分常用的命令,讀者可參考官網文件說明。

  • BLMOVE :阻塞式的 lmove;
  • BLMPOP:阻塞式的 lmpop;
  • BLPOP::從多個鍵的頭部彈出一個元素,阻塞式;
  • BRPOP:從多個鍵的尾部彈出一個元素,阻塞式;
  • BRPOPLPUSH:阻塞式的 RPOPLPUSH;
  • LINDEX:獲取指定索引下標的元素的值;
  • LINSERT:指定位置插入元素;
  • LLEN:獲取 list 的元素數量;
  • LMOVE:原子地返回並移除 list 的第一個/最後一個元素,並壓入到另一個list 中。
  • LMPOP:在多個 list 中彈出多個元素;
  • LPOP:在一個 list 中彈出多個元素;
  • LPOS:返回 list 中匹配元素的索引;
  • LPUSH:從頭部壓入多個元素;
  • LPUSHX:與 LPUSH 命令相似,如果 key 不存在,則 LPUSHX 不會建立新的 key;
  • LRANGE:獲取一定範圍的元素;
  • LREM:移除 N 個值為 value 的元素
  • LSET:通過指定索引設定元素的值;
  • LTRIM:list 上推送一個新元素,同時確保列表不會增長到超過從指定索引開始的元素數量
  • RPOP:在一個 list 中彈出多個元素;
  • RPUSH:從尾部壓入多個元素;
  • RPUSHX:與 RPUSH 命令相似,如果 key 不存在,則 LPUSHX 不會建立新的 key;

雜湊型別

雜湊型別可以儲存多個鍵值對,並且元素數量是沒有限制的,雜湊中每一行儲存一個鍵值對,每行的 key 稱為一個欄位。

HSET、HGET 用於設定或獲取雜湊的欄位的值。

# hset 可以同時設定多個欄位的值,但 hget 只能獲取一個欄位的值;hmget 可以同時獲取多個欄位的值;
127.0.0.1:6379> hset h a 1 b 2 c 3
(integer) 3
127.0.0.1:6379> hget h a
"1"
127.0.0.1:6379> hmget h a b c
1) "1"
2) "2"
3) "3"

另外,雜湊中也有類似字串的原子操作 HINCRBY、HINCRBYFLOAT 命令,這裡就不在展開講解。

HSCAN 命令可以從雜湊中搜尋符合條件的欄位的值,其使用格式如下:

HSCAN key cursor [MATCH pattern] [COUNT count]
  • cursor :遊標,開始位置,從上一次搜尋結果的第幾條結果開始再進行搜尋。
  • pattern :匹配的模式。
  • count :指定從資料集裡返回多少元素,實際返回個數會圍繞該數波動,預設值為 10 。

示例:

hscan test 0 match "a*" count 2

hash 型別具有以下列出的命令,有部分命令可能已經失效或在將來的版本中去除,本文只列舉部分常用的命令,讀者可參考官網文件說明。

  • HDEL:刪除某個欄位;
  • HEXISTS:判斷某個欄位是否存在;
  • HGET:獲取欄位值;
  • HGETALL:獲取所有欄位;
  • HINCRBY:原子操作;
  • HINCRBYFLOAT:原子操作;
  • HKEYS:獲取所有欄位的名稱;
  • HLEN:獲取元素數量;
  • HMGET:獲取多個欄位的值;
  • HRANDFIELD:獲取指定範圍的欄位的值;
  • HSCAN:搜尋元素;
  • HSET:設定欄位值;
  • HSETNX:設定欄位值,但如果欄位已經存在,則無效;
  • HSTRLEN:返回欄位的值的字串長度;
  • HVALS:獲取所有值;

集合

Redis Set 是無序字串集合,其內部使用雜湊表實現,因此新增,刪除,查詢的複雜度都是 O(1),最多可以包含 232- 1 個元素(4294967295),其內部的元素不能重複。

集合的主要操作是執行多個集合的交集、並集、差集等。例如使用者的訊息列表中,可以使用一個集合存放使用者的所有訊息,另一集合存放使用者已讀訊息,兩者的差集即為未讀訊息。除了交集、並集、差集,還可以提取隨機元素等。

其結構形式如圖所示。

SADD 命令可以往集合中新增值,如果值已經存在,則會被忽略,即不會替換舊的值。

127.0.0.1:6379> sadd set 5
(integer) 1
127.0.0.1:6379> sadd set 5
(integer) 0

SADD 命令可以同時設定多個元素值:

127.0.0.1:6379> sadd set 5 1 2 4 5 6
(integer) 1

SDIFF 可以獲取第一個集合與其他集合的差集並返回結果;而 SDIFFSTORE 獲取第二個集合與後面集合的差集,並儲存到第一個集合中。

SDIFF 命令:

key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SDIFF key1 key2 key3 = {b,d}

SDIFFSTORE 命令:

key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SDIFFSTORE key1 key3 key2 = {a,b,c,d}

使用 SDIFFSTORE 命令時,如果 key1 中有元素,則會被清空,然後儲存差集的結果。

SINTER 可以讓多個集合生成交集並返回交集;而 SINTERCARD 獲取交集中的元素數量;

key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SINTER key1 key2 key3 = {c}key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SINTER key1 key2 key3 = {c}

打牌遊戲

以打牌為例,一副牌中有 52 張牌,我們使用一些字元表示:

sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK

然後系統開始隨機給每個玩家派牌,每個使用者一張牌,那麼需要每次從牌中刪除一個元素,並返回到客戶端中,SPOP 可以隨機刪除一個元素並返回到客戶端,但是如果沒有儲存記錄,那麼後面無法記牌,因此需要使用將記錄放到使用者集合中。

# 隨機取 5 個元素,但是不處理集合
127.0.0.1:6379> SRANDMEMBER  deck 5
1) "HQ"
2) "S9"
3) "H9"
4) "SK"
5) "H7"
# 將這 5 個牌儲存到 user:1 中
# 這個步驟需要寫程式碼,可以結合 SMOVE 等命令來做,這裡手動操作;
127.0.0.1:6379> sadd user:1 HQ S9 H9 SK H7
(integer) 5

# 接著,生成差集,並儲存回 deck 中
127.0.0.1:6379> SDIFFSTORE deck deck user:1
(integer) 47

如法炮製,將 15 個牌分配到三個使用者中。

127.0.0.1:6379> SRANDMEMBER  deck 5
1) "D6"
2) "C4"
3) "C1"
4) "S6"
5) "S4"
127.0.0.1:6379>  sadd user:2 D6 C4 C1 S6 S4
(integer) 5
127.0.0.1:6379> SDIFFSTORE deck deck user:2
(integer) 42
127.0.0.1:6379> SRANDMEMBER  deck 5
1) "CJ"
2) "CK"
3) "H8"
4) "SJ"
5) "H2"
127.0.0.1:6379>  sadd user:3 CJ CK H8 SJ H2
(integer) 5
127.0.0.1:6379> 
127.0.0.1:6379> SDIFFSTORE deck deck user:3
(integer) 37

set 型別具有以下列出的命令,有部分命令可能已經失效或在將來的版本中去除,本文只列舉部分常用的命令,讀者可參考官網文件說明。

  • SADD:將一個或多個 member 元素加入到集合 key 當中,已經存在於集合的 member 元素將被忽略;
  • SCARD:返回集合 key 的基數(集合中元素的數量);
  • SDIFF:返回由第一個集合和所有後續集合之間的差異產生的集合成員;
  • SDIFFSTORE:此命令等於SDIFF,但不是返回結果集,而是儲存在destination
  • SINTER:返回由所有給定集合的交集產生的集合成員。
  • SINTERCARD:類似 SINTER。返回給定集合的交集 的元素數量;
  • SINTERSTORE:類似 SINTER,但它不返回結果集,將結果存到第一個 Key 中;
  • SISMEMBER:判斷是否為集合的成員;
  • SMEMBERS:返回儲存在集合的所有成員的值;
  • SMISMEMBER:判斷多個值是否在此集合中;
  • SMOVE:將一個值從集合中移動到另一個集合,操作是原子性的;
  • SPOP:從集合中刪除並返回一個或多個隨機成員key;如 SPOP myset 3 隨機刪除三個值;
  • SRANDMEMBER:如果命令執行時,只提供了 key 引數,那麼返回集合中的一個隨機元素;較為複雜,請檢視文件;
  • SREM:移除集合 key 中的一個或多個 member 元素,不存在的 member 元素會被忽略;
  • SSCAN:搜尋元素;
  • SUNION:並集;
  • SUNIONSTORE:生成並集儲存到第一個集合中;

有序集合

有序集合(sorted set) 與集合類似,不允許元素重複,有序集合的每個元素可以設定一個 score 屬性值,score 越小,元素的位置越靠前,有序集合通過 score 對元素進行排序。不同元素的 score 值可以相同,如果 score 相同,則接著比較元素的值大小。

127.0.0.1:6379> zadd test 1 B 1 A
(integer) 2

如上命令所示,建立有序集合時,建立順序是 B、A,且 score 值相同,但是 Redis 會接著比較元素的值,進行排序。

另外 score 是浮點型別,可以設定小數。

ZADD 命令可以建立有序集合,zrange 命令可以獲取指定範圍的元素。

127.0.0.1:6379> zrange test 0 -1
1) "A"
2) "B"

zrange 可以加上 WITHSCORES 引數,獲取元素的同時返回 score 值。

127.0.0.1:6379> zrange test 0 -1 WITHSCORES
1) "A"
2) "1"
3) "B"
4) "1"

score 也可以賦予一定的含義,如出生年份。

例如 A、B 兩人在 1950 年出生,C、D 兩人在 1951 年出生,其有序集合顯示如下:

zrangebyscore 可以根據 score 值對元素進行篩選,其命令格式如下:

zrangebyscore year min max [WITHSCORES] [LIMIT offset count]

例如,獲取 1950-1951 出生的 1 個人:

127.0.0.1:6379> zrangebyscore year 1950 1951 
1) "A"
2) "B"
3) "C"
4) "D"
127.0.0.1:6379> zrangebyscore year 1950 1951 limit 0 1
1) "A"

limit 0 1 表示偏移量為0,數量為1。

如果要表達小於或大於,可以使用 ( 符號,例如範圍在 1950-1951之間,但是不包含 1950,則可以使用 (1950 1951,示例:

127.0.0.1:6379> zrangebyscore year (1950 1951
1) "C"
2) "D"
127.0.0.1:6379> zrangebyscore year 1950 (1951
1) "A"
2) "B"
127.0.0.1:6379> zrangebyscore year (1950 (1951
(empty array)

如果要獲取有序集合中的所有元素,可以將 min max 的值設定為-inf +inf,示例:

127.0.0.1:6379> zrangebyscore year  -inf +inf
1) "A"
2) "B"
3) "C"
4) "D"

如果要表示小於 1951,示例如下:

127.0.0.1:6379> zrangebyscore year  -inf 1951
1) "A"
2) "B"
3) "C"
4) "D"

zrangebylex 命令可以根據元素的第一個字母進行範圍篩選,如獲取首字母在 A、C 範圍內的元素:

127.0.0.1:6379> zrangebylex year [A [C
1) "A"
2) "B"
3) "C"

另外,可以使用 -+ 表示負無窮和正無窮,示例:

127.0.0.1:6379> zrangebylex year - [C
1) "A"
2) "B"
3) "C"

有序集合的命令比較多,本文只列舉部分常用的命令,讀者可參考官網文件說明。

字串、雜湊、列表、集合、有序集合是 Redis 的基本資料型別,在此基礎上,Redis 增加了地理位置、點陣圖、日誌等多種功能或命令,讀者有興趣請參考官方文件。

Redis 功能

事務

Redis 的事務是多個命令的集合,但是 Redis 的事務不具備失敗回滾功能,甚至命令執行失敗也不會主動取消事務的執行。客戶端使用 MULTI 命令進入 Redis 事務,Redis 總是響應 "OK",此時,客戶端可以發出多個命令,Redis 不執行這些命令,而是將它們排隊,一旦呼叫 EXEC,將執行所有命令。

Redis 的事務主要命令有四個:MULTI, EXEC, DISCARD 和 WATCH,Redis 事務的設計理念傾向於快,因此缺少很多保障。

Redis 事務的設計目標主要有兩個:

1,事務中的所有命令都按順序序列化和執行。

2,要麼處理所有命令,要麼不處理命令,因此 Redis 事務也是原子的。

一個簡單的事務操作如下,每個命令都將加入到事務的命令佇列中:

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC

事務中執行的命令出現錯誤時,事務不會終止,而是一直執行下去,與此同時,Redis 的事務也不支援回滾,示例命令如下:

127.0.0.1:6379(TX)> BITFIELD mystring SET i8 #0 127
QUEUED
127.0.0.1:6379(TX)> BITFIELD mystring overflow fail incrby i8 #0 128
QUEUED
127.0.0.1:6379(TX)> get mystring
QUEUED
127.0.0.1:6379(TX)> exec
1) 1) (integer) 127
2) 1) (nil)
3) "\x7f"
127.0.0.1:6379(TX)> get 123
QUEUED
127.0.0.1:6379(TX)> get mystring
QUEUED
127.0.0.1:6379(TX)> exec
1) (nil)
2) "\x7f"

如果加入命令的時候命令格式不正確,則不會加入到命令佇列中。

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> incr mystring 1 1 1
(error) ERR wrong number of arguments for 'incr' command

在加入事務佇列命令時,可以使用 DISCARD 取消事務佇列,中止事務,但是如果事務已經執行,則不能停止此事務的執行。

Redis 的事務原子性的,事務中的命令要麼執行,要麼不執行;另外一個客戶端的事務在加入命令佇列的過程中,不會被其他客戶端干擾,每個客戶端建立的隊事務物件都是其自身可見;但是每個命令的操作不是原子性的,例如 A 客戶端的事務正在修改 mystring 的值,此時,B 客戶端也可以直接修改值,或也通過事務修改值。

如果事務執行過程中,有部分 Key 修改會影響事務的執行,可以使用 watch 命令監聽 Key,如果事務執行期間之前或執行期間, Key 被除自己外的客戶端改動或刪除,則事務會被終止。

A 客戶端監控 Key 的值,但還沒有執行事務:

127.0.0.1:6379> set tran 1
OK
127.0.0.1:6379> watch tran
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set tran 2
QUEUE

B 客戶端修改 Key 的值:

127.0.0.1:6379> set tran 2
OK

此時,A 客戶端執行事務:

127.0.0.1:6379(TX)> exec
(nil)

如果是客戶端自己對 Key 進行操作,則不會終止事務:

127.0.0.1:6379> watch tran
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set tran 3
QUEUED
127.0.0.1:6379(TX)> get tran 
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) "3"

釋出訂閱

Redis 的釋出訂閱也是很強大的,速度也快,在需求不是很嚴格的情況下,使用 Redis 做釋出訂閱更簡單易用。

首先,A 客戶端訂閱一個通道,在 Redis 中稱為通道(channel),在 MQTT 和一些訊息佇列中介軟體中一般稱為 topic,Redis 訂閱訊息示例如下:

127.0.0.1:6379> subscribe chan1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "chan1"
3) (integer) 1

此時 B 客戶端可以向 通道釋出訊息,所有訂閱者都可以收到相同的訊息:

127.0.0.1:6379> publish chan1 測試
(integer) 1

因為筆者是在 redis-cli 操作,所以傳輸的訊息內容都是些簡單的,一般在程式中傳遞的訊息都是具有一定格式的,如 json,訂閱者收到訊息後,使用工具進行反序列化為物件。

在 redis-cli 中,訂閱訊息後,則當前視窗會被阻塞,但是使用 TCP 直接連線 Redis ,訂閱訊息後,客戶端不會被阻塞,可以繼續傳送命令到 Redis 中,如果收到訊息推送,則 Redis 會傳送訊息到客戶端。不同程式語言的處理細節不一樣,具體細節可以參考程式語言的類庫。當客戶端想取消訂閱時,可以使用 unsubscribe 命令。

通道的 Key 是獨立存放的,不會跟基礎型別的 Key 衝突,另外通道的 Key 也可以使用 {物件型別}:{物件標識/id}:{屬性名稱} 進行命名,以一類標識做通道名稱方便訂閱者訂閱。

如網站中有多個欄目多篇文章,其中小明是負責《動物世界》專欄的主編,因此小明希望訂閱部分重點文章的最新反饋情況,這裡假設,小明要訂閱所有文章的狀態,那麼我們可以把每篇文章使用一個通道標識:columns:1:article:1columns:1:article:2 ... ...

那麼小明可以批量訂閱:

127.0.0.1:6379> subscribe columns:1:article:*
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "columns:1:article:*"
3) (integer) 1

慢查詢

一條命令的生命週期:
1,傳送命令
2,命令排隊
3,命令執行
4,返回結果

每條命令執行時,都會消耗一定的時間,如果我們能夠獲取每條命令的執行時間或篩選那些執行時間較大的命令執行記錄,然後通過工具或其他方式找到這些命令,便可以進一步優化它,而 Redis 便提供了一個稱為 慢查詢日誌的功能。

所謂慢查詢日誌就是記錄每條命令的執行時間,當超過預設閥值,就將這條命令的相關資訊記錄下來。慢查詢只記錄命令執行時間,並不包括命令排隊和網路傳輸時間。慢查詢日誌並不是指只記錄查詢相關的命令,而是包含所有命令。

你可以通過配置檔案的形式配置慢查詢設定,也可以通過以下命令快速設定:

# config set slowlog-log-slower-than 20000
config set slowlog-log-slower-than 2
config set slowlog-max-len 1000
config rewrite

如果你使用配置檔案啟動 Redis,那麼 config rewrite 會將配置重新整理到配置檔案中,如果是直接啟動,則不需要執行此命令;

slowlog-log-slower 是慢查詢閾值,執行時間超過此值,因為 Redis 命令的執行速度很快,我們這裡數量並沒有多少,不能預設生產環境大量資料的情況,因此這裡直接設定為 2ns,可以很容易收集到資料。

然後隨便執行一些命令,如 keys *,然後檢視慢查詢日誌:

# slowlog get [n]
slowlog get 2
1) 1) (integer) 6
   2) (integer) 1638100161
   3) (integer) 12
   4) 1) "keys"
      2) "*"
   5) "127.0.0.1:59586"
   6) ""
2) 1) (integer) 5
   2) (integer) 1638100155
   3) (integer) 6
   4) 1) "config"
      2) "set"
      3) "slowlog-log-slower-than"
      4) "2"
   5) "127.0.0.1:59586"
   6) ""

慢日誌中會顯示多個屬性資訊,這些屬性資訊的含義如下:

1) 1) (integer) 6				# Id
   2) (integer) 1638100161		# 執行命令的時間戳
   3) (integer) 12				# 命令耗時
   4) 1) "keys"					# 命令
      2) "*"					# 引數
   5) "127.0.0.1:59586"			# 客戶端
   6) ""

獲取慢查詢日誌數量:

127.0.0.1:6379> slowlog len
(integer) 8

重置慢查詢日誌記錄:

slowlog reset

通過 slowlog 命令,可以幫助我們找到 Redis 可能存在的效能瓶頸。據一些文件的建議,slowlog-log-slower-than 可以設定認定 10ms 為慢查詢。因為 Redis 是單執行緒執行命令,假設當慢命令執行時間是 10ms 時,那麼這個 Redis 的 OPS 是 100,如果慢命令執行時間是 1ms 時,這個系統的 OPS 是 1000。

Redis 效能測試

redis-benchmark 是 Redis 的一個基準效能測試工具,在安裝了 Redis 的系統中,一般會自帶。它有以下幾個命令可以幫助使用者連線到處於其他位置的 Redis Server。

 -h <hostname>      Server hostname (default 127.0.0.1)
 -p <port>          Server port (default 6379)
 -s <socket>        Server socket (overrides host and port)
 -a <password>      Password for Redis Auth
 --user <username>  Used to send ACL style 'AUTH username pass'. Needs -a.

如連線到本地 Redis 服務:

redis-benchmark -h 127.0.0.1 -p 6379 -a 123456 -n 200000 -c 20

加上 --csv 引數,可以將執行結果放到 Excel 中。

它主要有兩個測試引數:

  • -c:選項代表客戶端的併發數量,預設是50;
  • -n:選項代表客戶端請求總量,預設是100000;
====== MSET (10 keys) ======                                                   
  200000 requests completed in 2.51 seconds
  20 parallel clients
  3 bytes payload
  keep alive: 1
  host configuration "save": 3600 1 300 100 60 10000
  host configuration "appendonly": no
  multi-thread: no

Summary:
  throughput summary: 79744.82 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.216     0.056     0.199     0.311     0.631     5.919

可以看到,筆者的 Redis 例項 79744.82 requests per second ,即每秒處理了近8w個請求,平均每個命令耗時 0.216 ms,粗略計算,1s 可以處理 4,629 個命令。

redis-benchmark 會傳送很多命令,這些命令都是具有一定功能的,能夠很好模擬正常的操作,測試完成後,這些命令不會對你的 Redis 例項產生影響,但是會留下三個 Key,刪除掉即可。

使用 slowlog get 100 檢視都執行了什麼命令:

因為筆者的 Redis 例項平均執行命令時間是 0.216,因此可以將 config set slowlog-log-slower-than 設定大一點,例如 1ms。

redis-benchmark 在測試的時候不會插入很多鍵,如果有需要,可以使用 -r 引數,生成更多鍵和模擬命令。

redis-benchmark -h 127.0.0.1 -p 6379 -a 123456 -n 200000 -c 20 -r 1000

此時,Key 達到了驚人的 2000 數量:

127.0.0.1:6379> dbsize
(integer) 2009

上面的測試資料看起來不錯,但並不是真實的網路請求,而在真實情況中,跨主機跨子網引數資料的時間消耗比較驚人的!網路 IO 會消耗主機較大的效能,也會佔用較多的時間,如果客戶端與 Redis 服務之間的 ping 是 20ms(時延),那麼 200000/20 個客戶端,需要 10000ms,如果每個客戶端開啟 10 個執行緒併發傳送,可能也需要 1s。

相關文章