什么是超卖
超卖是指卖出的商品数量超过了实际库存数量,这种情况通常发生在高并发的电商网站上,比如双十一、618等大促销活动。超卖的原因主要是多线程并发操作数据库时,没有对库存数量进行加锁,导致多个线程同时读取库存数量,然后都判断库存数量足够,最后都执行了减库存操作,导致库存数量变为负数。
如何解决超卖问题
加锁是解决超卖问题的常用方法
锁的种类
悲观锁:悲观锁认为线程安全问题一定会发生,所以在操作数据之前先加锁,操作完成后再释放锁。串行化处理,效率低。
乐观锁:乐观锁认为线程安全问题不一定会发生,所以在操作数据之前不加锁,只是在更新数据时判断数据是否被其他线程修改过,如果没有修改过则更新成功,否则更新失败。
对于超卖问题,乐观锁更适合,因为超卖问题不是一定会发生的,只有在多个线程同时读取库存数量时才会发生,所以只需要在更新库存数量时判断库存数量是否足够即可。
乐观锁的实现方式
关键在于判断数据是否被其他线程修改过,一般有两种方式:
- 版本号:给数据表增加一个版本号字段,每次更新数据时将版本号加1,更新数据时判断版本号是否一致,如果一致则更新成功,否则更新失败。
- CAS(Compare And Swap):使用CAS指令更新数据,CAS指令是一种原子操作,可以保证数据的一致性。每次更新数据时,先读取数据的值,然后比较数据的值是否和预期值一致,如果一致则更新数据,否则更新失败。
注意,如果单纯使用CAS指令更新数据,可能会导致ABA问题,即数据的值被修改两次,但是版本号没有变化,所以需要使用版本号来解决ABA问题。
ABA问题
如果库存值从A变成B再变成A,那么CAS操作会认为库存值没有变化,但实际上库存值已经发生了变化,这就是ABA问题。
代码实现
这里单纯使用CAS指令更新数据,不考虑ABA问题。
1 |
|
注意,如果条件判断是要库存和之前的库存相等,这样会导致成功率低,未卖出问题,即当100个请求进入时,其中一个请求先执行完修改了库存,剩余99个因为这个条件判断失败都不会执行,这样就会导致未卖出问题。
所以这里使用了gt("stock", 0)
,即库存大于0时才执行扣减库存操作。保证了实现卖完即止。
注意,为什么不会存在库存为1,然后都修改的情况,因为这里的update是Mysql提供锁来进行隔离,不会存在都为1然后都修改的情况。
可优化方向
- 数据库锁:使用数据库锁来保证数据的一致性,比如行锁、表锁、读锁、写锁等。
- 缓存:将库存数量缓存到Redis中,减少数据库的访问次数。
- 消息队列:使用消息队列来异步处理订单,减少数据库的压力。
…