基於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最後一個元素時間比較,判斷是否限速。
連結: 參考原文.