Redis 管道、事務、Lua 指令碼對比
技術標籤:redis
概述
Redis
提供三種將客戶端多條命令打包傳送給服務端執行的方式: Pipelining(管道)
、 Transactions(事務)
和 Lua Scripts(Lua 指令碼)
。本文不會過細的討論三種方式的基礎知識,將從這三種方式的 優勢
、 侷限性
和 原子性
方面展開討論
Pipelining(管道)
Redis 管道是三者之中最簡單的,當客戶端需要執行多條 redis
命令時,可以通過管道一次性將要執行的多條命令傳送給服務端,其作用是為了降低 RTT(Round Trip Time)
對效能的影響,比如我們使用 nc
命令將兩條指令傳送給 redis
$ printf "INCR x\r\nINCR x\r\n" | nc localhost 6379
:1
:2
可以看到,管道只是簡單的將多個命令拼接在一起,命令之間用換行符(/r/n)分割,並沒有在第一條命令前或最後一條命令後面新增開始/結束標誌位
redis
服務端接收到管道傳送過來的多條命令後,會一直執命令,並將命令的執行結果進行快取,直到最後一條命令執行完成,再所有命令的執行結果一次性返回給客戶端
Pipelining 的優勢
在效能方面, Pipelining
有下面兩個優勢:
- 將多條命令打包一次性發送給服務端,減少了客戶端與服務端之間的網路呼叫次數,節省了
RTT
- 避免了上下文切換,當客戶端/服務端需要從網路中讀寫資料時,都會產生一次系統呼叫,系統呼叫是非常耗時的操作,其中設計到程式由使用者態切換到核心態,再從核心態切換回使用者態的過程。當我們執行 10 條
redis
命令的時候,就會發生 10 次使用者態到核心態的上下文切換,但如果我們使用Pipeining
將多條命令打包成一條一次性發送給服務端,就只會產生一次上下文切換
Pipelining 原子性
我們都知道, redis
執行命令的時候是單執行緒執行的,所以 redis
中的所有命令都具備原子性,這意味著 redis
並不會在執行某條命令的中途停止去執行另一條命令
但是 Pipelining
並不具備原子性,想象一下有兩個客戶端 client1
client2
同時向 redis
服務端傳送 Pipelining
命令,每條 Pipelining
包含 5 條 redis
命令。 redis
可以保證 client1
管道中的命令始終是順序執行的, client2
管道中的命令也是一樣,始終按照管道中傳入的順序執行命令
但是 redis
並不能保證等 client1
管道中的所有命令執行完成,再執行 client2
管道中的命令,因此,在服務端中的命令執行順序有可能是下面這種情況
![image.png](https://img-blog.csdnimg.cn/img_convert/29af8d8a442f82b89a9d26b0d1d72a15.png#align=left&display=inline&height=136&margin=[object Object]&name=image.png&originHeight=136&originWidth=369&size=5184&status=done&style=none&width=369)
這種行為顯示 Pipelining
在執行的時候並不會阻塞服務端。即使 client1
向客戶端傳送了包含多條指令的 Pipelining
,其他客戶端也不會被阻塞,因為他們傳送的指令可以插入到 Pipelining
中間執行
Pipelining 侷限性
只有在 Pipelining
內所有命令執行完後,服務端才會把執行結果通過陣列的方式返回給客戶端。在執行 Pipelining
內的命令的時候,如果某些指令執行失敗, Pipelining
仍會繼續執行
比如下面的例子
$ printf "SET name huangxy\r\nINCR name\r\nGET name\r\n" | nc localhost 6379
+OK
-ERR value is not an integer or out of range
$6
huangxy
Pipelining
中第二條指令執行失敗, Pipelining
並不會停止,而是會繼續執行,等所有命令都執行完的時候,再將結果返回給客戶端,其中第二條指令返回的是錯誤資訊
Pipelining
的這個特性會導致一個問題,就是當 Pipelining
中的指令需要讀取之前指令設定 key 的時候,需要額外小心,因為 key 的值有可能會被其他客戶端修改。此時 Pipelining
的執行結果往往就不是我們所預期的
Pipelining 使用場景
- 對效能有要求
- 需要傳送多個指令到服務端
- 不需要上個命令的返回結果作為下個命令的輸入
Transactions(事務)
redis
中的事務,跟我們之前在學關係型資料庫的時候所瞭解到的事務概念有點區別。 redis
中的事務機制主要是用來對多個命令進行排隊,並在最後決定是否需要執行事務中的所有命令與否
與管道不同,事務使用特殊的命令來標記事務的開始和結束( MULTI
、 EXEC
、 DISCARD
)。伺服器還可以對事務中的命令進行排隊(這樣客戶端可以一次傳送一條命令)。除此之外,一些第三方庫還喜歡在客戶端中對事務的命令進行快取,然後通過在管道中傳送整個事務的方式對其進行優化
事務的優點
事務提供了 WATCH
命令,使我們可以實現 CAS 功能,比如通過事務,我們可以實現跟 INCR
命令一樣的功能
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
事務的原子性
redis
事務具備原子性,當一個事務正在執行時,服務端會阻塞其接收到的其他命令,只有在事務執行完成時,才會執行接下來的命令,因此事務具備原子性
事務的侷限性
跟 Pipelining
一樣,只有在事務執行完成時,才會把事務中多個命令的結果一併返回給客戶端,因此客戶端在事務還沒有執行完的時候,無法獲取其命令的執行結果
如果事務中的其中一個命令發生錯誤,會有以下兩種可能性:
- 當發生語法錯誤,在執行
EXEC
命令的時候,事務將會被丟棄,不會執行 - 當發生執行時錯誤(操作了錯誤的資料型別)時,
redis
會將報錯資訊快取起來,繼續執行後面的命令,並在最後將所有命令的執行結果返回給客戶端(報錯資訊也會返回)。這意味著redis
事務中沒有回滾機制
事務使用場景
- 需要原子地執行多個命令
- 不需要事務中間命令的執行結果來編排後面的命令
Lua 指令碼
redis
從 2.6 版本開始引入對 Lua 指令碼的支援,通過在伺服器中嵌入 Lua 環境, redis
客戶端可以直接使用 Lua 指令碼,在服務端原子地執行多個 redis
命令
Lua 指令碼的優勢
與 Pipelining
和 事務不同的是,在指令碼內部,我們可以在指令碼中獲取中間命令的返回結果,然後根據結果值做相應的處理(如 if 判斷)
local key = KEYS[1]
local new = ARGV[1]
local current = redis.call('GET', key)
if (current == false) or (tonumber(new) < tonumber(current)) then
redis.call('SET', key, new)
return 1
else
return 0
end
同時, redis
服務端還支援對 Lua 指令碼進行快取(使用 SCRIPT LOAD
或 EVAL
執行過的指令碼服務端都會對其進行快取),下次可以使用 EVALSHA
命令呼叫快取的指令碼,節省頻寬
Lua 指令碼的原子性
Lua 指令碼跟事務一樣具備原子性,當指令碼執行中時,服務端接收到的命令會被阻塞
Lua 指令碼的侷限性
Lua 指令碼在功能上沒有過多的限制,但要注意的一點是,Lua 指令碼在執行的時候,會阻塞其他命令的執行,所以不宜在指令碼中寫太耗時的處理邏輯
Lua 指令碼的使用場景
- 需要原子性地執行多個命令
- 需要中間值來組合後面的命令
- 需要中間值來編排後面的命令
- 常用於擴充套件
redis
功能,實現符合自己業務場景的命令
參考文件
- https://redis.io/topics/pipelining
- https://redis.io/topics/transactions
- https://redis.io/commands/eval
- https://rafaeleyng.github.io/redis-pipelining-transactions-and-lua-scripts
- 《Redis 設計與實現》 黃健巨集著