Lua 語言模型與 Redis 應用

hm8617 8年前發布 | 31K 次閱讀 Lua Redis 語言模型 Lua開發

從 2.6 版本起,Redis 開始支持 Lua 腳本,可以讓開發者自己擴展 Redis。本文主要介紹了 Lua 語言不一樣的設計模型(相比于Java/C/C++、JS、PHP),以及 Redis 對 Lua 的擴展,最后結合 Lua 與 Redis 實現了一個支持過期時間的分布式鎖。希望讀者可以在讀完后, 體會到 Lua 這門語言不一樣的設計哲學,可以更加得心應手地使用/擴展 Redis。

案例-實現訪問頻率限制: 實現訪問者 $ip 在一定的時間 $time 內只能訪問 $limit 次.

  • 非腳本實現
private boolean accessLimit(String ip, int limit, int time, Jedisjedis) {
    boolean result = true;
 
    String key = "rate.limit:" + ip;
    if (jedis.exists(key)) {
        long afterValue = jedis.incr(key);
        if (afterValue > limit) {
            result = false;
        }
    } else {
        Transactiontransaction = jedis.multi();
        transaction.incr(key);
        transaction.expire(key, time);
        transaction.exec();
    }
    return result;
}
  • 以上代碼有兩點缺陷

    1. 可能會出現競態條件: 解決方法是用 WATCH 監控 rate.limit:$IP 的變動, 但較為麻煩;

    2. 以上代碼在不使用 pipeline 的情況下最多需要向Redis請求5條指令, 傳輸過多.

  • Lua腳本實現

    Redis 允許將 Lua 腳本傳到 Redis 服務器中執行, 腳本內可以調用大部分 Redis 命令, 且 Redis 保證腳本的 原子性:

    • 首先需要準備Lua代碼: script.lua

--
-- CreatedbyIntelliJIDEA.
-- User: jifang
-- Date: 16/8/24
-- Time: 下午6:11
--
 
localkey = "rate.limit:" .. KEYS[1]
locallimit = tonumber(ARGV[1])
localexpire_time = ARGV[2]
 
localis_exists = redis.call("EXISTS", key)
if is_exists == 1 then
    if redis.call("INCR", key) > limitthen
        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, Jedisconnection) 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 {
    Readerreader = new InputStreamReader(Client.class.getClassLoader().getResourceAsStream(fileName));
    return CharStreams.toString(reader);
}
  • Lua 嵌入 Redis 優勢:
    1. 減少網絡開銷: 不使用 Lua 的代碼需要向 Redis 發送多次請求, 而腳本只需一次即可, 減少網絡傳輸;
    2. 原子操作: Redis 將整個腳本作為一個原子執行, 無需擔心并發, 也就無需事務;
    3. 復用: 腳本會永久保存 Redis 中, 其他客戶端可繼續使用.

Lua語言模型

Lua是一種 便于嵌入應用程序 的腳本語言, 具備了作為通用腳本語言的所有功能. 其高速虛擬機實現非常有名(Lua的垃圾回收很有講究- 增量垃圾回收 ), 在很多虛擬機系性能評分中都取得了優異的成績. Home lua.org .

嵌入式 為方針設計的Lua, 在默認狀態下簡潔得嚇人. 除了基本的數據類型外, 其他一概沒有. 標注庫也就 Coroutine 、 String 、 Table 、 Math 、 I/O 、 OS , 再加上 Modules包加載 而已. 

注: 本文僅介紹 Lua 與眾不同的 設計模型 (對比 Java/C/C++JavaScriptPythonGo ), 語言細節可參考文內和附錄推薦的文章以及Lua之父 Roberto Ierusalimschy 的《Programming in Lua》(中文版: LUA程序設計(第2版)>)

基礎

1. 數據類型

  • 作為通用腳本語言, Lua的數據類型如下:

    • 數值型:
      全部為浮點數型, 沒有整型;
      只有 nil 和 false 作為布爾值的 false , 數字 0 和空串( ‘’ / ‘’ )都是 true ;

    • 字符串

    • 用戶自定義類型

    • 函數(function)

    • 表(table)

變量如果沒有特殊說明為全局變量(那怕是語句塊 or 函數內), 局部變量前需加 local 關鍵字.

2. 關鍵字

3. 操作符

  • Tips:

    • 數學操作符的操作數如果是字符串會自動轉換成數字;

    • 連接 .. 自動將數值轉換成字符串;

    • 比較操作符的結果一定是布爾類型, 且會嚴格判斷數據類型( '1' != 1 );

函數(function)

在 Lua 中, 函數是和字符串、數值和表并列的基本數據結構, 屬于 第一類對象 ( first-class-object /一等公民), 可以和數值等其他類型一樣 賦給變量 作為參數傳遞 , 以及作為 返回值接收(閉包) :

  • 使用方式類似JavaScript:

-- 全局函數: 求階乘
function fact(n)
    if n == 1 then
        return 1
    else
        return n * fact(n - 1)
    end
end
 
-- 1. 賦給變量
localfunc = fact
print("func type: " .. type(func), "fact type: " .. type(fact), "result: " .. func(4))
 
-- 2. 閉包
localfunction new_counter()
    localvalue = 0;
    return function()
        value = value + 1
        return value
    end
end
 
localcounter = new_counter()
print(counter(), counter(), counter())
 
-- 3. 返回值類似Go/Python
localrandom_func = function(param)
    return 9, 'a', true, "??π", param
end
 
localvar1, var2, var3, var4, var5 = random_func("no param is nil")
print(var1, var2, var3, var4, var5)
 
-- 4. 變數形參
localfunction square(...)
    localargv = { ... }
    for i = 1, #argv do
        argv[i] = argv[i] * argv[i]
    end
    return table.unpack(argv)
end
 
print(square(1, 2, 3))

表(table)

Lua最具特色的數據類型就是 表(Table) , 可以實現 數組 、 Hash 、 對象 所有功能的萬能數據類型:

-- array
localarray = { 1, 2, 3 }
print(array[1], #array)
 
-- hash
localhash = { x = 1, y = 2, z = 3 }
print(hash.x, hash['y'], hash["z"], #hash)
 
-- array & hash
array['x'] = 8
print(array.x, #array)
  • Tips:

    • 數組索引從 1 開始;

    • 獲取數組長度操作符 # 其’長度’只包括以 (正)整數 為索引的數組元素.

    • Lua用 表管理全局變量 , 將其放入一個叫 _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() 來替換 tablemetatable . Lua 事件一覽表:

對于這些操作, Lua 都將其關聯到 metatable 的事件Key, 當 Lua 需要對一個值發起這些操作時, 首先會去檢查其 metatable 中是否有對應的事件Key, 如果有則調用之以 控制Lua解釋器作出響應 .

MetaMethods

MetaMethods主要用作一些類似C++中的 運算符重載 操作, 如重載 + 運算符:

localfrac_a = { numerator = 2, denominator = 3 }
localfrac_b = { numerator = 4, denominator = 8 }
 
localoperator = {
    __add = function(f1, f2)
        localret = {}
        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)
 
localfrac_res = frac_a + frac_b
setmetatable(frac_res, operator) -- 使tostring()方法生效
print(tostring(frac_res))

關于更多Lua事件處理可參考文檔: Metamethods .

MetaTables 與 面向對象

Lua本來就不是設計為一種 面向對象 語言, 因此其面向對象功能需要通過 元表(metatable) 這種非常怪異的方式實現, Lua并不直接支持面向對象語言中常見的類、對象和方法: 其 對象 和 類 通過 表 實現, 而 方法 是通過 函數 來實現.

上面的 Event一覽表 內我們看到有 __index 這個事件重載,這個東西主要是重載了 find key 操作, 該操作可以讓Lua變得有點面向對象的感覺(類似JavaScript中的 prototype ). 通過Lua代碼模擬:

localfunction gettable_event(t, key)
    local h
    if type(t) == "table" then
        localvalue = rawget(t, key)
        if value ~= nilthen
            return value
        end
 
        h = getmetatable(t).__index
        if h == nilthen
            return nil
        end
    else
        h = getmetatable(t).__index
        if h == nilthen
            error("error")
        end
    end
 
    if type(h) == "function" then
        -- callthehandler
        return (h(t, key))
    else
        -- or repeatoprationonit
        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))
  • 對于任何事件, Lua的處理都可以歸結為以下邏輯:
    1. 如果存在規定的操作則執行它;
    2. 否則從元表中取出各事件對應的 __ 開頭的元素, 如果該元素為函數, 則調用;
    3. 如果該元素不為函數, 則用該元素代替 table 來執行事件所對應的處理邏輯.

這里的代碼僅作模擬, 實際的行為已經嵌入Lua解釋器, 執行效率要遠高于這些模擬代碼.

方法調用的實現

面向對象的基礎是創建對象和調用方法. Lua中, 表作為對象使用, 因此創建對象沒有問題, 關于調用方法, 如果表元素為函數的話, 則可直接調用:

-- 從obj取鍵為x的值, 將之視為function進行調用
obj.x(foo)

不過這種實現方法調用的方式, 從面向對象角度來說還有2個問題:

  • 首先: 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
}
 
