摘要:命令使用解釋器執(zhí)行腳本。命令根據(jù)給定的校驗碼,執(zhí)行緩存在服務器中的腳本。命令用于校驗指定的腳本是否已經(jīng)被保存在緩存當中。殺死當前正在運行的腳本。全局變量保護,為了防止不必要的數(shù)據(jù)泄漏進環(huán)境,腳本不允許創(chuàng)建全局變量。
基本命令
Redis 腳本使用 Lua 解釋器來執(zhí)行腳本。 Reids 2.6 版本通過內(nèi)嵌支持 Lua 環(huán)境。執(zhí)行腳本的常用命令為 EVAL。
EVAL script numkeys key [key ...] arg [arg ...] EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
1 EVAL script numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
2 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 執(zhí)行 Lua 腳本。
3 SCRIPT EXISTS script [script ...] 查看指定的腳本是否已經(jīng)被保存在緩存當中。
4 SCRIPT FLUSH 從腳本緩存中移除所有腳本。
5 SCRIPT KILL 殺死當前正在運行的 Lua 腳本。
6 SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但并不立即執(zhí)行這個腳本。
Redis Eval 命令使用 Lua 解釋器執(zhí)行腳本。
EVAL script numkeys key [key ...] arg [arg ...]
參數(shù)說明
script: 參數(shù)是一段 Lua 5.1 腳本程序。腳本不必(也不應該)定義為一個 Lua 函數(shù)。
numkeys: 用于指定鍵名參數(shù)的個數(shù)。
key [key ...]: 從 EVAL 的第三個參數(shù)開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數(shù)可以在 Lua 中通過全局變量 KEYS 數(shù)組,用 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
arg [arg ...]: 附加參數(shù),在 Lua 中通過全局變量 ARGV 數(shù)組訪問,訪問的形式和 KEYS 變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。
Redis Evalsha 命令根據(jù)給定的 sha1 校驗碼,執(zhí)行緩存在服務器中的腳本。
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
redis 127.0.0.1:6379> SCRIPT LOAD "return "hello moto"" "232fd51614574cf0867b83d384a5e898cfd24e5a" redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0 "hello moto"
Redis Script Exists 命令用于校驗指定的腳本是否已經(jīng)被保存在緩存當中。
SCRIPT EXISTS script [script ...]
redis 127.0.0.1:6379> SCRIPT LOAD "return "hello moto"" # 載入一個腳本 "232fd51614574cf0867b83d384a5e898cfd24e5a" redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 1 redis 127.0.0.1:6379> SCRIPT FLUSH # 清空緩存 OK redis 127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 0
SCRIPT FLUSH 從腳本緩存中移除所有腳本。
SCRIPT KILL 殺死當前正在運行的 Lua 腳本。
SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但并不立即執(zhí)行這個腳本。
這是從一個Lua腳本中使用兩個不同的Lua函數(shù)來調(diào)用Redis的命令的例子:
redis.call() redis.pcall()
redis.call() 與 redis.pcall()很類似, 他們唯一的區(qū)別是當redis命令執(zhí)行結(jié)果返回錯誤時, redis.call()將返回給調(diào)用者一個錯誤,而redis.pcall()會將捕獲的錯誤以Lua表的形式返回
redis.call() 和 redis.pcall() 兩個函數(shù)的參數(shù)可以是任意的 Redis 命令:
> eval "return redis.call("set","foo","bar")" 0 OK
需要注意的是,上面這段腳本的確實現(xiàn)了將鍵 foo 的值設為 bar 的目的,但是,它違反了 EVAL 命令的語義,因為腳本里使用的所有鍵都應該由 KEYS 數(shù)組來傳遞,就像這樣:
> eval "return redis.call("set",KEYS[1],"bar")" 1 foo OK
要求使用正確的形式來傳遞鍵(key)是有原因的,**因為不僅僅是 EVAL 這個命令,所有的 Redis 命令,在執(zhí)行之前都會被分析,籍此來確定命令會對哪些鍵進行操作。
因此,對于 EVAL 命令來說,必須使用正確的形式來傳遞鍵,才能確保分析工作正確地執(zhí)行。 **
當 Lua 通過 call() 或 pcall() 函數(shù)執(zhí)行 Redis 命令的時候,命令的返回值會被轉(zhuǎn)換成 Lua 數(shù)據(jù)結(jié)構(gòu)。 同樣地,當 Lua 腳本在 Redis 內(nèi)置的解釋器里運行時,Lua 腳本的返回值也會被轉(zhuǎn)換成 Redis 協(xié)議(protocol),然后由 EVAL 將值返回給客戶端。
下面兩點需要重點注意:
lua中整數(shù)和浮點數(shù)之間沒有什么區(qū)別。因此,我們始終將Lua的數(shù)字轉(zhuǎn)換成整數(shù)的回復,這樣將舍去小數(shù)部分。如果你想從Lua返回一個浮點數(shù),你應該將它作為一個字符串
有兩個輔助函數(shù)從Lua返回Redis的類型。
redis.error_reply(error_string) returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
redis.status_reply(status_string) returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.
return {err="My Error"} return redis.error_reply("My Error")腳本的原子性
Redis 使用單個 Lua 解釋器去運行所有腳本,并且, Redis 也保證腳本會以原子性(atomic)的方式執(zhí)行: 當某個腳本正在運行的時候,不會有其他腳本或 Redis 命令被執(zhí)行。 這和使用 MULTI / EXEC 包圍的事務很類似。 在其他別的客戶端看來,腳本的效果(effect)要么是不可見的(not visible),要么就是已完成的(already completed)。
腳本緩存和 EVALSHAEVAL 命令要求你在每次執(zhí)行腳本的時候都發(fā)送一次腳本主體(script body)。Redis 有一個內(nèi)部的腳本緩存機制,因此它不會每次都重新編譯腳本。
EVALSHA 命令,它的作用和 EVAL 一樣,都用于對腳本求值,但它接受的第一個參數(shù)不是腳本,而是腳本的 SHA1 校驗和(sum)。
客戶端庫的底層實現(xiàn)可以一直樂觀地使用 EVALSHA 來代替 EVAL ,并期望著要使用的腳本已經(jīng)保存在服務器上了,只有當 NOSCRIPT 錯誤發(fā)生時,才使用 EVAL 命令重新發(fā)送腳本,這樣就可以最大限度地節(jié)省帶寬。
刷新腳本緩存的唯一辦法是顯式地調(diào)用 SCRIPT FLUSH 命令,這個命令會清空運行過的所有腳本的緩存。通常只有在云計算環(huán)境中,才會執(zhí)行這個命令。
不能訪問系統(tǒng)時間或者其他內(nèi)部狀態(tài)
Redis 會返回一個錯誤,阻止這樣的腳本運行: 這些腳本在執(zhí)行隨機命令之后(比如 RANDOMKEY 、 SRANDMEMBER 或 TIME 等),還會執(zhí)行可以修改數(shù)據(jù)集的 Redis 命令。如果腳本只是執(zhí)行只讀操作,那么就沒有這一限制。
每當從 Lua 腳本中調(diào)用那些返回無序元素的命令時,執(zhí)行命令所得的數(shù)據(jù)在返回給 Lua 之前會先執(zhí)行一個靜默(slient)的字典序排序(lexicographical sorting)。舉個例子,因為 Redis 的 Set 保存的是無序的元素,所以在 Redis 命令行客戶端中直接執(zhí)行 SMEMBERS ,返回的元素是無序的,但是,假如在腳本中執(zhí)行 redis.call(“smembers”, KEYS[1]) ,那么返回的總是排過序的元素。
對 Lua 的偽隨機數(shù)生成函數(shù) math.random 和 math.randomseed 進行修改,使得每次在運行新腳本的時候,總是擁有同樣的 seed 值。這意味著,每次運行腳本時,只要不使用 math.randomseed ,那么 math.random 產(chǎn)生的隨機數(shù)序列總是相同的。
全局變量保護,為了防止不必要的數(shù)據(jù)泄漏進 Lua 環(huán)境, Redis 腳本不允許創(chuàng)建全局變量。如果一個腳本需要在多次執(zhí)行之間維持某種狀態(tài),它應該使用 Redis key 來進行狀態(tài)保存。避免引入全局變量的一個訣竅是:將腳本中用到的所有變量都使用 local 關鍵字定義為局部變量。
可用庫Redis Lua解釋器可用加載以下Lua庫:
base
table
string
math
debug
struct 一個Lua裝箱/拆箱的庫
cjson 為Lua提供極快的JSON處理
cmsgpack為Lua提供了簡單、快速的MessagePack操縱
bitop 為Lua的位運算模塊增加了按位操作數(shù)。
redis.sha1hex function. 對字符串執(zhí)行SHA1算法
每一個Redis實例都擁有以上的所有類庫,以確保您使用腳本的環(huán)境都是一樣的。
struct, CJSON 和 cmsgpack 都是外部庫, 所有其他庫都是標準。
redis 127.0.0.1:6379> eval "return cjson.encode({["foo"]= "bar"})" 0 "{"foo":"bar"}" redis 127.0.0.1:6379> eval "return cjson.decode(ARGV[1])["foo"]" 0 "{"foo":"bar"}" "bar" 127.0.0.1:6379> eval "return cmsgpack.pack({"foo", "bar", "baz"})" 0 "x93xa3fooxa3barxa3baz" 127.0.0.1:6379> eval "return cmsgpack.unpack(ARGV[1])" 0 "x93xa3fooxa3barxa3baz" 1) "foo" 2) "bar" 3) "baz"使用腳本記錄Redis 日志
在 Lua 腳本中,可以通過調(diào)用 redis.log 函數(shù)來寫 Redis 日志(log):
redis.log(loglevel,message)
其中, message 參數(shù)是一個字符串,而 loglevel 參數(shù)可以是以下任意一個值:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
上面的這些等級(level)和標準 Redis 日志的等級相對應。
只有那些和當前 Redis 實例所設置的日志等級相同或更高級的日志才會被散發(fā)。
以下是一個日志示例:
redis.log(redis.LOG_WARNING, "Something is wrong with this script.") 執(zhí)行上面的函數(shù)會產(chǎn)生這樣的信息: [32343] 22 Mar 15:21:39 # Something is wrong with this script.沙箱(sandbox)和最大執(zhí)行時間
腳本應該僅僅用于傳遞參數(shù)和對 Redis 數(shù)據(jù)進行處理,它不應該嘗試去訪問外部系統(tǒng)(比如文件系統(tǒng)),或者執(zhí)行任何系統(tǒng)調(diào)用。
除此之外,腳本還有一個最大執(zhí)行時間限制,它的默認值是 5 秒鐘,一般正常運作的腳本通??梢栽趲追种畮缀撩胫畠?nèi)完成,花不了那么多時間,這個限制主要是為了防止因編程錯誤而造成的無限循環(huán)而設置的。
最大執(zhí)行時間的長短由 lua-time-limit 選項來控制(以毫秒為單位),可以通過編輯 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令來修改它。
當一個腳本達到最大執(zhí)行時間的時候,它并不會自動被 Redis 結(jié)束,因為 Redis 必須保證腳本執(zhí)行的原子性,而中途停止腳本的運行意味著可能會留下未處理完的數(shù)據(jù)在數(shù)據(jù)集(data set)里面。
因此,當腳本運行的時間超過最大執(zhí)行時間后,以下動作會被執(zhí)行:
Redis 記錄一個腳本正在超時運行
Redis 開始重新接受其他客戶端的命令請求,但是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個命令會被處理,對于其他命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤。
可以使用 SCRIPT KILL 命令將一個僅執(zhí)行只讀命令的腳本殺死,因為只讀命令并不修改數(shù)據(jù),因此殺死這個腳本并不破壞數(shù)據(jù)的完整性
如果腳本已經(jīng)執(zhí)行過寫命令,那么唯一允許執(zhí)行的操作就是 SHUTDOWN NOSAVE ,它通過停止服務器來阻止當前數(shù)據(jù)集寫入磁盤
一旦在pipeline中因為 EVALSHA 命令而發(fā)生 NOSCRIPT 錯誤,那么這個pipeline就再也沒有辦法重新執(zhí)行了,否則的話,命令的執(zhí)行順序就會被打亂。
為了防止出現(xiàn)以上所說的問題,客戶端庫實現(xiàn)應該實施以下的其中一項措施:
總是在pipeline中使用 EVAL 命令
檢查pipeline中要用到的所有命令,找到其中的 EVAL 命令,并使用 SCRIPT EXISTS 命令檢查要用到的腳本是不是全都已經(jīng)保存在緩存里面了。如果所需的全部腳本都可以在緩存里找到,那么就可以放心地將所有 EVAL 命令改成 EVALSHA 命令,否則的話,就要在pipeline的頂端(top)將缺少的腳本用 SCRIPT LOAD 命令加上去。
案例1-實現(xiàn)訪問頻率限制:實現(xiàn)訪問者 $ip 在一定的時間 $time 內(nèi)只能訪問 $limit 次.
非腳本實現(xiàn)
private boolean accessLimit(String ip, int limit, int time, Jedis jedis) { boolean result = true; String key = "rate.limit:" + ip; if (jedis.exists(key)) { long afterValue = jedis.incr(key); if (afterValue > limit) { result = false; } } else { Transaction transaction = jedis.multi(); transaction.incr(key); transaction.expire(key, time); transaction.exec(); } return result; }
以上代碼有兩點缺陷
可能會出現(xiàn)競態(tài)條件: 解決方法是用 WATCH 監(jiān)控 rate.limit:$IP 的變動, 但較為麻煩;
以上代碼在不使用 pipeline 的情況下最多需要向Redis請求5條指令, 傳輸過多.
Lua腳本實現(xiàn)
Redis 允許將 Lua 腳本傳到 Redis 服務器中執(zhí)行, 腳本內(nèi)可以調(diào)用大部分 Redis 命令, 且 Redis 保證腳本的 原子性 :
首先需要準備Lua代碼: script.lua
local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end
Java
private boolean accessLimit(String ip, int limit, int timeout, Jedis connection) throws IOException { Listkeys = Collections.singletonList(ip); List argv = Arrays.asList(String.valueOf(limit), String.valueOf(timeout)); return 1 == (long) connection.eval(loadScriptString("script.lua"), keys, argv); } // 加載Lua代碼 private String loadScriptString(String fileName) throws IOException { Reader reader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName)); return CharStreams.toString(reader); }
Lua 嵌入 Redis 優(yōu)勢:
減少網(wǎng)絡開銷: 不使用 Lua 的代碼需要向 Redis 發(fā)送多次請求, 而腳本只需一次即可, 減少網(wǎng)絡傳輸;
原子操作: Redis 將整個腳本作為一個原子執(zhí)行, 無需擔心并發(fā), 也就無需事務;
復用: 腳本會永久保存 Redis 中, 其他客戶端可繼續(xù)使用.
案例2-使用Lua腳本重新構(gòu)建帶有過期時間的分布式鎖.案例來源: < Redis實戰(zhàn) > 第6、11章, 構(gòu)建步驟:
鎖申請
首先嘗試加鎖:
成功則為鎖設定過期時間; 返回;
失敗檢測鎖是否添加了過期時間;
wait.
鎖釋放
檢查當前線程是否真的持有了該鎖:
持有: 則釋放; 返回成功;
失敗: 返回失敗.
非Lua實現(xiàn)
String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) { String identifier = UUID.randomUUID().toString(); String key = "lock:" + lockName; long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut; while (System.currentTimeMillis() < acquireTimeEnd) { // 獲取鎖并設置過期時間 if (connection.setnx(key, identifier) != 0) { connection.expire(key, lockTimeOut); return identifier; } // 檢查過期時間, 并在必要時對其更新 else if (connection.ttl(key) == -1) { connection.expire(key, lockTimeOut); } try { Thread.sleep(10); } catch (InterruptedException ignored) { } } return null; } boolean releaseLock(Jedis connection, String lockName, String identifier) { String key = "lock:" + lockName; connection.watch(key); // 確保當前線程還持有鎖 if (identifier.equals(connection.get(key))) { Transaction transaction = connection.multi(); transaction.del(key); return transaction.exec().isEmpty(); } connection.unwatch(); return false; }
Lua腳本實現(xiàn)
Lua腳本: acquire
local key = KEYS[1] local identifier = ARGV[1] local lockTimeOut = ARGV[2] -- 鎖定成功 if redis.call("SETNX", key, identifier) == 1 then redis.call("EXPIRE", key, lockTimeOut) return 1 elseif redis.call("TTL", key) == -1 then redis.call("EXPIRE", key, lockTimeOut) end return 0
Lua腳本: release
local key = KEYS[1] local identifier = ARGV[1] if redis.call("GET", key) == identifier then redis.call("DEL", key) return 1 end return 0
參考:http://www.redis.cn/commands/...
http://www.redis.net.cn/tutor...
http://www.oschina.net/transl...
http://www.tuicool.com/articl...
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/38307.html
摘要:采取兩種實現(xiàn)命令其一類盡量堅持官方語法,但是以下除外沒有實現(xiàn),應該是線程安全的原因。線程安全性是線程安全的。由于線程安全原因,不提供實現(xiàn),因為它會導致數(shù)據(jù)庫的切換。 官網(wǎng):https://github.com/andymccurd...當前版本:2.10.5注:這不是完整翻譯,只提取了關鍵信息。省略了部分內(nèi)容,如lua腳本支持。 pip install redis pip instal...
閱讀 1186·2021-11-08 13:13
閱讀 1765·2019-08-30 15:55
閱讀 2830·2019-08-29 11:26
閱讀 2491·2019-08-26 13:56
閱讀 2616·2019-08-26 12:15
閱讀 2195·2019-08-26 11:41
閱讀 1453·2019-08-26 11:00
閱讀 1583·2019-08-23 18:30