我们利用分布式ID完成了优惠券秒杀的下单基本功能,但是在高并发场景下可能会存在超卖问题。我们将数据库中的秒杀券库存恢复到100,并且清空优惠券订单表,使用JMeter来还原一下单体架构下的一人下多单超卖场景。
秒杀业务需要用户登录,因此在JMeter的HTTP信息头管理器中设置token,模拟一个用户同时发送多次下单请求。 
设置线程数为200,正常情况下应该是卖出100张,另外50%请求抢券失败,而结果异常率却是56.50%

数据库中下单数量为109,库存为-9,这就是高并发场景下的超卖问题。

为什么会产生超卖问题呢?

线程1过来查询库存,发现库存充足,正准备去扣减库存,但是还没有来得及去扣减,此时线程2过来,线程2也去查询库存,同样发现库存充足,那么这两个线程都会去扣减库存,最终相当于多个线程一起去扣减库存,导致库存变成了负数,这就是库存超卖问题出现的原因。
超卖问题的常见解决方案
- 悲观锁:认为线程安全问题一定会发生,因此操作数据库之前都需要先获取锁,确保线程串行执行。悲观锁中又可以再细分为公平锁、非公平锁、可重入锁等等。常见的悲观锁有:synchronized、lock。
- 乐观锁:认为线程安全问题不一定发生,因此不加锁,只会在更新数据库的时候去判断有没有其它线程对数据进行修改,如果没有修改则认为是安全的,直接更新数据库中的数据即可,如果修改了则说明不安全,直接抛异常或者等待重试。常见的实现方式有:版本号法、CAS操作。
悲观锁和乐观锁的比较
- 悲观锁和乐观锁的解决共享变量冲突方式不同:悲观锁在冲突发生时直接阻塞其他线程;乐观锁则是在提交阶段检查冲突并进行重试。
- 悲观锁比乐观锁的性能低:悲观锁需要先加锁再操作,限制了并发性能;而乐观锁不需要加锁,所以乐观锁通常具有更好的性能。
- 应用场景:两者都是互斥锁,悲观锁适合写入操作较多、冲突频繁的场景;乐观锁适合读取操作较多、冲突较少的场景。