localobj = {}
setmetatable(obj, { __index = proto })
obj.x()

proto 變成了原型對象, 當 obj 中不存在的屬性被引用時, 就會去搜索 proto .

  • 其次: 通過方法搜索得到的函數對象只是單純的函數, 而無法獲得最初調用方法的表( 接收器 )相關信息. 于是, 過程和數據就發生了分離 .JavaScript中, 關于接收器的信息可由關鍵字 this 獲得, 而在Python中通過方法調用形式獲得的 并非單純的函數對象 , 而是一個 “方法對象” –其接收器會在內部 作為第一參數附在函數的調用過程中 .

    而Lua準備了支持方法調用的 語法糖 : obj:x() . 表示 obj.x(obj) , 也就是: 通過冒號記法調用的函數, 其接收器會被作為第一參數添加進來 ( obj 的求值只會進行一次, 即使有副作用也只生效一次).

-- 這個語法糖對定義也有效
function proto:y(param)
    print(self, param)
end
 
- Tips: 用冒號記法定義的方法, 調用時最好也用冒號記法, 避免參數錯亂
obj:y("parameter")

更多MetaTable介紹可參考文檔 Metatable 與博客 metatable和metamethod .

基于原型的編程

Lua雖然能夠進行面向對象編程, 但用元表來實現, 仿佛把對象剖開看到五臟六腑一樣.

《代碼的未來》中松本行弘老師向我們展示了一個基于原型編程的Lua庫, 通過該庫, 即使沒有深入解Lua原始機制, 也可以實現面向對象:

--
-- Author: Matz
-- Date: 16/9/24
-- Time: 下午5:13
--
 
-- Object為所有對象的上級
Object = {}
 
-- 創建現有對象副本
function Object:clone()
    localobject = {}
 
    -- 復制表元素
    for k, v in pairs(self) do
        object[k] = v
    end
 
    -- 設定元表: 指定向自身`轉發`
    setmetatable(object, { __index = self })
 
    return object
end
 
-- 基于類的編程
function Object:new(...)
    localobject = {}
 
    -- 設定元表: 指定向自身`轉發`
    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的Lua腳本中可使用 redis.call() / redis.pcall() 函數調用Reids命令:

redis.call("set", "foo", "bar")
localvalue = redis.call("get", "foo")

redis.call() 返回值就是Reids命令的執行結果, Redis回復與Lua數據類型的對應關系如下:

Reids返回值類型 Lua數據類型
整數 數值
字符串 字符串
多行字符串 表(數組)
狀態回復 表(只有一個 ok 字段存儲狀態信息)
錯誤回復 表(只有一個 err 字段存儲錯誤信息)

