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