在分布式高并发场景下,领取百万优惠码,怎样保证每个用户只能领取一个不超送


1,基本思路是:基于Redis实现分布式锁+幂等性;

2,具体实现逻辑:

使用redis的decr (对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。这个操作最大支持在64位有符号的整型数字。)可以实现原子性的递增递减操作控制优惠码不超送,然后给每个用户维护一个userid+优惠码活动的key保证幂等性,只要redis存在这种key,那就代表已经领取了,具体的优惠码分发可以异步执行。为了避免竞争(同一个用户,多个设备同时领取)使用Redis.setNX() 实现分布式锁(重复数据插入可用其来实现排他锁);

 3,相关核心代码逻辑

 

ApplicationContext context = new AnnotationConfigApplicationContext(RedisCacheConfiguration.class);
JedisPool jedisPool = (JedisPool)context.getBean("redisPoolFactory");

/**
 *  优惠劵发放主逻辑
 * @param user 用户数据
 * @param eventsPromoCodeMark 本次活动唯一标记
 */

public  void redisAcquireLockLock(User user, String eventsPromoCodeMark){
    //活动上线前设置优惠码总量一百万:jedisResources.set(promoCodeNumberKey, "1000000");  

    Jedis jedisResources = jedisPool.getResource();
    String lock = user.getId() + eventsPromoCodeMark;//设置本次活动优惠码锁(保证幂等性):userId + 优惠码活动的key
    String promoCodeNumberKey = eventsPromoCodeMark + "promoCodeNumber"; //设置优惠码总量key  
    String userKey = lock + "receiveCoupon";//设置用户优惠码领取key(自定义,只要保证一个用户在一个活动的唯一性即可)
    //优惠码领取库存校验
    boolean promoCode = jedisResources.exists(promoCodeNumberKey) &&
            StringUtils.isNotBlank(jedisResources.get(promoCodeNumberKey)) &&
            0L < Long.valueOf(jedisResources.get(promoCodeNumberKey));
    if(!promoCode){
        System.out.println("优惠码已经领取完了");
        jedisResources.close();
        return;
    }
    //为了避免竞争(同一个用户,多个设备同时领取).使用Redis.setNX() 实现分布式锁(重复数据插入可用其来实现排他锁)
    boolean flag = acquireLock(lock);
    if(flag){
        //1,获取锁成功,进行用户已领取标记,查询用户是否已经领取过
        Boolean isExists = jedisResources.exists(userKey);
        if(!isExists){
            //2,先使用redis的decr可以实现原子性的递增递减操作控制优惠码不超送,
            Long success = jedisResources.decr(promoCodeNumberKey);
            // 先减库存后发码(减库存后返回的现有库存数量大于等于0说明本次抢码成功,再进行发送优惠码,否则库存已经空了就不进行发送优惠码)
            if(success >= 0L){
                //3,再进行优惠码分发(可异步执行通过MQ进行发放,如果减库存成功,发放失败,进行发放补偿性操作)
                jedisResources.set(userKey, "received");
                System.out.println("领取成功");
                long oldValue = Long.valueOf(jedisResources.get(lock));//获取锁的旧值  
                if (oldValue > System.currentTimeMillis()) {//检查处理时间是否小于超时时间,释放锁
                    jedisResources.delete(lock);
                }
            }
        } else {
            System.out.println("已经领取过了");
        }
    } else {
        System.out.println("请重试");
    }
    jedisResources.close();
}



/**
 *  获取锁方法
 * @param lock 锁的k
 * @return boolean 本次获锁是否成功
 */
public  boolean acquireLock(String lock) {
    boolean success;//设置获锁成功标记
    Jedis jedis = jedisPool.getResource();
    int expired = 60 * 1000;//强制过期时间:60 秒
    long value = System.currentTimeMillis() + expired;
    long acquired = jedis.setnx(lock, String.valueOf(value)); //通过SETNX试图获取一个lock   
    if (1 == acquired) {//setnx 返回值为1则表示设置成功,则成功获取一个锁
        success = true;
    } else {//setnx 失败,说明锁仍然被其他对象保持,检查其是否已经超时
        long oldValue = Long.valueOf(jedis.get(lock));//获取锁的旧值  
        if (oldValue < System.currentTimeMillis()) {//检查是否超时
            String getOldValue = jedis.getSet(lock, String.valueOf(value));//超时,进行锁覆盖,并获取覆盖值之后的获取到的旧值
            // 进行最终获取锁是否成功原子性校验(覆盖旧值之前获取到的旧值 和 覆盖值之后的获取到的旧值 相同 则本次获锁成功,否则 失败已经被别的对象获取到)
            if (Long.valueOf(getOldValue) == oldValue) {
                success = true;
            } else {// 已被其他进程捷足先登了
                success = false;
            }
        } else {//未超时,则直接返回获取锁失败
            success = false;
        }
    }
    jedisPool.close();
    return success;
}

 

 获锁具体的使用步骤如下:

      1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。

      2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。

      3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。

      4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

      5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM