Redis 中使用 Lua 腳本

Redis 本身已經提供了豐富的命令,但是直接用來處理一些複雜業務時可能還不夠方便,會有一定的侷限性。因此,在 Redis2.6 版本開始提供了對 Lua 腳本的支持,Lua 腳本的使用還是比較廣泛的,比如商品秒殺、分佈式鎖等,使用 Lua 腳本可以帶來以下的好處:

  • 原子性,Redis 會將 Lua 腳本當做一個整體去執行,無需額外開啓事務,可以在高併發場景下保持數據的一致性。
  • 複用,編寫好的 Lua 腳本可以保存在本地,然後發送到 Redis 服務器執行;還可以把腳本緩存到 Redis 服務器,會返回腳本內容的 SHA1 校驗和的編碼,然後通過編碼調用腳本,這裏後邊會詳細介紹。
  • 減少網絡開銷,如果腳本內容很多,還是建議將腳本緩存到服務器,避免直接從本地發送腳本到服務器時在網絡傳輸上造成性能損失。

一、基本用法

爲了讓例子更加的貼近實際應用,這裏實現一個簡單版的分佈式鎖。這裏先用Jedis操作。

public class LuaService {
    public void test1() {
        JedisPool jedisPool = new JedisPool("localhost", 6379);
        Jedis jedis = jedisPool.getResource();
        jedis.auth("shehuan");
        try {
            // 1、獲取一個隨機字串
            String value = UUID.randomUUID().toString();
            // 2、獲取鎖
            // 爲了防止獲取鎖和設置鎖的過期時間之間,由於某種原因 Redis 掛了,沒成功設置過期時間,導致鎖不能被釋放,
            // 需要讓 setnx、expire 命令一起執行,保證原子性
            String lock = jedis.set("lock", value, new SetParams().nx().ex(5L));
            // 3、判斷是否成功獲取到鎖
            if (lock != null && "OK".equals(lock)) {
                // 4、獲取到鎖了,獲取到鎖執行具體的業務,例如修改庫存
                jedis.set("stock", "1000");
                // 5、釋放鎖
                // 簡單粗暴的方式是直接 jedis.del("lock")
                // 但這樣這可能會有問題,如果第一個線程獲取鎖後執行的業務比較耗時,在到達鎖的過期時間後還沒執行完,但此時鎖已經釋放,
                // 此時,第二個線程獲取到了鎖,在第二個線程執行業務的過程中,第一個線程的業務執行結束了,它去把鎖釋放掉了,
                // 但第一個線程釋放掉的是第二個線程獲取到的鎖,造成錯誤釋放,這樣亂套了,
                // 所以需要保證每個線程只能釋放自己獲得的鎖,不能釋放別人的, 釋放鎖的操作就可以通過 Lua 腳本實現
                // todo
            } else {
                // 6、沒獲取到鎖,可以停止操作,或稍後再繼續嘗試
                System.out.println("沒獲取到鎖");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
    }
}

上邊詳細的介紹了分佈式鎖的實現過程,以及可能出現的問題,最終,我們決定刪除鎖的操作使用 Lua 腳本實現,對應的腳本如下:

if redis.call("get", KEYS[1]) == ARGV[1] then
   return redis.call("del", KEYS[1])
else
   return 0
end

Lua 腳本中執行具體的 Redis 命令,需要使用redis.call()方法,KEYS表示客戶端發起腳本執行命令時攜帶的 Redis key 的一個集合,ARGV則是其它參數的一個集合,主意下標從1開始。結合我們的業務,這裏的KEYS[1]則表示lockARGV[1]則是一個隨機字符串。整個腳本的含義就是,如果客戶端傳遞的lockvalue和 Redis 中存儲的一致,就刪除lock

Lua 腳本的語法還是比較簡單的,具體內容可以自行學習。

前邊的準備工作基本結束了,文章開始說過執行腳本有兩種途徑,下邊我們具體來看:

1、客戶端直接發送腳本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList("lock"), Collections.singletonList(value));
if (result != null && (Long) result != 0) {
    System.out.println("成功釋放鎖");
}

這裏使用jedis.eval()發送腳本到 Redis 服務器執行,後兩個參數分別是 key 的集合,以及 value 參數的集合。

2、Redis 服務器緩存腳本

先將腳本以文件形式放到 Redis 裏,例如這樣:


然後通過如下命令讓 Redis 服務器緩存腳本:

redis-cli -x -a 連接redis的密碼 script load < xxx.lua

script load命令會在 Redis 服務器緩存 Lua 腳本,並且腳本內容經過 SHA-1 簽名算法處理後,會返回腳本內容的 SHA1 校驗和的編碼,然後在端調用時,傳入編碼字符串作爲參數,這樣 Redis 服務器就會執行對應緩存的腳本了,就不用了每次發送具體的腳本內容了。

Object result = jedis.evalsha("e58196026fa94e1c49d14a835c57e941f9ae4202", Collections.singletonList("k1"), Collections.singletonList(value));
if ((Long) result != 0) {
    System.out.println("成功釋放鎖");
}

還有兩個比較有用的命令:

  • script exists 使用 sha1 判斷是否存在對應的緩存腳本;
  • script flush刪除全部的緩存腳本。

除了使用上邊的命令緩存腳本、生成腳本的 SHA1 校驗和的編碼,還可以使用 Jedis 實現,但最終的 SHA1 編碼內容是不同的:

String sha1 = jedis.scriptLoad(script);

二、在 SpringBoot 中使用

實際的項目中,可能更多的會在 SpringBoot 項目中整合 Redis,此時執行 Lua 腳本的基本流程如下:

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
// 設置腳本內容
redisScript.setScriptText(script);
// 設置腳本返回值類型
redisScript.setResultType(Long.class);
// 執行腳本
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList("lock"), value);
if (result != null && result != 0) {
    System.out.println("成功釋放鎖");
}

核心的類就是DefaultRedisScript,它實現了RedisScript接口。execute()方法最後一個參數是可變類型的,用來傳遞多個 value 參數。初次執行execute()方法時,其內部會自動緩存 Lua 腳本到 Redis 服務器;同時每次執行腳本時會根據腳本內容自動計算出對應的 SHA1 校驗和的編碼,去匹配、執行緩存的腳本。

具體的 SHA1 校驗和的編碼,可以在execute()方法執行後,使用redisScript.getSha1()查看。使用 SpringBoot 方式 執行 Lua 腳本生成的 SHA1 校驗和的編碼和前邊直接使用 Jedis 生成的一致。

無論用那種方式在 Redis 中使用 Lua 腳本,其中的原理都是類似的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章