网站建设公司 电话销售没什么效果,网站首页设计布局方式,搜索优化引擎,免费客源软件【2023性能测试完整版】这可能是B站讲得最好的软件测试课程#xff08;Jmeter 接口测试实战 Loadrunner Tomcat综合教程#xff09;软件测试面试、自动化测试。 在拼夕夕面试中#xff0c;面试官问了一连串经典的问题#xff1a;“优惠券库存是怎么扣减的#xff1f;开发为… 【2023性能测试完整版】这可能是B站讲得最好的软件测试课程Jmeter 接口测试实战 Loadrunner Tomcat综合教程软件测试面试、自动化测试。 在拼夕夕面试中面试官问了一连串经典的问题“优惠券库存是怎么扣减的开发为了解决超发优惠券问题而设计的方案你了解过吗你又是如何测试的呢”
当时听到这些问题还挺懵的没遇到过超发问题啊开发设计的方案我怎么知道现在想起来还挺幼稚的其实现在想想电商中有很多类似的问题比如商品超卖归根究底就是一个问题那就是并发安全问题。
问题引入
就拿领取优惠券的问题来说
需求描述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 stockstock-1 where stock#{上一次的库存} and id #{id} and stock0
这种方法有个ABA的问题我们可以加个version字段每次修改数据的时候这个字段会加 1这样就可以避免 ABA 问题。但是这种依靠数据库进行并发安全保障会消耗数据库的资源一定请求量内需经过严格测试可使用。 update product set stockstock-1,versioin version1 where #{id} and stock0 and version#{上一次的版本号}
Redis分布式锁
引入 Redis 后当领取优惠券时会先去 Redis 里面去获取锁当锁获取成功后才可以对数据库进行操作。
在分布式锁中我们应该考虑如下
排他性在分布式集群中同一个方法在同一个时间只能被某一台机器上的一个线程执行 容错性当一个线程上锁后如果机器突然的宕机如果不释放锁此时这条数据将会被锁死 还要注意锁的粒度锁的开销 满足高可用高性能可重入。 伪代码如下
RestController
public class IndexController {public static final String REDIS_LOCK coupon_lock;AutowiredStringRedisTemplate template;RequestMapping(/getCoupon)public String getCoupon(){// 每个人进来先要进行加锁key值为good_lockString 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();}}}}
}
Redission 红锁
Redission红锁其实是上述redis分布式锁的升级版主要是框架已经封装好了我们需要的方法实际过程中只要引入相应的jar包使用对应的api即可。
Maven引入 org.redisson redisson 3.17.4 伪代码如下
public JsonData getCoupon(long couponId, CouponCategoryEnum categoryEnum) {String key lock:coupon: couponId;RLock rLock redisson.getLock(key);LoginUser loginUser LoginInterceptor.threadLocal.get();rLock.lock();try{//业务逻辑}finally {rLock.unlock();}return JsonData.buildSuccess();
}
使用这种方式也无需关心 key 过期时间续期的问题因为在 Redisson 一旦加锁成功就会启动一个 watch dog你可以将它理解成一个守护线程它默认会每隔 30 秒(可灵活配置)检查一下如果当前客户端还占有这把锁它会自动对这个锁的过期时间进行延长。
Zookeeper分布式锁
Zookeeper分布式锁应用了临时顺序节点。具体如何实现呢让我们来看一看详细步骤
获取锁
首先在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端Client1想要获得锁时需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后Client1查找ParentLock下面所有的临时顺序节点并排序判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点则成功获得锁。 这时候如果再有一个客户端 Client2 前来获取锁则在ParentLock下载再创建一个临时顺序节点Lock2。 Client2查找ParentLock下面所有的临时顺序节点并排序判断自己所创建的节点Lock2是不是顺序最靠前的一个结果发现节点Lock2并不是最小的。
于是Client2向排序仅比它靠前的节点Lock1注册Watcher用于监听Lock1节点是否存在。这意味着Client2抢锁失败进入了等待状态。
这时候如果又有一个客户端Client3前来获取锁则在ParentLock下载再创建一个临时顺序节点Lock3。 Client3查找ParentLock下面所有的临时顺序节点并排序判断自己所创建的节点Lock3是不是顺序最靠前的一个结果同样发现节点Lock3并不是最靠前的。
于是Client3向排序仅比它靠前的节点Lock2注册Watcher用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败进入了等待状态。 这样一来Client1得到了锁Client2监听了Lock1Client3监听了Lock2。这恰恰形成了一个等待队列很像是Java当中ReentrantLock所依赖的AQS。
怎么测试并发安全问题
首先我们要保证测试环境的项目是分布式、集群部署其次可以根据线上获取优惠券接口的实际QPS在测试环境使用工具jmeter并发请求优惠券接口运行一段时间后再去看下数据库相应的数据譬如优惠券库存信息抢购优惠券信息等等反复多次运行看下效果。
总结
本篇文章主要分享了电商项目中一种常见的并发安全问题以及相应的解决方案如果从性能的角度去考虑应该是Redis zookeeper 数据库。从可靠性安全性角度zookeeper Redis 数据库。下面是配套学习资料对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库这个仓库也陪伴我走过了最艰难的路程希望也能帮助到你 软件测试面试小程序
被百万人刷爆的软件测试题库谁用谁知道全网最全面试刷题小程序手机就可以刷题地铁上公交上卷起来
涵盖以下这些面试题板块 1、软件测试基础理论 2、web,app接口功能测试 3、网络 4、数据库 5、linux 6、web,app接口自动化 7、性能测试 8、编程基础9、hr面试题 10、开放性测试题11、安全测试12、计算机基础 文档获取方式
这份文档对于想从事【软件测试】的朋友来说应该是最全面最完整的备战仓库这个仓库也陪伴我走过了最艰难的路程希望也能帮助到你