- 1.引言
- 2.商品秒杀-超卖
- 3.解决商品超卖
- 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
- 模拟场景: 减库存->创建订单->模拟支付
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不了解的,可以参考这篇文章:
https://blog.youkuaiyun.com/zxd1435513775/article/details/106372446
这里在业务方法开始加了锁,在业务方法结束后释放了锁。但这里的事务提交却不是这样的,有可能在事务提交之前,就已经把锁释放了,这样会导致商品超卖现象。所以加锁的时机很重要!
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);