业务场景:

一个时间区间内业务系统需给一批设备下发任务,该任务需设备依赖外部系统完成执行,外部系统最多支持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);