業務場景:
一個時間區間內業務系統需給一批設備下發任務,該任務需設備依賴外部系統完成執行,外部系統最多支持60個設備同時訪問。設備通過週期上報,在業務系統中獲取任務下發通知。爲了防止大數據高併發場景下,大量設備同時訪問業務系統,查詢任務通知造成的阻塞,業務系統先將一批任務通知下發到緩存中,然後通過緩存中計數減數限制同時訪問外部系統的最大設備訪問量,從而達到限流目的。
實現思路:
爲保證執行任務的設備不超外部系統最大訪問量閾值限制,業務系統在緩存中計數,控制任務下發。
當設備通過週期上報業務系統查詢任務下發通知時,業務系統匹配到有該設備的任務下發通知,則獲取redis中的計數key,如果該key總數大於等於60則不進行任務下發等設備下一上報週期,再進行判斷。當該key總數小於60時,對該key進行加1,執行任務下發。設備獲取任務通知後,訪問外部系統執行任務,通過週期上報返回給業務系統執行結果。業務系統受到結果,對該key進行減1操作,保證下一設備在進行任務下發時不超外部系統最大同時訪問量閾值限制。
出現問題:
高併發下,多個設備可能同時獲取不超閾值的計數key,再執行加1操作後,計數key超過60最大閾值。或進行減1計數時,使該key瞬間變爲負數。同時考慮,假如業務系統同時下發了60個任務,由於網絡或者其他原因執行結果遲遲沒有回傳導致計數遲遲無法釋放,阻塞後續任務下發的情況。
解決方案:
1、在進行閾值範圍內查詢、加數和減數操作時,使用原子級操作處理。
2、對計數key和外部系統任務執行設置超時時間。
由於加數時,會先查詢計數key,然後對計數key進行判斷,不超閾值則加1,同時設置超時時間,需執行三條redis語句。故通過lua腳本進行實現,腳本String:
private static final String String arIncrStr="local count = redis.call('get',KEYS[1]);"
+ "if not count or tonumber(count) < tonumber(ARGV[1]) then "
+ "count=redis.call('incr',KEYS[1]);redis.call('EXPIRE', KEYS[1], 300);"
+ "end;"
+ "return tonumber(count)";
在進行減數時,會先查詢該key是否存在(該key會自動過期)且是否大於0(放置減1後爲負數),如果存在且大於0進行減1操作。lua腳本String:
private static final String arDecrStr="local count = redis.call('get',KEYS[1]);"
+ "if count and tonumber(count)>0 then "
+ "count=redis.call('incrby',KEYS[1],-1);"
+ "end;"
+ "return tonumber(count)";
在具體業務系統編碼中,基於Spring boot redisTemplate實現如下:
定義腳本類:
public class RedisLuaScript {
//ar測速計數加1
private static final String arIncrStr="local count = redis.call('get',KEYS[1]);"
+ "if not count or tonumber(count) < tonumber(ARGV[1]) then "
+ "count=redis.call('incr',KEYS[1]);redis.call('EXPIRE', KEYS[1], 300);"
+ "end;"
+ "return tonumber(count)";
public static final DefaultRedisScript<Long> arIncr=new DefaultRedisScript<>(arIncrStr,Long.class);
//ar測速計數減1
private static final String arDecrStr="local count = redis.call('get',KEYS[1]);"
+ "if count and tonumber(count)>0 then "
+ "count=redis.call('incrby',KEYS[1],-1);"
+ "end;"
+ "return tonumber(count)";
public static final DefaultRedisScript<Long> arDecr=new DefaultRedisScript<>(arDecrStr,Long.class);
}
腳本執行:
//加數
ArrayList<String> keyList = new ArrayList<>();
keyList.add(ConstantTable.AR + akaBean.getAreacode());
Long kdNum = stringRedisTemplate.execute(RedisLuaScript.arIncr, keyList,"60");
//如果超過閾值
if(kdNum!=null&&kdNum>=60){
...
}else{
...
}
//減數
ArrayList<String> keyList = new ArrayList<>();
keyList.add("ar:" + akaBean.getAreacode());
stringRedisTemplate.execute(RedisLuaScript.arDecr,keyList);