秒杀核心设计(减库存部分)-防超卖与高并发
假设有两个表,一个存放buy_record用户秒杀记录,一个是产品表goods
通过在buy_record添加uid,goods_id并加唯一索引
开启事务
插入 insert into buy_record
if(唯一索引报错?){
抛异常,用户已经秒过了,回滚
}else{
update goods set goods_num=goods_num-1 where goods_id=$goods_id and goods_num>0 ;//更新库存
if(受影响行数<=0){
抛异常,商品秒完了,回滚
}
}
其他方案:
主要思路:
1、前端三板斧【扩容】【限流】【静态化】
2、后端两条路【内存】+【排队】
解决方案1:
将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
优点:解决性能问题
缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。
解决方案2:
引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
优点:解决超卖问题,略微提升性能。
缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。
解决方案3:
将写操作前移到Memcached中,同时利用Memcached的轻量级的锁机制CAS来实现减库存操作。
优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。
缺点:没有实测,基于CAS的特性不知道高并发下是否会出现大量更新失败?不过加锁之后肯定对并发性能会有影响。
解决方案4:
将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。
优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
数据库层方案:
商品详情页面的静态化,varnish加速,秒杀商品库独立部署服务器这种就略过不讲了。只讨论库存部分的优化
mysql配置层面的优化可以参考我的这篇文章 《关于mysql innodb引擎性能优化的一点心得》
重点设计在数据库层面。
2张表:
第一张:判重表(buy_record),该用户有没秒杀过该商品
字段: id, uid, goods_id, addtime
第二张表:商品表 goods
字段: goods_id goods_num
方案1:
start transaction;
select id from buy_record where uid=$uid and goods_id=$goods_id;
if(结果不为空)
抛异常,回滚。
insert into buy_record。。。
if(受影响行数<=0)
抛异常,回滚。。。
select goods_num from goods where goods_id=$good_id;
if(库存<=0)
抛异常,回滚。。。
update goods set goods_num=goods_num-1 where goods_id=$goods_id;
if(受影响行数<=0)
该方法在高并发下几乎必然导致超卖。当库存为1的时候刚好多个用户同时 select goods_num from goods where goods_id=$good_id;此时库存刚好大于0,做update操作的时候必然减到小于0. 同时上面进行是否秒杀过的判重同样会出现类似问题
方案二:
start transaction;
select id from buy_record where uid=$uid and goods_id=$goods_id for update ;
if(结果不为空)
抛异常,回滚。
insert into buy_record。。。
if(受影响行数<=0)
抛异常,回滚。。。
select goods_num from goods where goods_id=$good_id for update ;
if(库存<=0)
抛异常,回滚。。。
update goods set goods_num=goods_num-1 where goods_id=$goods_id ;
if(受影响行数<=0)
抛异常,回滚。。。
该方法有效的防止了超卖,但是在每次select的时候加上了排它锁,每次select操作都会被堵塞 ,并发性能大大降低。
方案三: 对(uid,goods_id)加唯一索引!!
start transaction;
insert into buy_record。。。
if(唯一索引报错?)
抛异常,已经秒过了,回滚。。。
update goods set goods_num=goods_num-1 where goods_id=$goods_id and goods_num>0 ;
if(受影响行数<=0)
抛异常,商品秒完了,回滚。。。
该方法完美的解决了超卖与select排它锁导致的并发低的问题,并且4个sql缩减成2个sql语句。极大提升性能
原文:http://www.tuicool.com/m/articles/Bfa63e6
参考:http://m.blog.csdn.net/article/details?id=38067003