业务:平台推出商品秒杀活动,活动中可以添加多个商品,即同时有多个商品参与了秒杀,但每个人对同一个商品只能成功抢购一个,预计有1w人参加,即最高并发数10000。
思路:最常见的方法是使用reids来实现,创建两个队列,商品库存队列和抢购成功用户队列,用户抢购时,先判断用户是否在抢购成功用户队列中,是直接返回已抢购成功,否则从商品库存队列取出一个商品库存,取得成功将用户加入抢购成功用户队列,取出失败则直接返回已抢购完。在活动结束或数据库闲时处理抢购成功用户队列,生成待支付订单,提醒用户支付。
实现步骤:
- 秒杀活动生成后创建商品库存队列,队列使用的是list结构。
$num = 100; //商品库存 $goodsid = 1; //商品id //生成对应商品库存队列 $goods = "goods:".$goodsid; for ($i=0; $i < $num; $i++) { Redis::lpush($goods, 1); }
-
开始秒杀:
$goodsid = $request->input("goodsid"); //商品id $userId = $request->input("userId"); //抢购用户id $goods = "goods:".$goodsid; //对应商品库存队列 $robSuccessUser = "successUser".$goodsid; //对应商品抢购成功用户队列 $result = Redis::sismember($robSuccessUser,$userId); if ($result) { return "已经抢购过了"; } $count = Redis::lpop($goods); if (!$count) { return '已经抢光了哦'; } $success = Redis::sadd($robSuccessUser, $userId); if(!$success){ //已经在成功队列里了,加回库存,防止的是同个用户并发请求 Redis::lpush($goods, 1); return "已经抢购过了"; } //以下根据需要可以使用redis延迟队列生成订单,或者在数据库闲时再生成订单 .......
抢购成功用户队列使用的是set结构,添加进userId时如果set里已经存在则返回0,成功返回1,使用这个特性可以进行最后判断用户是否已经在成功队列中了,防止同个用户并发多个请求且通过了$result的判断(该步骤也可以使用多一个队列来做限制,用户请求队列,用户请求来到时先加入请求队列,只允许接受一次请求,之后的请求直接拒绝)。
总的来说是使用了redis的原子性,即使有多个用户同时到达,也是依次取出一个库存,这样就能保证商品不会被超卖。
关于减库存问题,推荐生成待支付订单立减库存,然后再将订单加入检查过期的延迟队列,规定时间后还没支付将订单过期然后加回库存。
优化
- 将秒杀系统单独部署,跟现有项目隔离开,避免造成整个项目崩溃。
- 将秒杀商品详情页面重新设计,将页面内容静态化,用户请求页面不经过应用服务。
- 为了避免用户直接访问下单页面URL,需要将改URL动态化,即使秒杀系统的开发者也无法在秒杀开始前访问下单页面的URL。办法是在下单页面URL加入由服务器端生成的随机数作为参数,在秒杀开始的时候才能得到。
- 将秒杀按钮置灰,到时间变亮,限制用户x秒点击一次。
- 过载保护,如果检测到系统满负载,直接拒绝请求。
- 检测IP请求频率,过高的时候弹出验证码或拒绝请求。
- 使用负载均衡。