zl程序教程

您现在的位置是:首页 >  其它

当前栏目

优惠券超发问题

问题 优惠券 超发
2023-06-13 09:14:14 时间

前言

做商城相关的小伙伴经常会有优惠劵的需求,如果没有处理好,很容易导致优惠劵超发,超出领取一系列的问题,影响还是很大的。

测试需求描述

现有一个优惠劵,库存有150张,测试用户领取该优惠劵。

实现过程

1、新建优惠劵表 coupon

2、新建领取优惠劵记录表 user_coupon_records

3、使用springboot+mybatisplus实现需求,这里只展示实现代码,其它相关代码就不做展示了

@Override
@Transactional(rollbackFor = Exception.class)
public Result claim() {
    //获取优惠劵是否有库存
    Coupon coupon = couponMapper.selectOne(new QueryWrapper<Coupon>()
            .eq("id", 1));
    if (coupon.getStock() < 1) {
        throw new MyException("库存不足");
    }
    //减少库存
    int u = couponMapper.inventoryReduction();
    if (u == 1) {
        //添加领取记录
        UserCouponRecords userCouponRecords = new UserCouponRecords();
        userCouponRecords.setUserId(1).setCouponId(1);
        userCouponRecordsMapper.insert(userCouponRecords);
    }
    return ResultUtil.error();
}
@Update("update coupon set stock = stock - 1")
int inventoryReduction();

上面的代码按照我们的逻辑是没有问题,我通过使用 apifox 软件测试也是没有问题,但是如果在高并发下就会出现问题了。

我们通过apifox 软件测试一下多线程下是什么效果。(4个线程)

可以看到库存与领取记录结果,竟然多领取了一张。

问题引发

如果同时来了两个线程(你可以理解成是两个请求),比如先来的那个请求通过了检查(线程 A),这时线程 A 还没有扣减库存,这时线程 B 经过一翻操作也通过了这个检查优惠券是否可领取的方法,然后线程 A 和线程 B 依次扣减库存或者是同时扣减库存。这样就会引发优惠劵超领的情况。

问题解决

| 解决方案 1(Java 代码加锁)

导致这一问题的根本原因是多个线程同时访问这个领取优惠券的方法,那只要保证在同一段只有一个线程进入到这个方法就可以了。

可以将代码这样改 synchronized (this){}

synchronized (this) {
    //获取优惠劵是否有库存
    Coupon coupon = couponMapper.selectOne(new QueryWrapper<Coupon>()
            .eq("id", 1));
    if (coupon.getStock() < 1) {
        throw new MyException("库存不足");
    }
    //减少库存
    int u = couponMapper.inventoryReduction();
    if (u == 1) {
        //添加领取记录
        UserCouponRecords userCouponRecords = new UserCouponRecords();
        userCouponRecords.setUserId(1).setCouponId(1);
        userCouponRecordsMapper.insert(userCouponRecords);
    }
    return ResultUtil.error();
}

虽然这样可以解决超发的问题,但是在项目中我们不可以这样写,原因如下:

synchronized 的作用范围是单个 JVM 实例,如果是集群部署系统这里的加锁你可以理解成失效。

在使用了 synchronized 加锁后,就会形成串行等待的问题,当一个线程 A 在领取优惠券方法内执行过久时,其它线程会等待直到线程 A 执行结束。

| 解决方案 2 (SQL层面解决)

@Update("update coupon set stock = stock - 1 where stock > 0")
int inventoryReduction();

发现了吗,只需要将 sql 加个库存判断即可!

MySQL 默认使用的是 InnoDB 引擎,使用 InnoDB 时在修改某一个记录的时候会将这条记录上锁,所以这个修改数据时不会出现多个线程同时修改数据。这样也可以避免优惠券超领。

还有种办法就是乐观锁,可以在表中加个version 字段,每次修改数据的时候这个字段会加 1,也可以直接使用mybatisplus中的乐观锁插件。

@Update("update coupon set stock = stock - 1,versioin = version+1 where version=#{上一次的版本号}")
int inventoryReduction();

这里就不演示了,可以自己去试试。