注: Lua 的 false 會轉化為空結果.

redis-cli提供了 EVAL 與 EVALSHA 命令執行Lua腳本:

  • EVAL
    EVAL script numkeys key [key ...] arg [arg ...]
    keyarg 兩類參數用于向腳本傳遞數據, 他們的值可在腳本中使用 KEYS 和 ARGV 兩個table訪問: KEYS 表示要操作的鍵名, ARGV 表示非鍵名參數(并非強制).
  • EVALSHA
    EVALSHA 命令允許通過腳本的 SHA1 來執行(節省帶寬), Redis在執行 EVAL / SCRIPT LOAD 后會計算腳本 SHA1 緩存, EVALSHA 根據 SHA1 取出緩存腳本執行.

創建Lua環境

為了在 Redis 服務器中執行 Lua 腳本, Redis 內嵌了一個 Lua 環境, 并對該環境進行了一系列修改, 從而確保滿足 Redis 的需要. 其創建步驟如下:

  • 創建基礎 Lua 環境, 之后所有的修改都基于該環境進行;
  • 載入函數庫到 Lua 環境, 使 Lua 腳本可以使用這些函數庫進行數據操作: 如基礎庫(刪除了 loadfile() 函數)、Table、String、Math、Debug等標準庫, 以及CJSON、 Struct(用于Lua值與C結構體轉換)、 cmsgpack等擴展庫(Redis 禁用Lua標準庫中與文件或系統調用相關函數, 只允許對 Redis 數據處理).
  • 創建全局表 redis , 其包含了對 Redis 操作的函數, 如 redis.call() 、 redis.pcall() 等;
  • 替換隨機函數: 為了確保相同腳本可在不同機器上產生相同結果, Redis 要求所有傳入服務器的 Lua 腳本, 以及 Lua 環境中的所有函數, 都必須是無副作用的 純函數 , 因此Redis使用自制函數替換了 Math 庫中原有的 math.random() 和 math.randomseed() .
  • 創建輔助排序函數: 對于 Lua 腳本來說, 另一個可能產生數據不一致的地方是那些 帶有不確定性質的命令 (如: 由于 set 集合無序, 因此即使兩個集合內元素相同, 其輸出結果也并不一樣), 這類命令包括 SINTER SUNION SDIFF SMEMBERS HKEYS HVALS KEYS 等.
    Redis 會創建一個輔助排序函數 __redis__compare_helper , 當執行完以上命令后, Redis會調用 table.sort() 以 __redis__compare_helper 作為輔助函數對命令返回值排序.
  • 創建錯誤處理函數: Redis創建一個 __redis__err__handler 錯誤處理函數, 當調用 redis.pcall() 執行 Redis 命令出錯時, 該函數將打印異常詳細信息.
  • Lua全局環境保護: 確保傳入腳本內不會將額外的全局變量導入到 Lua 環境內.

    小心: Redis 并未禁止用戶修改已存在的全局變量.

  • 完成Redis的 lua 屬性與Lua環境的關聯:


整個 Redis 服務器只需創建一個 Lua 環境.

Lua環境協作組件

  • Redis創建兩個用于與Lua環境協作的組件: 偽客戶端 – 負責執行 Lua 腳本中的 Redis 命令, lua_scripts 字典 – 保存 Lua 腳本:
    • 偽客戶端
      執行Reids命令必須有對應的客戶端狀態, 因此執行 Lua 腳本內的 Redis 命令 必須為 Lua 環境專門創建一個偽客戶端, 由該客戶端處理 Lua 內所有命令: redis.call() / redis.pcall() 執行一個Redis命令步驟如下:
    • lua_scripts 字典
      字典key為腳本 SHA1 校驗和, value為 SHA1 對應腳本內容, 所有被 EVAL 和 SCRIPT LOAD 載入過的腳本都被記錄到 lua_scripts 中, 便于實現 SCRIPT EXISTS 命令和腳本復制功能.

EVAL命令原理

