美文网首页
多线程加锁(抽奖活动用户重复中奖的bug查询)

多线程加锁(抽奖活动用户重复中奖的bug查询)

作者: 燃灯道童 | 来源:发表于2022-12-16 11:48 被阅读0次

问题背景:
原来写了一段代码,经反馈出现了bug。抽奖活动规则不支持重复中奖,但是同一个用户中了两个五等奖,奖项是一个优惠code,一个用户只能领一个。但是用户查询自己的中奖纪录的时候是有两个。

抽奖的逻辑代码:
进入抽奖方法,先对该用户进行加锁;如果该用户没有被加锁,则执行抽奖的流程;然后更新礼品的库存;保存用户中奖记录;最后释放锁。

        String key = "dec_lottery_lock_" + request.getOpenId();
        long time = System.currentTimeMillis();
        if (!RedisUtil.tryLock(key, String.valueOf(time))) {
            log.info("the key is "+key+" in LotteryDrawService locked.");
            return null;
        }
        try {
            // call lottery logic
             responseDTO = toDrow(request,lotterys,lotteryConfig,counter);

            // update inventory (if it is ‘thanks for your participation’, don't need to update the inventory)
            if(responseDTO.getGiftName().indexOf("谢谢")==-1){
                updateGiftInventory(responseDTO.getGiftId(),request.getSettingId());
            }

            // save winning records
            saveAwardRecord(request, responseDTO);
        } catch (Exception e) {
            log.error("an exception is appear when lottery draw,the exception information is: "+e);
            return responseDTO;
        } finally {
            //unlock
            RedisUtil.unlock(key, String.valueOf(time));
        }

进入抽奖方法的加锁解锁的逻辑代码

