參考:https://blog.csdn.net/zjf280441589/article/details/52716720javascript
從 2.6版本 起, Redis 開始支持 Lua 腳本 讓開發者本身擴展 Redis.
本篇博客主要介紹了 Lua 語言不同的設計模型(相比於Java/C/C++、JS、PHP), 以及 Redis 對 Lua 的擴展, 最後結合 Lua 與 Redis 實現了一個支持過時時間的分佈式鎖. 咱們但願這篇博客的讀者朋友能夠在讀完這篇文字以後, 體會到 Lua 這門語言不同的設計哲學, 以及 更加駕輕就熟的使用/擴展 Redis.php
案例-實現訪問頻率限制: 實現訪問者 $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 保證腳本的原子性:html
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
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 中, 其餘客戶端可繼續使用.
redisson實現分佈式鎖的原理:java
參考:https://www.jianshu.com/p/de5a69622e49git
Lua是一種 便於嵌入應用程序 的腳本語言, 具有了做爲通用腳本語言的全部功能. 其高速虛擬機實現很是有名(Lua的垃圾回收頗有講究- 增量垃圾回收 ), 在不少虛擬機系性能評分中都取得了優異的成績. Home lua.org.
github
以嵌入式爲方針設計的Lua, 在默認狀態下簡潔得嚇人. 除了基本的數據類型外, 其餘一律沒有. 標註庫也就 Coroutine、String、Table、Math、 I/O、OS, 再加上Modules包加載而已. 參考: Lua 5.1 Reference Manual - Standard Libraries(中文版: Lua 5.1 參考手冊).redis
注: 本文僅介紹 Lua 不同凡響的設計模型(對比 Java/C/C++、JavaScript、Python 與 Go), 語言細節可參考文內和附錄推薦的文章以及Lua之父Roberto Ierusalimschy的<Programming in Lua>(中文版: <LUA程序設計(第2版)>)sql
nil
和 false
做爲布爾值的 false
, 數字 0
和空串(‘’
/‘\0’
)都是 true
;變量若是沒有特殊說明爲全局變量(那怕是語句塊 or 函數內), 局部變量前需加
local
關鍵字.shell
..
自動將數值轉換成字符串;'1' != 1
);在 Lua 中, 函數是和字符串、數值和表並列的基本數據結構, 屬於第一類對象( first-class-object /一等公民), 能夠和數值等其餘類型同樣賦給變量、做爲參數傳遞, 以及做爲返回值接收(閉包):數據庫
-- 全局函數: 求階乘 function fact(n) if n == 1 then return 1 else return n * fact(n - 1) end end -- 1. 賦給變量 local func = fact print("func type: " .. type(func), "fact type: " .. type(fact), "result: " .. func(4)) -- 2. 閉包 local function new_counter() local value = 0; return function() value = value + 1 return value end end local counter = new_counter() print(counter(), counter(), counter()) -- 3. 返回值相似Go/Python local random_func = function(param) return 9, 'a', true, "ƒ∂π", param end local var1, var2, var3, var4, var5 = random_func("no param is nil") print(var1, var2, var3, var4, var5) -- 4. 變數形參 local function square(...) local argv = { ... } for i = 1, #argv do argv[i] = argv[i] * argv[i] end return table.unpack(argv) end print(square(1, 2, 3))
Lua最具特點的數據類型就是表(Table), 能夠實現數組、Hash
、對象全部功能的萬能數據類型:
-- array local array = { 1, 2, 3 } print(array[1], #array) -- hash local hash = { x = 1, y = 2, z = 3 } print(hash.x, hash['y'], hash["z"], #hash) -- array & hash array['x'] = 8 print(array.x, #array)
1
開始;#
其’長度’只包括以(正)整數爲索引的數組元素._G
的table內:-- pairs會遍歷全部值不爲nil的索引, 與此相似的ipairs只會從索引1開始遞遍歷到最後一個值不爲nil的整數索引. for k, v in pairs(_G) do print(k, " -> ", v, " type: " .. type(v)) end
用
Hash
實現對象的還有JavaScript, 將數組和Hash
合二爲一的還有PHP.
Every value in Lua can have a metatable/元表. This metatable is an ordinary Lua table that defines the behavior of the original value under certain special operations. You can change several aspects of the behavior of operations over a value by setting specific fields in its metatable. For instance, when a non-numeric value is the operand of an addition, Lua checks for a function in the field 「__add」 of the value’s metatable. If it finds one, Lua calls this function to perform the addition.
The key for each event in a metatable is a string with the event name prefixed by two underscores__
; the corresponding values are called metamethods. In the previous example, the key is 「__add」 and the metamethod is the function that performs the addition.
metatable中的鍵名稱爲事件/event, 值稱爲元方法/metamethod, 咱們可經過getmetatable()
來獲取任一值的metatable, 也可經過setmetatable()
來替換table的metatable. Lua 事件一覽表:
對於這些操做, Lua 都將其關聯到 metatable 的事件Key, 當 Lua 須要對一個值發起這些操做時, 首先會去檢查其metatable中是否有對應的事件Key, 若是有則調用之以控制Lua解釋器做出響應.
MetaMethods主要用做一些相似C++中的運算符重載操做, 如重載+
運算符:
local frac_a = { numerator = 2, denominator = 3 } local frac_b = { numerator = 4, denominator = 8 } local operator = { __add = function(f1, f2) local ret = {} ret.numerator = f1.numerator * f2.denominator + f1.denominator * f2.numerator ret.denominator = f1.denominator * f2.denominator return ret end, __tostring = function(self) return "{ " .. self.numerator .. " ," .. self.denominator .. " }" end } setmetatable(frac_a, operator) setmetatable(frac_b, operator) local frac_res = frac_a + frac_b setmetatable(frac_res, operator) -- 使tostring()方法生效 print(tostring(frac_res))
Lua原本就不是設計爲一種面嚮對象語言, 所以其面向對象功能須要經過元表(metatable)這種很是怪異的方式實現, Lua並不直接支持面嚮對象語言中常見的類、對象和方法: 其
對象
和類
經過表
實現, 而方法
是經過函數
來實現.
上面的Event一覽表內咱們看到有__index
這個事件重載,這個東西主要是重載了find key
操做, 該操做可讓Lua變得有點面向對象的感受(相似JavaScript中的prototype). 經過Lua代碼模擬:
local function gettable_event(t, key) local h if type(t) == "table" then local value = rawget(t, key) if value ~= nil then return value end h = getmetatable(t).__index if h == nil then return nil end else h = getmetatable(t).__index if h == nil then error("error") end end if type(h) == "function" then -- call the handler return (h(t, key)) else -- or repeat opration on it return h[key] end end -- 測試 obj = { 1, 2, 3 } op = { x = function() return "xx" end } setmetatable(obj, { __index = op['x'] }) print(gettable_event(obj, x))
__
開頭的元素, 若是該元素爲函數, 則調用;table
來執行事件所對應的處理邏輯.這裏的代碼僅做模擬, 實際的行爲已經嵌入Lua解釋器, 執行效率要遠高於這些模擬代碼.
面向對象的基礎是建立對象和調用方法. Lua中, 表做爲對象使用, 所以建立對象沒有問題, 關於調用方法, 若是表元素爲函數的話, 則可直接調用:
-- 從obj取鍵爲x的值, 將之視爲function進行調用 obj.x(foo)
obj.x
這種調用方式, 只是將表obj
的屬性x
這個函數對象取出而已, 而在大多數面嚮對象語言中, 方法的實體位於類中, 而非單獨的對象中. 在JavaScript等基於原型的語言中, 是以原型對象來代替類進行方法的搜索, 所以每一個單獨的對象也並不擁有方法實體. 在Lua中, 爲了實現基於原型的方法搜索, 須要使用元表的__index
事件: a
和b
,想讓b
做爲a
的prototype須要setmetatable(a, {__index = b})
, 以下例: 爲obj
設置__index
加上proto
模板來建立另外一個實例:proto = {
x = function() print("x") end } local obj = {} setmetatable(obj, { __index = proto }) obj.x()
proto
變成了原型對象, 當obj
中不存在的屬性被引用時, 就會去搜索proto
.
this
得到, 而在Python中經過方法調用形式得到的並不是單純的函數對象, 而是一個「方法對象」 –其接收器會在內部做爲第一參數附在函數的調用過程當中. obj:x()
. 表示obj.x(obj)
, 也就是: 經過冒號記法調用的函數, 其接收器會被做爲第一參數添加進來(obj
的求值只會進行一次, 即便有反作用也只生效一次).-- 這個語法糖對定義也有效 function proto:y(param) print(self, param) end - Tips: 用冒號記法定義的方法, 調用時最好也用冒號記法, 避免參數錯亂 obj:y("parameter")
Lua雖然可以進行面向對象編程, 但用元表來實現, 彷彿把對象剖開看到五臟六腑同樣.
<代碼的將來>中松本行弘老師向咱們展現了一個基於原型編程的Lua庫, 經過該庫, 即便沒有深刻解Lua原始機制, 也能夠實現面向對象:
-- -- Author: Matz -- Date: 16/9/24 -- Time: 下午5:13 -- -- Object爲全部對象的上級 Object = {} -- 建立現有對象副本 function Object:clone() local object = {} -- 複製表元素 for k, v in pairs(self) do object[k] = v end -- 設定元表: 指定向自身`轉發` setmetatable(object, { __index = self }) return object end -- 基於類的編程 function Object:new(...) local object = {} -- 設定元表: 指定向自身`轉發` setmetatable(object, { __index = self }) -- 初始化 object:init(...) return object end -- 初始化實例 function Object:init(...) -- 默認不進行任何操做 end Class = Object:new()
另存爲prototype.lua, 使用時只需require()
引入便可:
require("prototype") -- Point類定義 Point = Class:new() function Point:init(x, y) self.x = x self.y = y end function Point:magnitude() return math.sqrt(self.x ^ 2 + self.y ^ 2) end -- 對象定義 point = Point:new(3, 4) print(point:magnitude()) -- 繼承: Point3D定義 Point3D = Point:clone() function Point3D:init(x, y, z) self.x = x self.y = y self.z = z end function Point3D:magnitude() return math.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ 2) end p3 = Point3D:new(1, 2, 3) print(p3:magnitude()) -- 建立p3副本 ap3 = p3:clone() print(ap3.x, ap3.y, ap3.z)
在傳入到Redis的Lua腳本中可以使用redis.call()
/redis.pcall()
函數調用Reids命令:
redis.call("set", "foo", "bar") local value = redis.call("get", "foo")
redis.call()
返回值就是Reids命令的執行結果, Redis回覆與Lua數據類型的對應關係以下:
Reids返回值類型 | Lua數據類型 |
---|---|
整數 | 數值 |
字符串 | 字符串 |
多行字符串 | 表(數組) |
狀態回覆 | 表(只有一個ok 字段存儲狀態信息) |
錯誤回覆 | 表(只有一個err 字段存儲錯誤信息) |
注: Lua 的
false
會轉化爲空結果.
redis-cli提供了EVAL
與EVALSHA
命令執行Lua腳本:
EVAL script numkeys key [key ...] arg [arg ...]
KEYS
和ARGV
兩個table訪問: KEYS
表示要操做的鍵名, ARGV
表示非鍵名參數(並不是強制).EVALSHA
命令容許經過腳本的SHA1來執行(節省帶寬), Redis在執行EVAL
/SCRIPT LOAD
後會計算腳本SHA1緩存, EVALSHA
根據SHA1取出緩存腳本執行.爲了在 Redis 服務器中執行 Lua 腳本, Redis 內嵌了一個 Lua 環境, 並對該環境進行了一系列修改, 從而確保知足 Redis 的須要. 其建立步驟以下:
loadfile()
函數)、Table、String、Math、Debug等標準庫, 以及CJSON、 Struct(用於Lua值與C結構體轉換)、 cmsgpack等擴展庫(Redis 禁用Lua標準庫中與文件或系統調用相關函數, 只容許對 Redis 數據處理).redis
, 其包含了對 Redis 操做的函數, 如redis.call()
、 redis.pcall()
等;math.random()
和 math.randomseed()
.set
集合無序, 所以即便兩個集合內元素相同, 其輸出結果也並不同), 這類命令包括SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS、KEYS 等. __redis__compare_helper
, 當執行完以上命令後, Redis會調用table.sort()
以__redis__compare_helper
做爲輔助函數對命令返回值排序.__redis__err__handler
錯誤處理函數, 當調用 redis.pcall()
執行 Redis 命令出錯時, 該函數將打印異常詳細信息.當心: Redis 並未禁止用戶修改已存在的全局變量.
lua
屬性與Lua環境的關聯: Redis建立兩個用於與Lua環境協做的組件: 僞客戶端- 負責執行 Lua 腳本中的 Redis 命令, lua_scripts
字典- 保存 Lua 腳本:
redis.call()
/redis.pcall()
執行一個Redis命令步驟以下: lua_scripts
字典 EVAL
和SCRIPT LOAD
載入過的腳本都被記錄到 lua_scripts
中, 便於實現 SCRIPT EXISTS
命令和腳本複製功能.EVAL
命令執行分爲如下三個步驟:
定義Lua函數:
在 Lua 環境內定義 Lua函數 : 名爲f_
前綴+腳本 SHA1 校驗和, 體爲腳本內容自己. 優點:
EVALSHA
命令實現).將腳本保存到lua_scripts
字典;
EVAL
傳入的鍵名和參數分別保存到KEYS
和ARGV
, 而後將這兩個數組做爲全局變量傳入到Lua環境; hook
(handler
), 可在腳本出現運行超時時讓經過SCRIPT KILL
中止腳本, 或SHUTDOWN
關閉Redis; hook
; 對於會產生隨機結果但沒法排序的命令(如只產生一個元素, 如 SPOP、SRANDMEMBER、RANDOMKEY、TIME), Redis在這類命令執行後將腳本狀態置爲
lua_random_dirty
, 此後只容許腳本調用只讀命令, 不容許修改數據庫值.
使用Lua腳本從新構建帶有過時時間的分佈式鎖.
案例來源: <Redis實戰> 第六、11章, 構建步驟:
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; }
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
local key = KEYS[1] local identifier = ARGV[1] if redis.call("GET", key) == identifier then redis.call("DEL", key) return 1 end return 0
/** * @author jifang * @since 16/8/25 下午3:35. */ public class ScriptCaller { private static final ConcurrentMap<String, String> SHA_CACHE = new ConcurrentHashMap<>(); private String script; private ScriptCaller(String script) { this.script = script; } public static ScriptCaller getInstance(String script) { return new ScriptCaller(script); } public Object call(Jedis connection, List<String> keys, List<String> argv, boolean forceEval) { if (!forceEval) { String sha = SHA_CACHE.get(this.script); if (Strings.isNullOrEmpty(sha)) { // load 腳本獲得 sha1 緩存 sha = connection.scriptLoad(this.script); SHA_CACHE.put(this.script, sha); } return connection.evalsha(sha, keys, argv); } return connection.eval(script, keys, argv); } }
public class Client { private ScriptCaller acquireCaller = ScriptCaller.getInstance( "local key = KEYS[1]\n" + "local identifier = ARGV[1]\n" + "local lockTimeOut = ARGV[2]\n" + "\n" + "if redis.call(\"SETNX\", key, identifier) == 1 then\n" + " redis.call(\"EXPIRE\", key, lockTimeOut)\n" + " return 1\n" + "elseif redis.call(\"TTL\", key) == -1 then\n" + " redis.call(\"EXPIRE\", key, lockTimeOut)\n" + "end\n" + "return 0" ); private ScriptCaller releaseCaller = ScriptCaller.getInstance( "local key = KEYS[1]\n" + "local identifier = ARGV[1]\n" + "\n" + "if redis.call(\"GET\", key) == identifier then\n" + " redis.call(\"DEL\", key)\n" + " return 1\n" + "end\n" + "return 0" ); @Test public void client() { Jedis jedis = new Jedis("127.0.0.1", 9736); String identifier = acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300); System.out.println(releaseLock(jedis, "ret1", identifier)); } String acquireLockWithTimeOut(Jedis connection, String lockName, long acquireTimeOut, int lockTimeOut) { String identifier = UUID.randomUUID().toString(); List<String> keys = Collections.singletonList("lock:" + lockName); List<String> argv = Arrays.asList(identifier, String.valueOf(lockTimeOut)); long acquireTimeEnd = System.currentTimeMillis() + acquireTimeOut; boolean acquired = false; while (!acquired && (System.currentTimeMillis() < acquireTimeEnd)) { if (1 == (long) acquireCaller.call(connection, keys, argv, false)) { acquired = true; } else { try { Thread.sleep(10); } catch (InterruptedException ignored) { } } } return acquired ? identifier : null; } boolean releaseLock(Jedis connection, String lockName, String identifier) { List<String> keys = Collections.singletonList("lock:" + lockName); List<String> argv = Collections.singletonList(identifier); return 1 == (long) releaseCaller.call(connection, keys, argv, true); } }