Pipeline
客戶端將多條命令打包發送,服務器順序執行并一次性返回所有結果。可以減少網絡往返延遲(RTT)以提升吞吐量。
需要注意的是,Pipeline 中的命令按順序執行,但中間可能被其他客戶端的命令打斷。
典型場景:批量插入、查詢或更新數據,也就是命令間沒有什么依賴關系的情況。
比如下面這個例子,Pipeline 將 3 個 SET 打包發送,減少三次網絡往返為一次。假設單次命令 RTT 為 1ms,3 次命令需 3ms;使用 Pipeline 僅需約 1ms。
SET key1 value1
SET key2 value2
SET key3 value3
事務
可以確保一組命令“原子”執行,確保一組命令執行過程中不被其他客戶端打斷。如果事務中有命令失敗,整個事務不會回滾,但后續命令不會執行。
通常,它會結合 WATCH
來實現樂觀鎖。
另外,Redis 中的原子性和 MySQL 中的原子性意義不同。MySQL 的原子性意味著,要么全部執行成功,要么就不執行,它會涉及回滾的操作。但 Redis 沒有沒有回滾的操作(為了性能,實現回滾需要維護事務日志等其他一些機制,會增加開銷),它的原子性指,一組命令順序執行,不會被其他命令打斷。
還有,Redis 的事務并不會像 Pipeline 一樣,將命令一起發送給 Redis,而是會一條一條發送。為什么?可以結合下面的過程,這樣的話可以用來進行語法檢查。
客戶端發送 MULTI,開啟事務。
客戶端逐條發送命令(如 SET、INCR),每條命令立即傳輸到服務器。
服務器收到命令后,不執行,而是將其放入事務隊列,并返回 QUEUED 響應。
客戶端發送 EXEC,服務器原子性執行隊列中的所有命令,并返回結果。
或者,客戶端發送 DISCARD,清空隊列,取消事務。
在實際使用中,比如在 Python 中的 redis-py 庫,事務會以 Pipeline 的方式批量發送,來優化命令發送。
pipe = r.pipeline() # 創建 Pipeline
pipe.multi() # 開始事務
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.execute() # 發送并執行事務
典型場景:需要保證數據一致性的操作,如轉賬、庫存扣減。
WATCH accountA -- 監控賬戶 A 的鍵,防止被其他客戶端修改
MULTI
GET accountA -- 如果賬戶 A 的值發生改變,剩下的命令將不會執行
SET accountA $new_balanceA -- 更新賬戶 A 余額(減 100)
SET accountB $new_balanceB -- 更新賬戶 B 余額(加 100)
EXEC
Lua 腳本
Lua 腳本可以在一條命令中實現復雜邏輯,如條件判斷、循環、錯誤控制。它和事務功能上有些相似,都是為了實現原子性。不過 Lua 腳本有兩種選擇,redis.call()
失敗時腳本停止(前面已經執行完的命令不會受影響);redis.pcall()
捕獲錯誤繼續執行。
它比起事務有什么好處呢?
- 比如事務中需要依賴于一個 GET 操作的結果,來決定后面的操作,事務無法實現,需要結合客戶端的代碼,加大了復雜性。
- 事務中的每條命令都會與 Redis 服務器進行網絡交互,增加了客戶端與服務器的交互。
另外,為了避免每次都需要傳輸完整的 Lua 腳本給 Redis,Redis 還設立了一個緩沖區,來去存放 Lua 腳本的內容以及它的 SHA1 值(一種哈希算法,將 Lua 腳本內容映射為固定長度的唯一標識符)。之后只需傳輸對應的 SHA1 值即可執行對應的腳本。
總之,Lua 腳本會更靈活,事務能做的,Lua 都能做。所以能用 Lua 就用 Lua。
典型場景:需要保證數據一致性的操作,如轉賬、庫存扣減。和事務差不多。
local accountA = KEYS[1] -- 賬戶 A 的鍵
local accountB = KEYS[2] -- 賬戶 B 的鍵
local amount = tonumber(ARGV[1]) -- 轉賬金額(轉換為數字)-- 獲取賬戶 A 余額(不存在則默認為 0)
local balanceA = redis.call('GET', accountA) or 0
balanceA = tonumber(balanceA)-- 檢查余額是否足夠
if balanceA < amount thenreturn 0 -- 余額不足,返回 0
end-- 獲取賬戶 B 余額(不存在則默認為 0)
local balanceB = redis.call('GET', accountB) or 0
balanceB = tonumber(balanceB)-- 執行轉賬
redis.call('SET', accountA, balanceA - amount) -- 扣減賬戶 A
redis.call('SET', accountB, balanceB + amount) -- 增加賬戶 Breturn 1 -- 轉賬成功,返回 1-- Redis 調用方式:
EVAL "local accountA = KEYS[1] local accountB = KEYS[2] local amount = tonumber(ARGV[1]) local balanceA = redis.call('GET', accountA) or 0 balanceA = tonumber(balanceA) if balanceA < amount then return 0 end local balanceB = redis.call('GET', accountB) or 0 balanceB = tonumber(balanceB) redis.call('SET', accountA, balanceA - amount) redis.call('SET', accountB, balanceB + amount) return 1" 2 accountA accountB 30
參考
- Redis 事務 vs Lua,區別以及如何選擇
- 【Redis】- 事務和Lua腳本
- Redis 管道、事務、Lua 腳本對比