超卖问题解决思路

项目设计

无并发情况

首先我们考虑最简单的没有并发的情况。下单时,需要考虑如下步骤:查询数据库、判断库存是否满足(大于等于)订单商品需求。如果满足就进行更新操作、下单成功。如果不满足就返回失败。

下面,我们考虑有并发时如何解决超卖问题。

并发导致超卖

超卖问题涉及到多线程高并发的场景,比如某件商品库存为10个,这个时候有两笔订单要下单。一个是下单8件,一个是下单7件。当他们同时读数据库库存进行判断时,都会认为可以下单成功。然而这就会产生卖出15件的问题。

解决的六种方案:

  1. 应用层加锁
  2. MySQL层加锁
  3. Redis事务
  4. Redis队列
  5. Redis分布式锁
  6. Redis原子操作+MySQL互斥锁

Redis原子操作+MySQL互斥锁是高并发场景下较为优秀的解决方案。

应用层加互斥锁(不适用分布式环境)

可以在应用层该下单方法上加互斥锁,比如通过synchronized关键字加互斥锁,使得该方法的调用变为串行调用执行,多个请求到达时能够串行执行,整createOrde中的查询库存和减库存成为一个“原子操作”,防止超卖。但是在高并发场景下,这使得大量请求到达时,不是并发执行,而是串行执行,排队等待互斥锁的请求很多,后面的请求响应时间很长,性能很差

注意在分布式应用环境下,比如应用部署在两个机器上,那么此方式是无效得,因为多机之间没有约束。

MySQL数据库层加互斥锁(MySQL有压力)

将查询和更新的sql语句合并为一条sql

例如:将

select amount from product where productID = 12345
// 判断amount - quantity是否大于等于0,如果是,则继续
update product set amount = amount - quantity where productID = 12345

注意:如果将这两条sql语句写进一个事务里会导致超卖。例如多个查询sql请求到达数据库,此时如果是InnoDB存储引擎,多版本控制机制下多个事务的查询sql(快照读),在此时均可以读取到一样的库存数量,然后多个事务的更新操作会被MySQL通过互斥锁变成串行执行操作,且均可以执行减库存的更新操作,从而使得超卖问题出现。

合并为:

update product set amount = amount - quantity 
where productID = 12345 and amount >= quantity

如上,一条语句执行查询和减库存操作,使得多个请求到达数据库时,数据库直接将多个更新语句通过互斥锁串行化执行,保证不会出现超卖。但是将互斥操作下沉到数据库可行吗?应该是不可行的,大量的写请求到达数据库,然后串行执行,这样操作的性能差,对数据库的压力也大。在并发数量比较少的情况下,还可以接受,但是如果是高并发的场景,上述方法不可取。

通过Redis事务解决(Redis有压力)

将库存数量放在redis中,查询库存和减库存均走redis而不操作mysql,从而减轻数据库的压力,但是这个方式和应用层互斥、mysql互斥没有本质区别,仅仅是将互斥操作放在了redis中而已。

通过Redis队列解决(只支持限卖一件)

将秒杀的商品id作为键,库存作为redis中的list,提前加入redis缓存中,多少件商品就入队列多少个1,高并发请求到达时依次在队列中排序获取库存,能够获得库存则继续执行下单逻辑,否则库存不足抢不到。但是这种方式下,每个请求只能购买一件商品。

int result = 0;
if (result = jedis.lpop("12345") > 0) {
// 有库存
} else {
// 无库存
}

通过Redis分布式锁解决(麻烦)

简单的分布式锁解决超卖问题流程如上。但是这样的问题在于所有用户都对同一个分布式锁进行等待和加锁、释放,同样无法适应高并发的场景。

优化Redis分布式锁:分段加锁。

假如你现在iphone有1000个库存,那么你完全可以给拆成20个库存段。可以在数据库的表里建20个库存字段,比如stock_01,stock_02,…,也可以在redis放20个库存key。总之,就是把你的1000件库存给他拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存。

接着,每个请求可以根据自己写的一个简单的随机算法,随机对应在20个分段库存里,选择一个进行加锁。当然也要考虑到分段库存为零时,对这个请求重新分配一个分段。

但这样优化的分布式锁也不是最优的解决高并发超卖问题的方法。

通过Redis原子操作和MySQL锁实现(推荐)

我们可以将商品的库存数量缓存到Redis中,在Redis里通过原子操作来判断库存是否满足购买数量要求。如果满足就更新数据库(Redis缓存更新机制会在之后将数据库同步到Redis)。

具体流程是:首先去Redis中查询有没有对应商品的key的键值对,如果查到了就直接返回目前redis中商品的库存。如果redis中没有,就去mysql中查商品库存,并通过setnx命令写入redis(setnx是为了防止其他线程先完成了设置)。然后判断该商品库存是否满足,不满足直接返回。通过decrby key numbers命令减Redis库存,然后得到返回值,对返回值进行判断,如果返回值<0则说明库存不足,此时将减去的库存加上 incrby key numbers,然后返回库存不足;如果返回值>=0则说明库存足够,则执行MySQL减库存操作(update 语句 where amount > quantity),然后执行下单的后续操作。

这样就将大量查询操作放在了Redis上,MySQL仍然能支持更改数据的写操作。