在分布式高並發場景下,領取百萬優惠碼,怎樣保證每個用戶只能領取一個不超送


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