高并发遇到的坑

最近公众号做了一个抽奖活动,遇到了同一个奖品被抽走两次的情况。经排查是事务的问题造成的,在这里做个记录。

首先让我先来简单阐述下业务:

每个新用户都会赠送一些抽奖机会,可以通过用户自行分享操作可以额外增加抽奖次数。并且中奖几率会随着抽奖次数的增加而减少。具体的接口如下。

/**
 * 抽奖
 *
 * @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方法。

/**
 * 劳资中奖啦
 *
 * @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("抱歉您没中奖");
}

这个方法大概做的事情就是,从数据库的奖池表中随机抽取一个未被标记为已抽中的奖品,记录到中奖纪录表中,并在奖池表将该产品标记为已抽中。

表结构如下

奖池表

152316_9LkH_2474041.png

中奖纪录表

152411_VMEp_2474041.png

    好了,业务描述完毕。乍一看好像没什么问题。但是实际在线上运行,由于活动自身热度,并发量相当大。在查中奖纪录计算成本时,惊奇的发现,奖池中同一个奖品居然被抽中了两次。

    在编写代码时,我就曾考虑到并发量可能会很大,会出现奖品被多次抽取,所以我在代码中加了同步锁,确保只有一个线程执行抽奖方法,这样就不会出现同一个奖品被抽走两次的情况。但是事已愿为。

    接下来分析原因:虽然代码中确保了一次只会有一个线程能够执行抽奖,但是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时则表示执行成功。问题解决。

转载于:https://my.oschina.net/u/2474041/blog/816270

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值