Redis 事務與 Lua 腳本詳解

Redis 事務詳解

Redis 的事務和 MySQL 的事務在概念上是類似的,都是把一系列操作綁定成一組,讓這一組能夠批量執行。不過,Redis 事務與 MySQL 事務存在以下區別:

Redis 事務本質上是在服務器上創建了一個事務隊列。每次客戶端在事務中進行一個操作,都會把命令先發給服務器,放到事務隊列中(但是不會立即執行),而是在真正收到 EXEC 命令之後,才真正執行隊列中的所有操作。因此,Redis 事務的功能相比於 MySQL 來說是弱化很多的,只能保證事務中的這幾個操作是連續的,不會被別的客戶端 “加塞”。Redis 事務的意義就在於 “打包”,避免其他客戶端的命令插隊到中間。

Redis 事務的主要操作如下:

Lua 腳本詳解

Lua 是一種輕量小巧的腳本語言,用標準 C 語言編寫並以源代碼形式開放,其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。Lua 語言的特點包括:

Lua 語言的應用場景包括遊戲開發、獨立應用腳本、Web 應用腳本、擴展和數據庫插件(如 MySQL Proxy 和 MySQL WorkBench)、安全系統(如入侵檢測系統)等。

案例分析

以下是一個 Redis 整合 Lua 腳本實現限流效果的案例:

限流是指某應用模塊需要限制指定 IP(或指定模塊、指定應用)在單位時間內的訪問次數。例如,在某高併發場景裏,會員查詢模塊對風險控制模塊的限流需求是在 10 秒裏最多允許有 1000 個請求。Lua 腳本天然具有原子性,而且執行 Lua 腳本的 Redis 服務器是以單線程模式處理命令,所以用 Lua 腳本能有效地實現限流。

以下是一個實現基於計數模式的限流功能的 Lua 腳本:

local obj=KEYS[1]
local limitNum=tonumber(redis.call('get',obj)or"0")
local curVisitNum=tonumber(redis.call('get',obj) or "0") + 1
if curVisitNum>limitNum then
  return 0
else
  redis.call("INCRBY",obj,"1")
  redis.call("EXPIRE",obj,tonumber(ARGV[2]))
  return curVisitNum
end

該腳本共有 3 個參數:

腳本的功能是限制 KEYS[1] 對象在 ARGV[2] 時間範圍內只能訪問 ARGV[1] 次。以下是腳本的詳細解釋:

也就是說,在調用該 Lua 腳本時,如果返回值是 0,就說明當前訪問量已經達到限流標準,否則還可以繼續訪問。

以下是一個調用上述腳本實現限流效果的 Java 代碼示例:

import redis.clients.jedis.Jedis;

public class LuaLimitByCount extends Thread {
    @Override
    public void run() {
        Jedis jedis = new Jedis("192.168.159.33", 6379);
        // 在本線程內,模擬在單位時間內發5個請求
        for (int visitNum = 0; visitNum < 5; visitNum++) {
            boolean visitFlag = LimitByCount.canVisit(jedis, Thread.currentThread().getName(), "10", "3");
            if (visitFlag) {
                System.out.println(Thread.currentThread().getName() + " can visit.");
            } else {
                System.out.println(Thread.currentThread().getName() + " can not visit.");
            }
        }
    }

    public static void main(String[] args) {
        // 開啓3個線程
        for (int cnt = 0; cnt < 3; cnt++) {
            new LuaLimitByCount().start();
        }
    }
}

// 封裝是否需要限流的方法
class LimitByCount {
    // 判斷是否需要限流
    public static boolean canVisit(Jedis jedis, String modelName, String limitTime, String limitNum) {
        String script = "local obj=KEYS[1] " +
                        "local limitNum=tonumber(ARGV[1]) " +
                        "local curVisitNum=tonumber(redis.call('get',obj) or \"0\")+1 " +
                        "if curVisitNum>limitNum then " +
                        " return 0 " +
                        "else " +
                        " redis.call(\"incrby\",obj,\"1\") " +
                        " redis.call(\"expire\",obj,ARGV[2])) " +    
                        " return curVisitNum " +    
                        " end";    

        Object result = jedis.eval(script, 1, modelName, limitNum, limitTime);    
        if (result.equals(0L)) {    
            return false;    
        } else {    
            return true;    
        }    
    }

解釋

LimitByCount類中,canVisit方法用於判斷指定模塊(modelName)是否能在指定時間(limitTime)內訪問指定次數(limitNum)。

Lua 腳本的邏輯是:

  1. 獲取當前模塊的訪問次數(如果不存在則默認爲 0),並加 1。

  2. 如果加 1 後的訪問次數超過了最大訪問次數,則返回 0,表示不能訪問。

  3. 否則,使用INCRBY命令將訪問次數加 1,並使用EXPIRE命令設置鍵值對的生存時間(即限流時間),然後返回當前的訪問次數,表示可以訪問。

在 Java 代碼中,LuaLimitByCount類繼承自Thread,並在run方法中模擬了多個請求。每個請求都會調用LimitByCount.canVisit方法來判斷是否可以訪問。根據返回值,打印相應的信息。

注意事項

  1. Lua 腳本的原子性:Redis 在執行 Lua 腳本時,會將腳本作爲一個整體執行,期間不會被其他命令打斷,從而保證了原子性。

  2. 參數傳遞:在調用 Lua 腳本時,需要注意參數的傳遞順序和類型。在 Java 代碼中,使用jedis.eval方法時,需要按照script, keyCount, key1, arg1, arg2, ...的順序傳遞參數。

  3. 異常處理:在實際應用中,需要添加異常處理邏輯,以處理可能發生的 Redis 連接異常、執行異常等。

  4. 性能考慮:雖然 Lua 腳本在 Redis 中執行具有較高的性能,但在高併發場景下,仍需要考慮腳本的執行時間和 Redis 服務器的負載情況。

通過上述案例,我們瞭解瞭如何使用 Redis 事務和 Lua 腳本實現限流功能。在實際應用中,可以根據具體需求對腳本進行定製和優化。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aby4sJA4e0ik5142sdXIpA