在高并发的电商等特殊的场景下有一个不容忽视的问题——超卖问题
这个问题是怎么产生的呢?
我们举一个简单的例子。
在前端点击比如说抢购的按钮后,开始发起请求。
我们往往会先判断比如说我们商品的库存、看看我们的库存是否够呢?
比如说现在的库存是1。我们有两个线程
分别是线程1和线程2。
在理想的状态下,当线程1发起请求时,我们开始
查询库存。当发现库存为1时,好的,我们可以直接削减库存。然后线程2开始请求,它发现库存已经变为零了。那么就给前端返回一个比如说:商品库存不足。
但是现实情况是,这两个线程可能在大概率下并不是线性执行,而是并行执行,也就是说,这个查询库存,并且削减库存的数据库操作可能是交叉执行的。
当线程1开始查询库存时,库存还剩下1个。那么它开始准备削减库存,但是在它还没有削减完成时,线程2进来了,它也发现还剩下最后一个,好的它也开始准备削减库存。
那么在这种情况下,就产生了超卖问题。
也就是说最后的结果就是:只有1件货,那么你卖了2件。
那么这种超卖问题的解决方案有哪些呢?
超卖问题作为多线程高并发场景下的常见问题,我们往往选择使用加锁的方案来解决线程穿插的问题
既然我们决定加锁来解决这种超卖问题
锁主要分为悲观锁和乐观锁两种
在这种场景下,我们该选择哪种锁呢?
顺便提一下:乐观锁和悲观锁并不是真正有这种锁、他们只是一种锁的设计理念
悲观锁:认为线程安全一定会发生、因此在操作数据之前先获取锁,确保各个线程在串行而非并行的情况下对数据进行操作、例如Synchronized。这也就导致了性能相对来说其实并不是很好,特别是在高并发的情况下,每个线程只能一个一个执行。
乐观锁:认为线程安全其实并不一定会发生,所以更多像一个乐子人,因此并不加锁,只是在修改数据时判断有没有其他线程对数据进行了修改。
如果没有被修改则认为是安全的,自己才会更新数据。
如果已经被其他线程修改了数据,那么说明发生了线程安全问题,会提示进行重试的操作。
乐观锁的关键:怎么判断有没有其他线程修改了数据?
常见的有两种方法
版本号法:给数据增加一个版本号的字段:每当数据发生了修改,那么版本号就会加1
当我想要判断这个数据有没有被修改过时,那么只需要判断版本号有没有变化即可。
不过其实我们可以思考一下、我们是不是可以使用库存来代替版本号的这个字段呢、在最后需要进行库存削减时,我们判断一下,我们查到的库存和数据库中的库存是否一致呢?
这个就是CAS法
但是其实乐观锁也是有一定的弊端的,那就是在修改数据时过于小心,这一点可能会导致大量的线程执行失败,所以我们需要进行一些改进。
还是那个业务场景、现在有一百个线程,他们在同一时间进行抢购,我们不再选择在修改数据时,对查到的库存和数据库中的库存进行对比,而是选择只要库存大于零即可,那么就可以修改数据。
// 选择与查到的库存进行对比
set stock = stock – 1 where id = ? and stock = ?
// 选择只要库存大于零即可
set stock = stock – 1 where id = ? and stock > 0
虽然我们通过 set stock = stock – 1 where id = ? and stock > 0 解决了超卖问题,但这依然是基于数据库层面的解决方案。
在真正的“秒杀”场景下,并发量可能高达几十万QPS,直接让数据库扛这几十次更新请求依然会造成巨大的压力,甚至拖垮数据库。
因此,实战中我们通常会结合 Redis 来做预扣减库存,利用内存的极高性能抵挡绝大部分流量,只有抢到Redis锁的请求才有资格去更新数据库。
另外,如果系统是集群部署,Synchronized 这种单机锁就失效了,这时候就需要请出 分布式锁 了。