//加锁
    public static Boolean tryLock(String key, String value) {
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题  所以多一次判断保证线程的安全
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
            if (StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

//解锁
    public static void unlock(String key, String value) {
        String currentValue = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotEmpty(currentValue) && currentValue.equals(value)) {
            stringRedisTemplate.opsForValue().getOperations().delete(key);
        }
    }

刚看到这个问题有点懵,一直考虑的方向是代码的事务问题,当用户存储中奖纪录的事务还没提交的时候,新的线程又进来了。但是数据库中存储了两条,相隔时间为1s。如果是事务的问题不应该存入两条且间隔时间这么长。
静下心来重新捋下代码,重新看加锁的逻辑。

原因:
原来是加锁的逻辑,为了防止多线程请求导致数据错乱,加锁时添加了双重判断。第二层的判断原意是好的,但也是导致这个问题的原因。可以看接下来的代码。
同一个用户在锁还没释放的时候,再次进入请求方法的时候应直接返回null;如上加锁代码,在判断多线程时,取得redis中的当前值和原来的值其实是相等的,所以在抽奖方法没走完之前,该用户再次请求进来依旧能抽奖,导致同一个用户中奖了两次。

加锁的逻辑,主要涉及redis的setIfAbsent和getAndSet两个方法。
setIfAbsent 键不存在则新增,返回true;键存在则不改变已经有的值,无法赋值,返回false
getAndSet 获取原来key键对应的值 并重新赋新值

尝试还原原意
一,是多线程的时候,第二个线程进来的时候,更新redis的值。但是这样也是不解决问题,必须让用户第一次抽奖未结束,第二次进来时给提前返回才能解决问题。

    public static Boolean tryLock(String key, String value) {
        //setIfAbsent 键不存在则新增,返回true;键存在则不改变已经有的值,无法赋值,返回false
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
//        String currentValue = stringRedisTemplate.opsForValue().get(key);
        String currentValue = value;

        if (StringUtils.isNotEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题  所以多一次判断保证线程的安全
            //getAndSet 获取原来key键对应的值 并重新赋新值
            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
//            String oldValue = stringRedisTemplate.opsForValue().get(key);
            log.info("锁中原来的值为:{},当前的值为:{},结果值为:{}", oldValue, currentValue, oldValue.equals(currentValue));
            if (StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

二, 是第二个线程进来的时候进行判断,如果只是判断的话,这段代码删掉,保留第一个即可。

    public static Boolean tryLock(String key, String value) {
        //setIfAbsent 键不存在则新增,返回true;键存在则不改变已经有的值,无法赋值,返回false
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
//        String currentValue = stringRedisTemplate.opsForValue().get(key);
        String currentValue = value;

        if (StringUtils.isNotEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间 如果高并发的情况可能会出现已经被修改的问题  所以多一次判断保证线程的安全
//            String oldValue = stringRedisTemplate.opsForValue().getAndSet(key, value);
            String oldValue = stringRedisTemplate.opsForValue().get(key);
            log.info("锁中原来的值为:{},当前的值为:{},结果值为:{}", oldValue, currentValue, oldValue.equals(currentValue));
            if (StringUtils.isNotEmpty(oldValue) && oldValue.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

调整成这样之后,表中还是会存在多条数据,看了value值为获取当前时间的毫秒数才明白,
System.currentTimeMillis()获取当前的总毫秒数,1秒=1000毫秒(ms),当我进行压测1秒500的时候会存入数据,1秒1000个线程时存入的数据更多了。因为1秒拆分成1000份执行代码的时候,存入redis中的新值和老值相等的概率还是很大的。

最终调整的代码如下,再次用压测工具验证成功。

    public static Boolean tryLock(String key, String value) {
        //setIfAbsent 键不存在则新增,返回true;键存在则不改变已经有的值,无法赋值,返回false
        if (stringRedisTemplate.opsForValue().setIfAbsent(key, value)) {
            return true;
        }
        return false;
    }

做压测的时,单个用户高并发的进行抽奖的场景验证得不充分,单人1s请求上百次的场景没有测试到。单人瞬间多次请求抽奖接口,正常情况下比较少见,但是也不排除恶意攻击。前端可以在上一个接口没有返回的时候把按钮置灰,不让用户进行点击。当然后端也要做相应的处理,前后端双重保险更安全。

相关文章

  • 北京免费密室逃脱

    此活动为免费抽奖活动,最终中奖用户我们会通过电话确认,请中奖的用户凭中奖短信前去体验,凭报名成功短信不可前去体验哦...

  • 阿里云注册登陆抽奖活动,最高888元无门槛代金券,100%中奖

    注册有礼活动是阿里云为新用户注册并登陆账号而推出的抽奖活动,用户完成注册并登陆活动页面即可参与抽奖,100%中奖,...

  • 中奖推送设置

    近期,我们更新了一个特殊的“中奖推送”功能,在以往的抽奖或游戏中奖中,用户中奖数据只能在后台查到,且活动后兑换时找...

  • 抽奖小程序结果页设计

    今天开始迭代抽奖小程序,之前做过一个抽奖活动只有一人中奖的场景,但是在新版抽奖小程序支持每个奖项可以多人中奖,这个...

  • 『Android自定义View实战』自定义完美的刮刮乐效果

    前言 在很多电商或者金融类App中,经常会有各种线上抽奖活动,为了提高用户的交互性,让用户对中奖的体验度更为真实,...

  • 中奖了!

    文/安小虾 很少抽奖能中奖的我,这次居然中奖了。 我参加的是知乎的活动,也就抱着随手转发也不难的心理参加了这次抽奖...

  • 电子工程师福利看这里

    看到这个活动在这里给各位老铁分享一下,回馈用户抽奖的,完全凭运气了,希望各位老铁能中奖,哈哈

  • 抽奖活动API

    抽奖活动API 获取参与抽奖名单 分页获取参与抽奖名单 导出参与名单的报表模板 导入参与名单 内定中奖名单列表 获...

  • Easy UI弹出加载中(loading)提示的两种方法

    最近项目的一个查询耗时比较久,用户点击查询没有反应可能造成重复点击,并且以为是bug,所以为了用户体验好一点想弹出...

  • 抽奖

    1.获取活动信息 2.获取抽奖结果 3.中奖算法DPrizeHelper::prize();

网友评论

      本文标题:多线程加锁(抽奖活动用户重复中奖的bug查询)

      本文链接:https://www.haomeiwen.com/subject/cbbgfdtx.html