分佈式限流最關鍵的是要將限流服務作成原子化,而解決方案可使使用redis+lua或者nginx+lua技術進行實現,經過這兩種技術能夠實現的高併發和高性能。
首先咱們來使用redis+lua實現時間窗內某個接口的請求數限流,實現了該功能後能夠改造爲限流總併發/請求數和限制總資源數。Lua自己就是一種編程語言,也可使用它實現複雜的令牌桶或漏桶算法。
以下操做因是在一個lua腳本中(至關於原子操做),又因Redis是單線程模型,所以是線程安全的。java
相比Redis事務來講,Lua腳本有如下優勢
減小網絡開銷: 不使用 Lua 的代碼須要向 Redis 發送屢次請求, 而腳本只需一次便可, 減小網絡傳輸;
原子操做: Redis 將整個腳本做爲一個原子執行, 無需擔憂併發, 也就無需事務;
複用: 腳本會永久保存 Redis 中, 其餘客戶端可繼續使用.nginx
Lua腳本redis
local key = KEYS[1] --限流KEY(一秒一個) local limit = tonumber(ARGV[1]) --限流大小 local current = tonumber(redis.call('get', key) or "0") if current + 1 > limit then --若是超出限流大小 return 0 else --請求數+1,並設置2秒過時 redis.call("INCRBY", key,"1") redis.call("expire", key,"2") end return 1
java代碼算法
import org.apache.commons.io.FileUtils; import redis.clients.jedis.Jedis; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; public class RedisLimitRateWithLUA { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(1); for (int i = 0; i < 7; i++) { new Thread(new Runnable() { public void run() { try { latch.await(); System.out.println("請求是否被執行:"+accquire()); } catch (Exception e) { e.printStackTrace(); } } }).start(); } latch.countDown(); } public static boolean accquire() throws IOException, URISyntaxException { Jedis jedis = new Jedis("127.0.0.1"); File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + "limit.lua"); String luaScript = FileUtils.readFileToString(luaFile); String key = "ip:" + System.currentTimeMillis()/1000; // 當前秒 String limit = "5"; // 最大限制 List<String> keys = new ArrayList<String>(); keys.add(key); List<String> args = new ArrayList<String>(); args.add(limit); Long result = (Long)(jedis.eval(luaScript, keys, args)); // 執行lua腳本,傳入參數 return result == 1; } }
運行結果apache
請求是否被執行:true 請求是否被執行:true 請求是否被執行:false 請求是否被執行:true 請求是否被執行:true 請求是否被執行:true 請求是否被執行:false
從結果可看出只有5個請求成功執行編程
IP限流Lua腳本安全
local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end