Redis 腳本使用 Lua 解釋器來執行腳本。 Reids 2.6 版本經過內嵌支持 Lua 環境。執行腳本的經常使用命令爲 EVAL。html
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 ...] 執行 Lua 腳本。
2 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 執行 Lua 腳本。
3 SCRIPT EXISTS script [script ...] 查看指定的腳本是否已經被保存在緩存當中。
4 SCRIPT FLUSH 從腳本緩存中移除全部腳本。
5 SCRIPT KILL 殺死當前正在運行的 Lua 腳本。
6 SCRIPT LOAD script 將腳本 script 添加到腳本緩存中,但並不當即執行這個腳本。redis
Redis Eval 命令使用 Lua 解釋器執行腳本。算法
EVAL script numkeys key [key ...] arg [arg ...]
參數說明
script: 參數是一段 Lua 5.1 腳本程序。腳本沒必要(也不該該)定義爲一個 Lua 函數。
numkeys: 用於指定鍵名參數的個數。
key [key ...]: 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key),這些鍵名參數能夠在 Lua 中經過全局變量 KEYS 數組,用 1 爲基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推)。
arg [arg ...]: 附加參數,在 Lua 中經過全局變量 ARGV 數組訪問,訪問的形式和 KEYS 變量相似( ARGV[1] 、 ARGV[2] ,諸如此類)。編程
Redis Evalsha 命令根據給定的 sha1 校驗碼,執行緩存在服務器中的腳本。
EVALSHA sha1 numkeys key [key ...] arg [arg ...]json
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 命令用於校驗指定的腳本是否已經被保存在緩存當中。
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 添加到腳本緩存中,但並不當即執行這個腳本。緩存
這是從一個Lua腳本中使用兩個不一樣的Lua函數來調用Redis的命令的例子:服務器
redis.call() redis.pcall()
redis.call() 與 redis.pcall()很相似, 他們惟一的區別是當redis命令執行結果返回錯誤時, redis.call()將返回給調用者一個錯誤,而redis.pcall()會將捕獲的錯誤以Lua表的形式返回
redis.call() 和 redis.pcall() 兩個函數的參數能夠是任意的 Redis 命令:網絡
> eval "return redis.call('set','foo','bar')" 0 OK
須要注意的是,上面這段腳本的確實現了將鍵 foo 的值設爲 bar 的目的,可是,它違反了 EVAL 命令的語義,由於腳本里使用的全部鍵都應該由 KEYS 數組來傳遞
,就像這樣:數據結構
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
要求使用正確的形式來傳遞鍵(key)是有緣由的,**由於不只僅是 EVAL 這個命令,全部的 Redis 命令,在執行以前都會被分析,籍此來肯定命令會對哪些鍵進行操做。
所以,對於 EVAL 命令來講,必須使用正確的形式來傳遞鍵,才能確保分析工做正確地執行。 **
當 Lua 經過 call() 或 pcall() 函數執行 Redis 命令的時候,命令的返回值會被轉換成 Lua 數據結構。 一樣地,當 Lua 腳本在 Redis 內置的解釋器裏運行時,Lua 腳本的返回值也會被轉換成 Redis 協議(protocol),而後由 EVAL 將值返回給客戶端。
下面兩點須要重點注意:
lua中整數和浮點數之間沒有什麼區別。所以,咱們始終將Lua的數字轉換成整數的回覆,這樣將捨去小數部分。若是你想從Lua返回一個浮點數,你應該將它做爲一個字符串
有兩個輔助函數從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)的方式執行: 當某個腳本正在運行的時候,不會有其餘腳本或 Redis 命令被執行。 這和使用 MULTI / EXEC 包圍的事務很相似。 在其餘別的客戶端看來,腳本的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。
EVAL 命令要求你在每次執行腳本的時候都發送一次腳本主體(script body)。Redis 有一個內部的腳本緩存機制,所以它不會每次都從新編譯腳本。
EVALSHA 命令,它的做用和 EVAL 同樣,都用於對腳本求值,但它接受的第一個參數不是腳本,而是腳本的 SHA1 校驗和(sum)。
客戶端庫的底層實現能夠一直樂觀地使用 EVALSHA 來代替 EVAL ,並指望着要使用的腳本已經保存在服務器上了,只有當 NOSCRIPT 錯誤發生時,才使用 EVAL 命令從新發送腳本,這樣就能夠最大限度地節省帶寬。
刷新腳本緩存的惟一辦法是顯式地調用 SCRIPT FLUSH 命令,這個命令會清空運行過的全部腳本的緩存。一般只有在雲計算環境中,纔會執行這個命令。
不能訪問系統時間或者其餘內部狀態
Redis 會返回一個錯誤,阻止這樣的腳本運行: 這些腳本在執行隨機命令以後(好比 RANDOMKEY 、 SRANDMEMBER 或 TIME 等),還會執行能夠修改數據集的 Redis 命令。若是腳本只是執行只讀操做,那麼就沒有這一限制。
每當從 Lua 腳本中調用那些返回無序元素的命令時,執行命令所得的數據在返回給 Lua 以前會先執行一個靜默(slient)的字典序排序(lexicographical sorting)。舉個例子,由於 Redis 的 Set 保存的是無序的元素,因此在 Redis 命令行客戶端中直接執行 SMEMBERS ,返回的元素是無序的,可是,假如在腳本中執行 redis.call(「smembers」, KEYS[1]) ,那麼返回的老是排過序的元素。
對 Lua 的僞隨機數生成函數 math.random 和 math.randomseed 進行修改,使得每次在運行新腳本的時候,老是擁有一樣的 seed 值。這意味着,每次運行腳本時,只要不使用 math.randomseed ,那麼 math.random 產生的隨機數序列老是相同的。
全局變量保護,爲了防止沒必要要的數據泄漏進 Lua 環境, Redis 腳本不容許建立全局變量
。若是一個腳本須要在屢次執行之間維持某種狀態,它應該使用 Redis key 來進行狀態保存。避免引入全局變量的一個訣竅是:將腳本中用到的全部變量都使用 local
關鍵字定義爲局部變量。
Redis Lua解釋器可用加載如下Lua庫:
base
table
string
math
debug
struct 一個Lua裝箱/拆箱的庫
cjson 爲Lua提供極快的JSON處理
cmsgpack爲Lua提供了簡單、快速的MessagePack操縱
bitop 爲Lua的位運算模塊增長了按位操做數。
redis.sha1hex function. 對字符串執行SHA1算法
每個Redis實例都擁有以上的全部類庫,以確保您使用腳本的環境都是同樣的。
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 "\x93\xa3foo\xa3bar\xa3baz" 127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz" 1) "foo" 2) "bar" 3) "baz"
在 Lua 腳本中,能夠經過調用 redis.log 函數來寫 Redis 日誌(log):
redis.log(loglevel,message)
其中, message 參數是一個字符串,而 loglevel 參數能夠是如下任意一個值:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
上面的這些等級(level)和標準 Redis 日誌的等級相對應。
只有那些和當前 Redis 實例所設置的日誌等級相同或更高級的日誌纔會被散發。
如下是一個日誌示例:
redis.log(redis.LOG_WARNING, "Something is wrong with this script.") 執行上面的函數會產生這樣的信息: [32343] 22 Mar 15:21:39 # Something is wrong with this script.
腳本應該僅僅用於傳遞參數和對 Redis 數據進行處理,它不該該嘗試去訪問外部系統(好比文件系統),或者執行任何系統調用。
除此以外,腳本還有一個最大執行時間限制,它的默認值是 5 秒鐘,通常正常運做的腳本一般能夠在幾分之幾毫秒以內完成,花不了那麼多時間,這個限制主要是爲了防止因編程錯誤而形成的無限循環而設置的。
最大執行時間的長短由 lua-time-limit
選項來控制(以毫秒爲單位),能夠經過編輯 redis.conf 文件或者使用 CONFIG GET 和 CONFIG SET 命令來修改它。
當一個腳本達到最大執行時間的時候,它並不會自動被 Redis 結束,由於 Redis 必須保證腳本執行的原子性,而中途中止腳本的運行意味着可能會留下未處理完的數據在數據集(data set)裏面。
所以,當腳本運行的時間超過最大執行時間後,如下動做會被執行:
Redis 記錄一個腳本正在超時運行
Redis 開始從新接受其餘客戶端的命令請求,可是隻有 SCRIPT KILL 和 SHUTDOWN NOSAVE 兩個命令會被處理,對於其餘命令請求, Redis 服務器只是簡單地返回 BUSY 錯誤。
可使用 SCRIPT KILL
命令將一個僅執行只讀命令的腳本殺死,由於只讀命令並不修改數據,所以殺死這個腳本並不破壞數據的完整性
若是腳本已經執行過寫命令,那麼惟一容許執行的操做就是 SHUTDOWN NOSAVE
,它經過中止服務器來阻止當前數據集寫入磁盤
一旦在pipeline中由於 EVALSHA 命令而發生 NOSCRIPT 錯誤,那麼這個pipeline就再也沒有辦法從新執行了,不然的話,命令的執行順序就會被打亂。
爲了防止出現以上所說的問題,客戶端庫實現應該實施如下的其中一項措施:
老是在pipeline中使用 EVAL 命令
檢查pipeline中要用到的全部命令,找到其中的 EVAL 命令,並使用 SCRIPT EXISTS
命令檢查要用到的腳本是否是全都已經保存在緩存裏面了。若是所需的所有腳本均可以在緩存裏找到,那麼就能夠放心地將全部 EVAL 命令改爲 EVALSHA 命令,不然的話,就要在pipeline的頂端(top)將缺乏的腳本用 SCRIPT LOAD
命令加上去。
實現訪問者 $ip 在必定的時間 $time 內只能訪問 $limit 次.
非腳本實現
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; }
以上代碼有兩點缺陷
可能會出現競態條件: 解決方法是用 WATCH 監控 rate.limit:$IP 的變更, 但較爲麻煩;
以上代碼在不使用 pipeline 的狀況下最多須要向Redis請求5條指令, 傳輸過多.
Lua腳本實現
Redis 容許將 Lua 腳本傳到 Redis 服務器中執行, 腳本內能夠調用大部分 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 { List<String> keys = Collections.singletonList(ip); List<String> 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 優點:
減小網絡開銷: 不使用 Lua 的代碼須要向 Redis 發送屢次請求, 而腳本只需一次便可, 減小網絡傳輸;
原子操做: Redis 將整個腳本做爲一個原子執行, 無需擔憂併發, 也就無需事務;
複用: 腳本會永久保存 Redis 中, 其餘客戶端可繼續使用.
案例來源: < Redis實戰 > 第六、11章, 構建步驟:
鎖申請
首先嚐試加鎖:
成功則爲鎖設定過時時間; 返回;
失敗檢測鎖是否添加了過時時間;
wait.
鎖釋放
檢查當前線程是否真的持有了該鎖:
持有: 則釋放; 返回成功;
失敗: 返回失敗.
非Lua實現
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腳本實現
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...