標籤:os ar 使用 for strong 檔案 資料 on 問題
背景介紹
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。
redis之lua指令碼