參看來源:
https://blog.51cto.com/u_15708799/5703440
測試有效代碼:我們要做的是:當併發請求超出了限定閾值時,要延遲請求,而不是直接丟棄 。當然也可以把結果給業務端,看業務端是提醒用戶下次,還是延遲處理,還是丟棄。
@Test public void testLimitWait() throws InterruptedException { ExecutorService pool = Executors.newCachedThreadPool(); log.info("--------{}", redisTemplate.opsForValue().get("abc")); for (int j = 1; j <= 5; j++) { int i=j; pool.execute(() -> { Thread.currentThread().setName( Thread.currentThread().getName().replace("-","_")); limitWait("abc", 3, 1); log.info(i + ":" + true + " ttl:" + redisTemplate.getExpire("abc", TimeUnit.MILLISECONDS)); try { // 線程等待,模擬執行業務邏輯 Thread.sleep(new Random().nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } }); } pool.shutdown(); pool.awaitTermination(2,TimeUnit.SECONDS); } /** * 達到限流時,則等待,直到新的間隔。 * * @param key 可以是ip + 當前秒 或者 是 uid + 當前秒 或者 固定的一個入口 key * @param limitCount 一定時間內最多訪問次數 * @param limitSecond 給定的時間範圍 單位(秒) */ public void limitWait(String key, int limitCount, int limitSecond) { boolean ok;//放行標誌 do { ok = limit(key, limitCount, limitSecond); log.info("放行標誌={}", ok); if (!ok) { Long ttl = redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); if (null != ttl && ttl > 0) { try { Thread.sleep(ttl); log.info("sleeped:{}", ttl); } catch (InterruptedException e) { e.printStackTrace(); } } } } while (!ok); } /** * 限流方法 true-放行;false-限流 * * @param key * @param limitCount * @param limitSecond * @return */ public boolean limit(String key, int limitCount, int limitSecond) { List<String> keys = Collections.singletonList(key); String luaScript = buildLuaScript(); RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long count = redisTemplate.execute(redisScript, keys, limitCount, limitSecond); log.info("Access try count is {} for key = {}", count, key); if (count != null && count.intValue() <= limitCount) { return true;//放行 } else { return false;//限流 // throw new RuntimeException("You have been dragged into the blacklist"); } } /** * 編寫 redis Lua 限流腳本 */ public String buildLuaScript() { StringBuilder lua = new StringBuilder(); lua.append("local c"); lua.append("\nc = redis.call('get',KEYS[1])"); // 實際調用次數超過閾值,則直接返回 lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then"); lua.append("\nreturn c;"); lua.append("\nend"); // 執行計算器自加 lua.append("\nc = redis.call('incr',KEYS[1])"); lua.append("\nif tonumber(c) == 1 then"); // 從第一次調用開始限流,設置對應鍵值的過期 lua.append("\nredis.call('expire',KEYS[1],ARGV[2])"); lua.append("\nend"); lua.append("\nreturn c;"); return lua.toString(); }
關於TTL(Time to Live)
不管是redis還是jedis,其實都是利用了消息的ttl(Time to Live),即,當消息的ttl=0時,消息會自動過期。ttl還見諸於RabbitMQ的死信隊列,隊列裏的消息會延遲消費,當等待ttl指定的時間後,纔會自動轉移到實時隊列。
redis是使用RedisTemplate.expire來設置ttl;使用RedisTemplate.getExpire(key)或RedisTemplate.getExpire(key,TimeUnit)方法來獲取ttl。當然,對於併發限流,我們需要使用後者指定時間單位爲TimeUnit.MILLISECONDS來得到精確的剩餘毫秒數。
jedis是使用Jedis.expire來設置ttl;使用Jedis.ttl(key)方法來獲取ttl,返回的時間是毫秒。
getExpire/ttl返回值:
-2:key不存在
-1:未設置ttl
n:實際的剩餘ttl
redis.incr指令說明
關於redis的increment :
當key不存在時,創建key,默認值是delta值(不指定delta的話,則爲1)。
當key存在時,按delta來遞增。