发奖控制数量的几种方式(锁,数据库锁,分布式锁,无锁)

8 篇文章 0 订阅
1 篇文章 0 订阅

在我们日常编码过程中,做营销类系统时,一定会遇到发奖、秒杀等业务,在这些业务中,奖品数量的控制尤为重要,如果没控制好,多发了奖品,会对运营成本造成超支,或者影响用户体验。本篇就介绍几种数量控制方案,供大家讨论。

一:通过锁控制数量

最简单直白的方式就是通过java自带的锁来控制数量的发放,伪代码如下:

    synchronized(this) {
        int count = countByPrizeId(prizeId);
        if (count > totalCount) {
            throw new BizException("奖品数量不足");
        }
        fetch(userId, prizeId);
    }

这种模式的优点在于:代码简单易读,对于程序员的要求很低。

风险点分析

  1. synchronized只能在单机环境中生效,如果分布式/集群部署,则无法控制数量
  2. 程序中形成单点,所有线程都会竞争这个锁,若随着数据量增大,锁内部操作需要时间增长,则会导致大量线程阻塞,最终使服务宕掉。

二:通过数据库控制数量

针对synchronized只能在单机环境中使用的问题,此处考虑将锁放到jvm之外,这里就引入数据库行级锁控制:

比如在奖品表中加入已发放数量,每次通过更新数量,成功则插入用户流水,失败则返回奖品发完,如下:

    UPDATE T_PRIZE SET FETCH_COUNT = FETCH_COUNT + 1 WHERE FETCH_COUNT < TOTAL_COUNT;

又或者不增加发放数量字段,通过mysql的insert into select方式能达到一样的效果:

    INSERT INTO T_USER_PRIZE SELECT field FROM DUAL WHERE (SELECT COUNT(*) FROM T_USER_PRIZE WHERE T_PRIZE_ID = '1') < '${TOTAL_COUNT}'

这种模式的优点在于:容易理解,且针对业务量小的发奖来说,QPS能达到要求。

风险点分析

  1. 所有压力都堆到了数据库,一旦量超出预期,很容易造成数据库挂掉。

三:通过分布式锁控制数量

通过分布式锁控制数量是一种比较流行的方案,此处用redis做示例,伪代码如下:

    // redis锁
    private boolean tryLock(String key) {
        redisTemplate.execute(connection -> {
            while(此处定义获取锁失败重试次数){
                if(connection.setNX(key, LocalDateTime.now().getTime() + EXPIRE_TIME)) {
                    return true;
                }
                // 取出时间判断过期了,则新来的key获取锁
                Long oldTime = connection.get(key);
                if(oldTime > LocalDateTime.now().getTime() + EXPIRE_TIME)) {
                    connection.set(key, LocalDateTime.now().getTime() + EXPIRE_TIME);
                    return true;
                }
            }
            return false;
        })
    }
    // redis释放锁
    private void deleteKey(String key) {
        redisTemplate.execute(connection -> connection.del(key);
    }
    //调用
    public void fetch(Long prizeId, String userId) {
        // 每个奖品一个锁
        String key = REDIS_KEY_PREFIX + prizeId;
        try() {
            if(tryLock(key)) {
                int count = countByPrizeId(prizeId);
                if (count > totalCount) {
                    throw new BizException("奖品数量不足");
                }
                fetch(userId, prizeId);
            }
        } finally {
            // 释放锁
            deleteKey(key);
        }
    }

代码中大致写了通过redis实现分布式锁的方式,代码为在markdown中纯手打,如果错误请自行改正。

使用分布式锁有点在于:

  1. key分散,每个奖品一个锁,锁竞争变少。(当然,用synchronized也有锁分散方案)。
  2. redis操作都在内存中,效率高,不考虑数据库因素,单机能达到近十万级别QPS。

风险点分析:

  1. 数据量超过预期,一样会造成程序宕机。
  2. redis一旦挂掉或出现通信障碍,整个流程将会不可用(通过集群部署等方案解决)。

四:无锁控制数量

一旦加上锁,效率肯定会变低,这里介绍一种无锁控制数量的方式来实现,同样基于redis来实现,伪代码如下:

    // 此方法中简化对redis操作的代码,只写出调用的哪个方法,不再具体写connection那些。
    public void fetch(Long prizeId, String userId) {
        // 每个奖品一个锁
        String key = REDIS_KEY_PREFIX + prizeId;
        if (redisTemplate.get(key) == null) {
            // 未发奖时统计出数据为0,发奖后统计出具体发放数据
            int count = countByPrizeId(prizeId);
            // 注意此处使用setNX,不能使用set,否则会有并发问题。
            redisTemplate.setNX(key, count);
        }
        // redis对于key中的值自增,并返回自增后的值
        Long count = redisTemplate.incrr(key);
        if (count > totalCount) {
            throw new BizException("奖品数量不足");
        }
        fetch(userId, prizeId);
    }

此方法原理为使用redis的incrr方法做一个奖品数量的计数器,每个请求进来都会调用自增拿到最新的数量,之后可进行校验。

此方式优点:未使用锁,效率更高。

风险点分析:

  1. 同样依赖于redis,redis挂掉会导致程序不可用。
  2. 在缓存未命中时去查库的操作,可能会造成同时大批请求进入db,导致db挂掉。可通过预先放入缓存并将缓存的失效时间设置超过活动时间的方式解决。
  3. 执行incrr方法之后,若程序抛出异常,可能会造成奖品少发的情况。

以上四种方式,各有优劣,我们可以根据自己的业务进行选择。技术始终是服务于业务的,比如我们做一个发奖活动,用户只有几十个人,那么肯定是选择直接使用synchronized即可,没必要去搞那些分布式锁等等东西。技术方案很多,选择适合我们当前业务场景的即可!


我的博客:www.scarlettbai.com

好的,以下是一个简单的用 Java 实现的竞猜发奖代码: ```java // 奖励发放方法 public void awardPrizes(List<Winner> winners) { for (Winner winner : winners) { if (winner.getPrizeType() == PrizeType.VIRTUAL) { // 发放虚拟奖励 sendVirtualPrize(winner); } else if (winner.getPrizeType() == PrizeType.REAL) { // 发放实物奖励 sendRealPrize(winner); } } } // 发放虚拟奖励方法 public void sendVirtualPrize(Winner winner) { // 调用发送邮件/短信等的接口进行奖励发放 String recipient = winner.getEmail(); // 获奖者邮箱 String content = "恭喜您获得竞猜活动的奖励,奖励内容为:" + winner.getPrizeName(); sendEmail(recipient, content); // 发送邮件 } // 发放实物奖励方法 public void sendRealPrize(Winner winner) { // 调用快递公司等的接口进行奖励发放 String recipient = winner.getAddress(); // 获奖者地址 String content = "恭喜您获得竞猜活动的奖励,奖励内容为:" + winner.getPrizeName(); sendParcel(recipient, content); // 发送快递 } ``` 在上述代码中,`Winner` 类表示获奖者信息,包括姓名、奖励种类、数量、发放时间等;`PrizeType` 表示奖励种类,包括虚拟奖品和实物奖品;`sendVirtualPrize` 和 `sendRealPrize` 分别表示发放虚拟奖励和实物奖励的方法,需要根据具体情况进行实现;`awardPrizes` 方法则是对获奖者列表进行遍历,根据奖励种类调用不同的发放方法进行奖励的发放。 需要注意的是,在实际应用中,还需要考虑奖励发放的安全性和可靠性,如数据加密、接口访问权限控制、错误处理等问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值