软件测试商城项目优惠券超发问题该怎么测试?_优惠系统测试点(全过程)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

需求描述:A 优惠券一共发行 100张,每一个用户最多可以领取5张。

当一个用户领取优惠券成功的时候,把领取的记录写入另外一个表中(这张表我们暂且称为表 B)。

在领取优惠券的过程中,优惠券库存的扣减过程,一般操作如下:

1、select查询优惠券的库存。

2、计算优惠券库存是否足够,如果优惠券存库不足则抛出库存不足的异常,如果优惠券库存足够,则判断是否在领取时间、判断用户领取数量是否超过个人最高领取限制。

3、如果2成立,则减去扣除的库存得到最新的库存剩余值。

4、set设置最新的优惠券库存剩余值

伪代码如下:

扣减优惠券sql如下:

update coupon set stock = stock - 1 where id = #{coupon_id}

并发量比较低的时候,几乎看不出来有问题,可是当我们开启多线程,去请求这个抢优惠券的接口时,问题出现了,id为19的这个优惠券库存为负数。多发了一个,什么原因呢?

深入解读并发安全问题

为啥并发量高的时候会出现优惠券库存多发的问题呢?原因如下截图:

上图中出现问题的环节其实是判断优惠券库存那个步骤,重点来了:

高并发情况下,如果同时来了两个线程线程 A和线程 B(可以理解成是两个请求),比如先来的那个线程A请求通过了检查,这时线程 A 还没有扣减库存,这时经过一番操作,线程B也通过了这个检查优惠券是否可领取的方法,然后线程 A 和线程 B 依次扣减库存或者是同时扣减库存。所以就出现了刚刚数据库出现的现象,优惠券库存为-1个,就像下图。

怎么解决并发安全问题?

Java 代码加锁

synchronized (this){
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    CouponDO couponDO = couponMapper.selectOne(new QueryWrapper()
                                    .eq("id", couponId)
                                    .eq("category", categoryEnum.name()));
    if(couponDO == null){
        throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
    }
    this.checkCoupon(couponDO,loginUser.getId());

    //构建领券记录
    CouponRecordDO couponRecordDO = new CouponRecordDO();
    BeanUtils.copyProperties(couponDO,couponRecordDO);
    couponRecordDO.setCreateTime(new Date());
    couponRecordDO.setUseState(CouponStateEnum.NEW.name());
    couponRecordDO.setUserId(loginUser.getId());
    couponRecordDO.setUserName(loginUser.getName());
    couponRecordDO.setCouponId(couponDO.getId());
    couponRecordDO.setId(null);

    int row = couponMapper.reduceStock(couponId);
    if(row == 1){
        couponRecordMapper.insert(couponRecordDO);
    }else{
        log.info("发送优惠券失败:{},用户:{}",couponDO,loginUser);
    }
}

加个synchronized关键字,这样每个请求都得排队执行这个扣减库存操作,可以一定程度解决并发安全问题,但由于synchronized关键字基于jvm级别加锁,当集群环境下,有多个jvm进程,所以这种方法仅适用于单机节点。

Sql版本号

update product set stock=stock-1 where stock=#{上一次的库存}  and id = #{id} and stock>0

这种方法有个ABA的问题,我们可以加个version字段,每次修改数据的时候这个字段会加 1,这样就可以避免 ABA 问题。但是这种依靠数据库进行并发安全保障,会消耗数据库的资源,一定请求量内(需经过严格测试)可使用。

     update product set stock=stock-1,versioin = version+1 where   #{id} and stock>0 and version=#{上一次的版本号}

Redis分布式锁

引入 Redis 后,当领取优惠券时会先去 Redis 里面去获取锁,当锁获取成功后才可以对数据库进行操作。

在分布式锁中我们应该考虑如下:

  • 排他性,在分布式集群中,同一个方法,在同一个时间只能被某一台机器上的一个线程执行;
  • 容错性,当一个线程上锁后,如果机器突然的宕机,如果不释放锁,此时这条数据将会被锁死;
  • 还要注意锁的粒度,锁的开销;
  • 满足高可用,高性能,可重入。

伪代码如下:

@RestController
public class IndexController {

    public static final String REDIS_LOCK = "coupon_lock";

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/getCoupon")
    public String getCoupon(){

        // 每个人进来先要进行加锁,key值为"good_lock"
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            // 为key加一个过期时间
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);
            // 加锁失败
            if(!flag){
                return "抢锁失败!";
            }
            System.out.println( value+ " 抢锁成功");
            String result = template.opsForValue().get("coupon:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 在此处需要处理抢购优惠券业务,处理时间较长。。。
                int realTotal = total - 1;
                template.opsForValue().set("coupon:001", String.valueOf(realTotal));
                System.out.println("获取优惠券成功,库存还剩:" + realTotal + "件, 服务端口为8001");
                return "获取优惠券成功,库存还剩:" + realTotal + "件, 服务端口为8001";
            } else {
                System.out.println("获取优惠券失败,服务端口为8001");
            }
            return "获取优惠券失败,服务端口为8001";
        }finally {
            // 谁加的锁,谁才能删除,使用Lua脚本,进行锁的删除
            Jedis jedis = null;
            try{
                jedis = RedisUtils.getJedis();
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";

                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if("1".equals(eval.toString())){
                    System.out.println("-----del redis lock ok....");
                }else{
                    System.out.println("-----del redis lock error ....");
                }
            }catch (Exception e){

            }finally {
                if(null != jedis){
                    jedis.close();
                }
            }
        }
    }


![img](https://img-blog.csdnimg.cn/img_convert/b198087a3deb34a2b382a6d9af157829.png)
![img](https://img-blog.csdnimg.cn/img_convert/12e84b2dd8267a8b737ef8d4a11b3539.png)
![img](https://img-blog.csdnimg.cn/img_convert/81494424515ea3014ae47d9456f5610e.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化的资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值