EVAL 命令執行分為以下三個步驟:

  1. 定義Lua函數:
    在 Lua 環境內定義 Lua函數 : 名為 f_ 前綴+腳本 SHA1 校驗和, 體為 腳本內容本身 . 優勢:
    • 執行腳本步驟簡單, 調用函數即可;
    • 函數的局部性可保持 Lua 環境清潔, 減少垃圾回收工作量, 且避免使用全局變量;
    • 只要記住 SHA1 校驗和, 即可在不知腳本內容的情況下, 直接調用 Lua 函數執行腳本( EVALSHA 命令實現).
  2. 將腳本保存到 lua_scripts 字典;
  3. 執行腳本函數:
    執行剛剛在定義的函數, 間接執行 Lua 腳本, 其準備和執行過程如下:
    1). 將 EVAL 傳入的鍵名和參數分別保存到 KEYS 和 ARGV , 然后將這兩個數組作為全局變量傳入到Lua環境;
    2). 為Lua環境裝載超時處理 hook ( handler ), 可在腳本出現運行超時時讓通過 SCRIPT KILL 停止腳本, 或 SHUTDOWN 關閉Redis;
    3). 執行腳本函數;
    4). 移除超時 hook ;
    5). 將執行結果保存到客戶端輸出緩沖區, 等待將結果返回客戶端;
    6). 對Lua環境執行垃圾回收.

對于 會產生隨機結果但無法排序的命令 (如只產生一個元素, 如 SPOP SRANDMEMBER RANDOMKEY TIME ), Redis在這類命令執行后將腳本狀態置為 lua_random_dirty , 此后只允許腳本調用只讀命令, 不允許修改數據庫值.

實踐

使用Lua腳本重新構建帶有過期時間的分布式鎖.

案例來源: Redis實戰> 第6、11章, 構建步驟:

  • 鎖申請
    • 首先嘗試加鎖:
      • 成功則為鎖設定過期時間; 返回;
      • 失敗檢測鎖是否添加了過期時間;
    • wait.
  • 鎖釋放
    • 檢查當前線程是否真的持有了該鎖:
      • 持有: 則釋放; 返回成功;
      • 失敗: 返回失敗.

非Lua實現

String acquireLockWithTimeOut(Jedisconnection, 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 (InterruptedExceptionignored) {
        }
    }
    return null;
}
 
boolean releaseLock(Jedisconnection, String lockName, String identifier) {
    String key = "lock:" + lockName;
 
    connection.watch(key);
    // 確保當前線程還持有鎖
    if (identifier.equals(connection.get(key))) {
        Transactiontransaction = connection.multi();
        transaction.del(key);
        return transaction.exec().isEmpty();
    }
    connection.unwatch();
 
    return false;
}

Lua腳本實現

  • Lua腳本: acquire

localkey = KEYS[1]
localidentifier = ARGV[1]
locallockTimeOut = 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
localkey = KEYS[1]
localidentifier = ARGV[1]
 
if redis.call("GET", key) == identifierthen
    redis.call("DEL", key)
    return 1
end
return 0
  • Pre工具: 腳本執行器

/**
* @author jifang
* <a >@since</a> 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 ScriptCallergetInstance(String script) {
        return new ScriptCaller(script);
    }
 
    public Object call(Jedisconnection, 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);
    }
}
  • Client

public class Client {
 
    private ScriptCalleracquireCaller = 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 ScriptCallerreleaseCaller = 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() {
        Jedisjedis = new Jedis("127.0.0.1", 9736);
        String identifier = acquireLockWithTimeOut(jedis, "ret1", 200 * 1000, 300);
        System.out.println(releaseLock(jedis, "ret1", identifier));
    }
 
    String acquireLockWithTimeOut(Jedisconnection, 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 (InterruptedExceptionignored) {
                }
            }
        }
 
        return acquired ? identifier : null;
    }
 
    boolean releaseLock(Jedisconnection, 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);
    }
}

 

 

來自:http://blog.jobbole.com/106456/

 

 本文由用戶 hm8617 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!