背景介紹
redis數據庫提供了一些管理功能比如
流水線:打包發送多條命令,並在一個回復里面接收所有被執行命令的結果。
事務:一次執行多條命令,被執行的命令要么就全部都被執行,要么就一個也不執行。並且事務執行過
程中不會被其他工作打斷。
樂觀鎖:監視特定的鍵,防止事務出現競爭條件。
雖然這些附加功能都非常有用,但它們也有一些缺陷。
流水線的缺陷
盡管使用流水線可以一次發送多個命令,但是對於一個由多個命令組成的復雜操作來說,為了執行該
操作而不斷地重復發送相同的命令,這並不是最高效的做法,會對網絡資源造成浪費。
如果我們有辦法避免重復地發送相同的命令,那么客戶端就可以減少花在網絡傳輸方面的時間,操作
就可以執行得更快。
事務和樂觀鎖的缺陷
雖然使用事務可以一次執行多個命令,並且通過樂觀鎖可以防止事務產生競爭條件,但是在實際中,要
正確地使用事務和樂觀鎖並不是一件容易的事情。
1. 對於一個復雜的事務來說,通常需要仔細思考才能知道應該對哪些鍵進行加鎖:鎖了不應該鎖的鍵會增加事務失敗的機會,甚至可能會造成程序出錯;而忘了對應該鎖的鍵進行加鎖的話,程序又會產生競爭條件。
2. 有時候為了防止競爭條件發生,即使操作本身不需要用到事務,但是為了讓樂觀鎖生效,我們也會使用事務將命令包裹起來, 這增加了實現的復雜度,並且帶來了額外的性能損耗。
誤用示例
《事務》一節介紹的 ZDECRBY 命令的實現,這里的事務僅僅是為了讓 WATCH 生效而用的:
def ZDECRBY(key, decrment, member):
# 監視輸入的有序集合
WATCH key
# 取得元素當前的分值
old_score = ZSCORE key member
# 使用當前分值減去指定的減量,得出新的分值
new_score = old_score - decrment
# 使用事務包裹 ZADD 命令
# 確保 ZADD 命令只會在有序集合沒有被修改的情況下執行
MULTI
ZADD key new_score member # 為元素設置新分值,覆蓋現有的分值
EXEC
避免事務被誤用的辦法
如果有一種方法,可以讓我們以事務方式來執行多個命令,並且這種方法不會引入任何競爭條件,那么我們就可以使用這種方法來代替事務和樂觀鎖。
擴展 Redis 功能時的麻煩
Redis 針對每種數據結構都提供了相應的操作命令,也對數據庫本身提供了操作命令,但如果我們需要對數據結構進行一些 Redis 命令不支持的操作,那么就需要使用客戶端取出數據,然后由客戶端對數據進行處理,最后再將處理后的數據儲存回 Redis 服務器。
舉個簡單的例子,因為 Redis 沒有提供刪除列表里面所有偶數數字的命令,所以為了執行這一操作,客戶端需要取出列表里面的所有項,然后在客戶端里面進行過濾,最后將過濾后的項重新推入到列表里面:
lst = LRANGE lst 0 -1 # 取出列表包含的所有元素
DEL lst # 刪除現有的列表
for item in lst: # 遍歷整個列表
if item % 2 != 0: # 將非偶數元素推入到列表里面
RPUSH lst item
並且為了保證這個操作的安全性, 還要用到事務和樂觀鎖,非常麻煩。
Lua 腳本
為了解決以上提到的問題, Redis 從 2.6 版本開始在服務器內部嵌入了一個 Lua 解釋器,使得用戶可以在服務器端執行 Lua 腳本。
這個功能有以下好處:
1. 使用腳本可以直接在服務器端執行 Redis 命令,一般的數據處理操作可以直接使用 Lua 語言或者Lua 解釋器提供的函數庫來完成,不必再返回給客戶端進行處理。
2. 所有腳本都是以事務的形式來執行的,腳本在執行過程中不會被其他工作打斷,也不會引起任何競爭條件,完全可以使用 Lua 腳本來代替事務和樂觀鎖。
3. 所有腳本都是可重用的,也即是說,重復執行相同的操作時,只要調用儲存在服務器內部的腳本緩存就可以了,不用重新發送整個腳本,從而盡可能地節約網絡資源。
執行 Lua 腳本
EVAL script numkeys key [key ...] arg [arg ...]
script 參數是要執行的 Lua 腳本。
numkeys 是腳本要處理的數據庫鍵的數量,之后的 key [key …] 參數指定了腳本要處理的數據庫鍵,被傳入的鍵可以在腳本里面通過訪問 KEYS 數組來取得,比如 KEYS[1] 就取出第一個輸入的鍵,KEYS[2] 取出第二個輸入的鍵,諸如此類。
arg [arg …] 參數指定了腳本要用到的參數,在腳本里面可以通過訪問 ARGV 數組來獲取這些參數。顯式地指定腳本里面用到的鍵是為了配合 Redis 集群對鍵的檢查,如果不這樣做的話,在集群里面使用腳本可能會出錯。
另外,通過顯式地指定腳本要用到的數據庫鍵以及相關參數,而不是將數據庫鍵和參數硬寫在腳本里面,用戶可以更方便地重用同一個腳本。
EVAL 命令使用示例
redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVAL "return 1+1" 0
(integer) 2
redis> EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 "msg" "age" 123 "hello world"
1) "msg" # KEYS[1]
2) "age" # KEYS[2]
3) "123" # ARGV[1]
4) "hello world" # ARGV[2]
在 Lua 腳本中執行 Redis 命令
通過調用 redis.call() 函數或者 redis.pcall() 函數,我們可以直接在 Lua 腳本里面執行 Redis 命令。
redis> EVAL "return redis.call('PING')" 0 # 在 Lua 腳本里面執行 PING 命令
PONG
redis> EVAL "return redis.call('DBSIZE')" 0 # 在 Lua 腳本里面執行 DBSIZE 命令
(integer) 4
# 在 Lua 腳本里面執行 GET 命令,取出鍵 msg 的值,並對值進行字符串拼接操作
redis> SET msg "hello world"
OK
redis> EVAL "return 'The message is: ' .. redis.call('GET', KEYS[1]) '" 1 msg
"The message is: hello world"
redis.call() 和 redis.pcall() 的區別
redis.call() 和 redis.pcall() 都可以用來執行 Redis 命令,它們的不同之處在於,當被執行的腳本出錯時,redis.call() 會返回出錯腳本的名字以及 EVAL 命令的錯誤信息,而 redis.pcall() 只返回 EVAL 命令的錯誤信息。
redis> EVAL "return redis.call('NotExistsCommand')" 0
(error) ERR Error running script (call to f_ddabd662fa0a8e105765181ee7606562c1e6f1ce):
@user_script:1: @user_script: 1: Unknown Redis command called from Lua script
redis> EVAL "return redis.pcall('NotExistsCommand')" 0
(error) @user_script: 1: Unknown Redis command called from Lua script
換句話來說,在被執行的腳本出錯時, redis.call() 可以提供更詳細的錯誤信息,方便進行查錯。
示例:使用 Lua 腳本重新實現 ZDECRBY 命令
創建一個包含以下內容的 zdecrby.lua 文件:
local old_score = redis.call('ZSCORE', KEYS[1], ARGV[2])
local new_score = old_score - ARGV[1]
return redis.call('ZADD', KEYS[1], new_score, ARGV[2])
然后通過以下命令來執行腳本:
$ redis-cli --eval zdecrby.lua salary , 300 peter
(integer) 0
這和在 redis-cli 里面執行 EVAL “local … ” 1 salary 300 peter 效果一樣,但先將腳本內容保存到文件里面,再執行腳本文件的做法,比起直接在客戶端里面一個個字輸入要容易一些。另外,這個腳本實現的 ZDECRBY 也比使用事務和樂觀鎖實現的 ZDECRBY 要簡單得多。
使用 EVALSHA 來減少網絡資源損耗
任何 Lua 腳本,只要被 EVAL 命令執行過一次,就會被儲存到服務器的腳本緩存里面,用戶只要通過EVALSHA 命令,指定被緩存腳本的 SHA1 值,就可以在不發送腳本的情況下,再次執行腳本:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
通過 SHA1 值來重用返回 ‘hello world’ 信息的腳本:
redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"
通過 SHA1 值來重用之前實現的 ZDECRBY 命令,這樣就不用每次都發送整個腳本了:
redis> EVALSHA 918130cae39ff0759b8256948742f77300a91cb2 1 salary 500 peter
(integer) 0
腳本管理命令
SCRIPT EXISTS sha1 [sha1 ...]
檢查 sha1 值所代表的腳本是否已經被加入到腳本緩存里面,是的話返回 1 ,不是的話返回 0 。
SCRIPT LOAD script
將腳本儲存到腳本緩存里面,等待將來 EVALSHA 使用。
SCRIPT FLUSH
清除腳本緩存儲存的所有腳本。
SCRIPT KILL
殺死運行超時的腳本。如果腳本已經執行過寫入操作,那么還需要使用 SHUTDOWN NOSAVE 命令來強制服務器不保存數據,以免錯誤的數據被保存到數據庫里面。
函數庫
Redis 在 Lua 環境里面載入了一些常用的函數庫,我們可以使用這些函數庫,直接在腳本里面處理數據,它們分別是標准庫:
• base 庫 :包含 Lua 的核心(core)函數,比如 assert、tostring、error、type 等。
• string 庫 :包含用於處理字符串的函數,比如 find、format、len、reverse 等。
• table 庫:包含用於處理表格的函數,比如 concat、insert、remove、sort 等。
• math 庫:包含常用的數學計算函數,比如 abs、sqrt、log 等。
• debug 庫:包含調試程序所需的函數,比如 sethook、gethook 等。
以及外部庫
• struct 庫:在 C 語言的結構和 Lua 語言的值之間進行轉換。
• cjson 庫:將 Lua 值轉換為 JSON 對象,或者將 JSON 對象轉換為 Lua 值。
• cmsgpack 庫:將 Lua 值編碼為 MessagePack 格式,或者從 MessagePack 格式里面解碼出 Lua值。
另外還有一個用於計算 sha1 值的外部函數 redis.sha1hex。