- 3.1 方式一(改进版加锁)
- 3.2 方式二(AOP版加锁)
- 3.3 方式三(悲观锁一)
- 3.4 方式四(悲观锁二)
- 3.5 方式五(乐观锁)
- 3.6 方式六(阻塞队列)
- 3.7.方式七(Disruptor队列)
1.引言
高并发场景在现场的日常工作中很常见,特别是在互联网公司中,这篇文章就来通过秒杀商品来模拟高并发的场景。文章末尾会附上文章的所有代码、脚本和测试用例。
- 本文环境: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger2.9.2
- 模拟工具: Jmeter
- 模拟场景: 减库存->创建订单->模拟支付
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
2.商品秒杀-超卖
在开发中,对于下面的代码,可能很熟悉:在Service里面加上@Transactional
事务注解和Lock锁
控制层:Controller
@ApiOperation(value="秒杀实现方式——Lock加锁") @PostMapping("/start/lock") public Result startLock(long skgId){ try { log.info("开始秒杀方式一..."); final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000; Result result = secondKillService.startSecondKillByLock(skgId, userId); if(result != null){ log.info("用户:{}--{}", userId, result.get("msg")); }else{ log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!"); } } catch (Exception e) { e.printStackTrace(); } finally { } return Result.ok(); }
业务层:Service
@Override @Transactional(rollbackFor = Exception.class) public Result startSecondKillByLock(long skgId, long userId) { lock.lock(); try { // 校验库存 SecondKill secondKill = secondKillMapper.selectById(skgId); Integer number = secondKill.getNumber(); if (number > 0) { // 扣库存 secondKill.setNumber(number - 1); secondKillMapper.updateById(secondKill); // 创建订单 SuccessKilled killed = new SuccessKilled(); killed.setSeckillId(skgId); killed.setUserId(userId); killed.setState((short) 0); killed.setCreateTime(new Timestamp(System.currentTimeMillis())); successKilledMapper.insert(killed); // 模拟支付 Payment payment = new Payment(); payment.setSeckillId(skgId); payment.setSeckillId(skgId); payment.setUserId(userId); payment.setMoney(40); payment.setState((short) 1); payment.setCreateTime(new Timestamp(System.currentTimeMillis())); paymentMapper.insert(payment); } else { return Result.error(SecondKillStateEnum.END); } } catch (Exception e) { throw new ScorpiosException("异常了个乖乖"); } finally { lock.unlock(); } return Result.ok(SecondKillStateEnum.SUCCESS); }
对于上面的代码应该没啥问题吧,业务方法上加事务,在处理业务的时候加锁。
但上面这样写法是有问题的,会出现超卖的情况,看下测试结果:模拟1000个并发,抢100商品
Jmeter不了解的,可以参考这篇文章:
这里在业务方法开始加了锁,在业务方法结束后释放了锁。但这里的事务提交却不是这样的,有可能在事务提交之前,就已经把锁释放了,这样会导致商品超卖现象。所以加锁的时机很重要!
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
3. 解决商品超卖
对于上面超卖现象,主要问题出现在事务中锁释放的时机,事务未提交之前,锁已经释放。(事务提交是在整个方法执行完)。如何解决这个问题呢,就是把加锁步骤提前
- 可以在controller层进行加锁
- 可以使用Aop在业务方法执行之前进行加锁
3.1 方式一(改进版加锁)
@ApiOperation(value="秒杀实现方式——Lock加锁") @PostMapping("/start/lock") public Result startLock(long skgId){ // 在此处加锁 lock.lock(); try { log.info("开始秒杀方式一..."); final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000; Result result = secondKillService.startSecondKillByLock(skgId, userId); if(result != null){ log.info("用户:{}--{}", userId, result.get("msg")); }else{ log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!"); } } catch (Exception e) { e.printStackTrace(); } finally { // 在此处释放锁 lock.unlock(); } return Result.ok(); }
上面这样的加锁就可以解决事务未提交之前,锁释放的问题,可以分三种情况进行压力测试:
- 并发数1000,商品100
- 并发数1000,商品1000
- 并发数2000,商品1000
对于并发量大于商品数的情况,商品秒杀一般不会出现少卖的请况,但对于并发数小于等于商品数的时候可能会出现商品少卖情况,这也很好理解。
对于没有问题的情况就不贴图了,因为有很多种方式,贴图会太多
3.2 方式二(AOP版加锁)
对于上面在控制层进行加锁的方式,可能显得不优雅,那就还有另一种方式进行在事务之前加锁,那就是AOP
自定义AOP注解
@Target({ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ServiceLock { String description() default ""; }
定义切面类
@Slf4j @Component @Scope @Aspect @Order(1) //order越小越是最先执行,但更重要的是最先执行的最后结束 public class LockAspect { /** * 思考:为什么不用synchronized * service 默认是单例的,并发下lock只有一个实例 */ private static Lock lock = new ReentrantLock(true); // 互斥锁 参数默认false,不公平锁 // Service层切点 用于记录错误日志 @Pointcut("@annotation(com.scorpios.secondkill.aop.ServiceLock)") public void lockAspect() { } @Around("lockAspect()") public Object around(ProceedingJoinPoint joinPoint) { lock.lock(); Object obj = null; try { obj = joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(); } finally{ lock.unlock(); } return obj; } }
在业务方法上添加AOP注解
@Override @ServiceLock // 使用Aop进行加锁 @Transactional(rollbackFor = Exception.class) public Result startSecondKillByAop(long skgId, long userId) { try { // 校验库存 SecondKill secondKill = secondKillMapper.selectById(skgId); Integer number = secondKill.getNumber(); if (number > 0) { //扣库存 secondKill.setNumber(number - 1); secondKillMapper.updateById(secondKill); //创建订单 SuccessKilled killed = new SuccessKilled(); killed.setSeckillId(skgId); killed.setUserId(userId); killed.setState((short) 0); killed.setCreateTime(new Timestamp(System.currentTimeMillis())); successKilledMapper.insert(killed); //支付 Payment payment = new Payment(); payment.setSeckillId(skgId); payment.setSeckillId(skgId); payment.setUserId(userId); payment.setMoney(40); payment.setState((short) 1); payment.setCreateTime(new Timestamp(System.currentTimeMillis())); paymentMapper.insert(payment); } else { return Result.error(SecondKillStateEnum.END); } } catch (Exception e) { throw new ScorpiosException("异常了个乖乖"); } return Result.ok(SecondKillStateEnum.SUCCESS); }
控制层:
@ApiOperation(value="秒杀实现方式二——Aop加锁") @PostMapping("/start/aop") public Result startAop(long skgId){ try { log.info("开始秒杀方式二..."); final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000; Result result = secondKillService.startSecondKillByAop(skgId, userId); if(result != null){ log.info("用户:{}--{}", userId, result.get("msg")); }else{ log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!"); } } catch (Exception e) { e.printStackTrace(); } return Result.ok(); }
这种方式在对锁的使用上,更高阶、更美观!