高并发场景下,使用redis lua脚本实现计数范围内原子级增减

业务场景:

一个时间区间内业务系统需给一批设备下发任务,该任务需设备依赖外部系统完成执行,外部系统最多支持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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis中,Lua脚本可以通过使用EVAL命令来实现原子性操作。通过将多个Redis命令组合在一个Lua脚本中执行,可以确保这些命令在同一时间内被连续地执行,从而保证原子性。引用 在Lua脚本中,可以使用redis.call()和redis.pcall()这两个不同的函数来调用Redis命令。redis.call()函数用于执行普通的Redis命令,而redis.pcall()函数则用于执行Redis命令并处理错误。通过使用这两个函数,可以在Lua脚本中执行多个Redis命令,并保证它们的原子性。引用 另外,Redis在执行Lua脚本时采用了单线程的方式,这意味着同一时间内只能执行一个Lua脚本,不会被其他脚本Redis命令打断。因此,在执行Lua脚本期间,不会发生并发问题,从而保证了原子性。引用 综上所述,通过将多个Redis命令组合在Lua脚本中,并使用redis.call()和redis.pcall()函数来执行这些命令,结合Redis的单线程执行机制,可以保证Lua脚本的原子性操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Redis Lua脚本实现原子性操作](https://blog.csdn.net/kuishao1314aa/article/details/120367618)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值