【电商系统】— 防超卖&高并发扣减方案

549 阅读4分钟

image.png

传统的通过数据库防止不超卖

普通数据库悲观锁与乐观锁实现

悲观锁:select for update
begin; select * from goods where id = 1 for update; update goods set stock = stock - 1 where id = 1; commit;

乐观锁:
每次获取商品时,不对该商品加锁。在更新数据的时候需要比较程序中的库存量与数据库中的库存量是否相等,如果相等则进行更新,反之程序重新获取库存量,再次进行比较,直到两个库存量的数值相等才进行数据更新。乐观锁适合读取频繁的场景。

`select * from goods where id = 1

begin;

#更新 stock 值,这里需要注意 where 条件 “stock = cur_stock”,只有程序中获取到的库存量与数据库中的库存量相等才执行更新

update goods set stock = stock - 1 where id = 1 and stock = cur_stock;

commit;`

如果是下单频率很高,则使用悲观锁;如果是查询频率比下单频率高,则使用乐观锁。

通过数据库保证

使用数据库的事务来保证,通过sql判断剩余的库存是否够用。为了保证扣减不重复,使用一个防重表来防治重复的提交,做到幂等性。

事务开始  
Insert into antiRe(code) value (‘订单号+Sku’)  
Update stockNum set num=num-下单数量 where skuId=商品ID and num-下单数量>0  
事务结束

问题:当流量很大,例如秒杀场景时,数据库的瓶颈就暴露出来了。分库分表没用的,因为都是针对少量商品的,集中在一个表里。

Redis缓存做库存扣减的方案

其实核心就两点:

  • 超卖校验
  • 扣减库存并持久化

在传统数据库中,两步是一起处理的。redis可以将其分为两步,这样很大一部分流量在第一步就会被卡住,减少数据库的流量。

image.png

第一关超卖校验:
可以把数据放入Redis中,每次扣减库存,都对Redis中的数据进行incryby 扣减,如果返回的数量大于0,说明库存够,因为Redis是单线程,可以信任返回结果。第一关是Redis,可以抗高并发,性能Ok。超卖校验通过后,进入第二关。
第二关落库:
经过第一关后,第二关不需要再判断数量是否足够,只需要傻瓜扣减库存就行,对数据库执行如下语句,当然还是需要处理防重幂等的,不需要判断数量是否大于0了,扣减SQL只要如下写就可以。

其实为了防止redis挂了,数据库再可以做一层校验,做好流控即可。

事务开始  
Insert into antiRe(code) value (‘订单号+Sku’)Update stockNum set num=num-下单数量 where skuId=商品ID  
事务结束

那么此时热点怎么解决呢?
任务库使用订单号进行分库分表,可以消除数据库单点问题。

redis热点防刷?
单机流控
Redis扣减原理
Redis的incrby 命令可以用做库存扣减,扣减项可能多个,使用Hash结构的hincrby命令,先用Reids原生命令模拟整个过程,为了简化模型下面将演示一个数据项的操作,多个数据项原理完全等同。

扣减的幂等性
如果应用调用Redis扣减后,不知道是否成功,可以针对批量扣减命令增加一个防重码,对防重码执行setnx命令,当发生异常的时候,可以根据防重码是否存在来决定是否扣减成功,针对批量命名可以使用pipeline提高成功率。

// 初始化库存  
127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存    
127.0.0.1:6379> hget iphone inStock   #查看苹果手机可售库存为1"1"// 应用线程一扣减库存,订单号a100,  
jedis开启pipeline  
127.0.0.1:6379> set a100_iphone "1" NX EX 10 #通过订单号和商品防重码   
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功(integer) 0  
//结束pipeline,执行结果OK和0会一起返回

防止并发扣减后校验
为了防止并发扣减,需要对Redis的hincrby命令返回值是否为负数,来判断是否发生高并发超卖,如果扣减后的结果为负数,需要反向执行hincrby,把数据进行加回。

如果调用中发生网络抖动,调用Redis超时,应用不知道操作结果,可以通过get命令来查看防重码是否存在来判断是否扣减成功。

127.0.0.1:6379> get a100_iphone   #扣减成功"1"  
127.0.0.1:6379> get a100_iphone   #扣减失败(nil)

上边的情况有可能会造成:防重码设置成功,扣减库存失败。此时会进行重试,发现防重码不为空,则认为扣减成功。。。此时就会造成超买。

所以,应该先扣减库存,然后设置防重码。