高併發遇到的坑

最近公衆號作了一個抽獎活動,遇到了同一個獎品被抽走兩次的狀況。經排查是事務的問題形成的,在這裏作個記錄。spring

首先讓我先來簡單闡述下業務:數據庫

每一個新用戶都會贈送一些抽獎機會,能夠經過用戶自行分享操做能夠額外增長抽獎次數。而且中獎概率會隨着抽獎次數的增長而減小。具體的接口以下。併發

/**
 * 抽獎
 *
 * @return
 */
@Transactional
public JsonResult lottery(int userId) {
    //判斷抽獎次數
    if (getLotteryNum(userId) == 0) {
        return JsonResult.error("沒有抽獎次數了");
    }
    //防止用戶屢次提交形成計算錯誤
    synchronized (this) {
        //判斷使用哪套抽獎規則
        int lotteryCount = lotteryRecordDao.countByUserId(userId);
        boolean hit = false;
        //若是沒有抽過獎,則有兩次機會,機率50%
        if (lotteryCount < 2) {
            hit = LotteryMath.lottery50();
            lotteryRecord(userId, "0");
            logger.info("userId:" + userId + "當前抽獎次數爲:" + lotteryCount + ",使用系統贈送抽獎");
        } else {
            String now = getNowZero();
            //判斷是否使用的是系統贈送抽獎次數
            if (lotteryRecordDao.countByUserIdAndCreateTimeAndType(userId, now, "0") == 0) {
                lotteryRecord(userId, "0");
                logger.info("userId:" + userId + "共抽:" + lotteryCount + "次,當天抽獎次數爲0,使用系統天天贈送抽獎");
            } else {
                //使用人氣積分抽獎
                lotteryRecord(userId, "1");
                logger.info("userId:" + userId + "共抽:" + lotteryCount + "次,使用人氣積分贈送抽獎");
            }

            if (lotteryCount > 2 && lotteryCount < 5) {
                //第3到5次33%機率
                hit = LotteryMath.lottery33();
            } else {
                //大於5次20%機率
                hit = LotteryMath.lottery20();
            }
        }

        if (hit) {
            return hited(userId);
        }
    }
    return JsonResult.error("抱歉您沒中獎");
}

斷定爲中獎時,執行hited方法。dom

/**
 * 勞資中獎啦
 *
 * @return
 */
private JsonResult hited(int userId) {
    //從獎池中隨機獲取一個獎品
    Giftpool giftpool = randomGetGift();
    if (giftpool != null) {
        //判斷是否中的是今日大獎
        if (giftpool.getGiftId() == 6) {
            String now = getNowZero();
            int count = lotteryRecordDao.countByCreateTime(now);
            if (count < 500) {
                logger.info("userId:" + userId + "是終極衰鬼,與今日大獎擦肩而過");
                return JsonResult.error("抱歉您沒中獎");
            }
        }
        Winner winner = WinRecord(userId, giftpool);
        logger.info("userId:" + userId + "中獎,中獎紀錄ID爲:" + winner.getId());
        //標記爲獎品已抽走
        giftpool.setIsHit("1").setUpdateTime(new Date());
        giftpoolDao.save(giftpool);
        logger.info("獎池ID爲:" + giftpool.getId() + "標記爲已抽走");
        return JsonResult.success(winner);
    }
    //填充獎池
    fillGiftpool();
    logger.info("衰鬼userId:" + userId + "原本中獎了,恰好碰上獎池填充,斷定爲不中了");
    //獎池爲空直接認定爲沒中獎
    return JsonResult.error("抱歉您沒中獎");
}

這個方法大概作的事情就是,從數據庫的獎池表中隨機抽取一個未被標記爲已抽中的獎品,記錄到中獎紀錄表中,並在獎池表將該產品標記爲已抽中。this

表結構以下線程

獎池表接口

中獎紀錄表隊列

    好了,業務描述完畢。乍一看好像沒什麼問題。可是實際在線上運行,因爲活動自身熱度,併發量至關大。在查中獎紀錄計算成本時,驚奇的發現,獎池中同一個獎品竟然被抽中了兩次。事務

    在編寫代碼時,我就曾考慮到併發量可能會很大,會出現獎品被屢次抽取,因此我在代碼中加了同步鎖,確保只有一個線程執行抽獎方法,這樣就不會出現同一個獎品被抽走兩次的狀況。可是事已願爲。get

    接下來分析緣由:雖然代碼中確保了一次只會有一個線程可以執行抽獎,可是spring的事務提交時間倒是不可控的,它不是個隊列,咱們並不知道spring會在何時提交事務。具體的說就是,兩個線程同時進入了該方法,其中一個線程執行完後,此時事務並沒立刻提交,第二個線程執行完後,兩個線程都提交了事務。此時同一個獎品就產生了兩條中獎紀錄。

    解決問題:加行鎖。

if (giftpoolDao.updateHit("1", giftpool.getId()) > 0) {
    Winner winner = WinRecord(userId, giftpool);
    logger.info("userId:" + userId + "中獎,中獎紀錄ID爲:" + winner.getId());
    //標記爲獎品已抽走
    giftpool.setIsHit("1").setUpdateTime(new Date());
    giftpoolDao.save(giftpool);
    logger.info("獎池ID爲:" + giftpool.getId() + "標記爲已抽走");
    return JsonResult.success(winner);
}
@Modifying
@Query("update Giftpool l set l.isHit=?1 where l.id=?2 and l.isHit=0")
int updateHit(String hit, int id);

當執行update時,會爲該條記錄加上鎖,直到事務處理完畢後才釋放。執行完後會返回一個影響了多少條記錄的數值,因此,當大於0時則表示執行成功。問題解決。

相關文章
相關標籤/搜索