1. 程式人生 > >基於Redis的分散式限流

基於Redis的分散式限流

遇到這種場景:要求某個介面1s最多請求10次,在分散式環境下guava的RateLimiter用不上。redis可以滿足需求,於是baidu一下redis分散式限流的程式碼實現,總結看基本分為兩種,指令碼實現、非指令碼實現。非指令碼實現缺點明顯,lua實現優勢滿滿,肯定用lua啊啊啊啊。但是還是要看下非指令碼實現的坑在哪裡,lua實現的兩種方式:均勻實現和非均勻實現。當然用lua的均勻實現方式是最好用的,也是推薦的。

看具體的實現之前,還是給一個場景:限定登入介面1s最多請求5次

非指令碼實現

實現思路:用String結構,value儲存當前登入次數,設定key的過期時間是1s。所以只要key沒過期並且value<10就可以繼續登入。

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; }

這段程式碼存在以下幾個問題:

  • 可能出現競態條件
  • 不使用pipeline的情況下,最多傳送5條指令給redis,傳輸太多
  • 限速不均勻

下面一一來看一下這幾個問題

可能出現競態條件

redis是單執行緒單程序,多客戶端的命令請求是序列在服務端執行的,所以在服務端不存在競爭條件,競爭條件存在於多客戶端,沒辦法保證一個客戶端的多次命令請求是一個原子操作。redis事物可以解決這個問題,redis事物可以保證一個客戶端的多個命令原子執行。但是啊但是,redis事物也不是萬能的,使用受限,使用的時候要考慮自己的使用場景。

redis事物使用需要注意:
1.實現樂觀鎖需要配合WATCH命令
2.redis事物只支援單機或者單節點。所在redis cluster環境下,需要操作多個key的情況不能使用事物。因為多個key很可能在不同的redis節點。

經過上面的分析,在redis叢集環境,上面的程式碼是可以使用redis事物,因為事物裡邊只有一個key,肯定事物的作用範圍也只有一個redis節點。再加上WATCH上面的程式碼就滴水不漏了。

但是啊但是,這個實現太麻煩,跟redis的互動也太多。

限速不均勻

上面程式碼實現的時間窗不平滑。
舉個例子:限速每秒5個,如下的場景9個請求都能被接收,因為第一請求設定1s過期,第5個請求又設定1s過期,所以這9個請求都不會被限速攔截。但是中間的7個請求也是在1s內,已經違背了限速每秒5個。

指令碼實現

指令碼實現是指用lua+redis實現,可保證lua指令碼原子執行,並且和redis服務端只互動一次。限速是否均勻要看實現方式。

指令碼實現-非均勻實現

實現思路:跟上邊的非指令碼實現一樣的思路。

lua指令碼:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then

    if redis.call("INCR", key) > limit then

        return 10
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key,expire_time )
    return 1
end

順便說一下在redis叢集環境下使用lua遇到的問題:

redis.clients.jedis.exceptions.JedisClusterException: No way to dispatch this command to Redis Cluster because keys have different slots.

	at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:46)
	at redis.clients.jedis.JedisCluster.eval(JedisCluster.java:1737)

或者是這樣的報錯

@user_script:2: @user_script: 2: Lua script attempted to access a non local key in a cluster node

因為lua指令碼中的key也必須在同一個槽中,所以必須給key加{}保證lua中的key都在同一個槽中。

兩點說明:

  • 呼叫lua指令碼傳遞的引數key就要帶有{}。在lua指令碼中給傳遞進來的key加{}是不行的。
  • 即便lua中只操作一個key,也要加{}。

指令碼實現-均勻實現

實現思路:用list結構實現,儲存有限個元素,(限速每秒請求5次則list最多儲存5個元素),value記錄請求時間。每次請求,用當前時間和list最後一個元素時間比較,判斷是否限速。

連結: 參考原文.