关于高并发下超卖问题的解决方案

超卖问题:

数据库方案

一个简单的订单表

create table orders (
      sku_id int PRIMARY KEY,
      count int
)

一个/buy接口

begin()
count = db.Query(`select count  from orders where sku_id = '123'`)
if count < 0 {
    fmt.Println("卖光了")
    return 
}
db.Exec(`update orders set count = count -1 where sku_id = '123'`)
commit()

由于sql 支持并行加上事务的隔离性,所以当多个事务并行时,select出来的值并不一定准确的,进而update之后的值也就不正确了。

解决方法:

begin()
update orders set count = count -1 where count > 0 and sku_id = '123'
commit()

这里是利用了update造成的行级锁,避免了超卖的问题。

缓存方案

由于数据库的性能问题,无法应对高并发的秒杀场景,所以通常的解决方案是利用缓存来完成,先在缓存中完成计数,然后再通过消息队列异步地入库。
redis由于其高速+单进程模型,省掉了很多并发的问题,所以可以被选来进行高速秒杀的工作。

方案一, redis list

我们建立sku:id为键的list结构, 在得到sku的库存余量N之后,在sku:id中push N个值为1的元素。当秒杀时,执行lpop即可。当lpop返回nil时, 即代表库存卖完了。
优点: 实现简单
缺点: 当库存非常大时, 会占用非常多的内存。
不过好在用于秒杀的商品,大多不会过万。

方案二, redis incr

仍然是sku:id为键,但是直接设置为库存余量N。当秒杀时,执行DECR 即可,当DECR返回值小于0时,即代表库存卖完了。
优点: 节约内存
缺点:decr incr的操作范围都是int64,当decr min_int64时,redis会报告overflow。不过好在考虑到业务实际,几乎不会出现该情况,毕竟库存终究会刷新的,秒杀也不可能一直持续下去。

方案三, redis 分布式锁

锁似乎一直都是用来解决并发问题的良药。redis由于其单进程的模型,很多设计都可以作为分布式锁来用,在sku:id中保存库存余量, 以sku_lock:id作为其锁,当成功获得锁后, 对sku:id进行获取,减1。
比较常见的是setnx来做分布式锁。
setnx: 当只在键 key 不存在的情况下, 将键 key 的值设置为 value ,返回1。
若键 key 已经存在, 则 SETNX 命令不做任何动作,返回0。
因此当setnx返回1时即意味着加锁成功,返回0即加锁失败。解锁操作比较简单,del掉这个key就可以了。
优点: 锁似乎永远都可以作为解决并发问题的银弹,因此当你发现并发造成数据不一致的时候,从解决问题的思路出发,第一反应应该是加锁,第二反应才应该是无锁化的优化操作。
缺点:明显比前两种方案增加了一些代价。另外,由于该锁并不是阻塞型的,没有排队机制,并不会遵循先到先到的逻辑。

你可能感兴趣的:(关于高并发下超卖问题的解决方案)