高并发下如何设计秒杀系统

之前我做过一个卖布料的电商,里面经常会有秒杀活动,在这里总结一下秒杀活动的流程:
在这里插入图片描述
1. 瞬间高并发
一般秒杀活动都是在秒杀时间的前几分钟和秒杀后的后几分钟,这时候,用户请求会突然增加,这时候并发量就会很大,所以那,这并不是我们后端做一些操作就可以了,也是需要前端和后端共同努力的

2. 前端页面静态化

活动页面是用户流量的第一入口,所以是并发量最大的地方,但是那,活动页面绝大部分内容都是固定的,比如:商品名称、商品描述、图片等,这时候,这些数据就不要向后端要了,把这些东西写死就好了(页面静态化),图片实在不行的话就直接搞到代码里面,也不要请求oss了

3. 秒杀按钮设置

大部分用户怕错过时间,一般会提前进入活动页面,那么没到开始的时候,秒杀按钮一定要置灰,不让用户点击,只有到了秒杀时间点那一时刻,秒杀按钮才会自动点亮,变成可点击的

但此时很多用户已经迫不及待了,通过不停刷新页面,争取在第一时间看到秒杀按钮的点亮。

从前面得知,该活动页面是静态的。那么我们在静态页面中如何控制秒杀按钮,只在秒杀时间点时才点亮呢?

没错,使用js文件控制

为了性能考虑,一般会将css、js和图片等静态资源文件提前缓存到CDN上,让用户能够就近访问秒杀页面,不然的话,你服务器在北京,那么北京和深圳的人同时刷新页面,肯定是北京的人先看到按钮被放开

看到这里,有些聪明的小伙伴,可能会问:CDN上的js文件是如何更新的?就是每次请求的时候,随机产生携带一个随机数,如:https://www.xxxx.com/seckill.html?random=随机数,用来确保每次的请求都是最新的js代码

此外,前端还可以加一个定时器,控制比如:10秒之内,只允许发起一次请求。如果用户点击了一次秒杀按钮,则在10秒之内置灰,不允许再次点击,等到过了时间限制,又允许重新点击该按钮。

4. 读多写少/mysql并发问题/缓存问题/恶意请求

晚上8点,秒杀开始了,身为程序员的你看着自己写的代码,开心的不能行,突然db管理员告诉你,db挂掉了,你一脸蒙蔽,第二天被开了… 这时,你的秒杀流程是这样的
在这里插入图片描述

秒杀开始的时候,突然几万个请求进来了,甚至更多,请求一上来就查询数据库看是否有库存,mysql不瞬间被打死才怪…

后来你痛定思痛,那就不用msyql,我用redis,我提前预热,我把这次要秒杀的商品id、商品名称、商品库存、规格等信息都放到redis里面,同时数据库中也有相关的信息,毕竟缓存不完全可靠,此时你的秒杀流程是这样的
在这里插入图片描述
这时,你想,总算没事了吧,我都提前预热了(提前把秒杀的数据放到redis里面了,就不会走那个分叉去查询mysql了),但是,又没过几秒,db管理员又告诉你mysql挂掉了,这时,你才意识到,被恶意攻击了,缓存穿透,这样怎么处理那,反正这个接口只是售卖秒杀的商品,我也预热过了,如果我在redis查不到,直接结束,我不查mysql了,这样总行了吧

5. 库存问题

对于库存问题,如果用户在一段时间内,还没有完成支付,扣减的库存还是要加回去的,所以,会有一个"预扣库存"的概念,流程图如下:
在这里插入图片描述
上面的流程图是预扣库存的流程图,那如果库存超卖那?就比如现在库存只剩下1个了,我们高并发嘛,同一时间请求10个,一起查询了发现都是还有1个,这时最后的结果就是-9,这明显就是超卖了,

那这怎么处理那?

redis里面有个计数器incr,具有原子性,我们可以提前将库存设置成计数器就可以了,每卖一件减一即可

还有一种方法就是Lua脚本,Lua脚本是类似Redis事务,有一定的原子性,不会被其他命令插队,可以完成一些Redis事务性的操作。这点是关键

知道原理了,我们就写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情嘛

lua脚本有段非常经典的代码:

  StringBuilder lua = new StringBuilder();
  lua.append("if (redis.call('exists', KEYS[1]) == 1) then");
  lua.append("    local stock = tonumber(redis.call('get', KEYS[1]));");
  lua.append("    if (stock == -1) then");
  lua.append("        return 1;");
  lua.append("    end;");
  lua.append("    if (stock > 0) then");
  lua.append("        redis.call('incrby', KEYS[1], -1);");
  lua.append("        return stock;");
  lua.append("    end;");
  lua.append("    return 0;");
  lua.append("end;");
  lua.append("return -1;");

该代码的主要流程如下:

  1. 先判断商品id是否存在,如果不存在则直接返回。
  2. 获取该商品id的库存,判断库存如果是-1,则直接返回,表示不限制库存
  3. 如果库存大于0,则扣减库存
  4. 如果库存等于0,是直接返回,表示库存不足。

6. 分布式锁

如果我们的服务是单机的话,上面的就够用了,那如果我们的服务是分布式的那?我们的redis也是集群部署的那?

首先我们要知道,分布式锁的作用是什么?

redis分布式锁是严格意义上的原子操作,也就是同一时间内指允许一个用户操作成功,分布式锁的执行原理就是:

1. 先竞争锁
2. 查询库存
3. 扣减库存

在redis2.6之后,redis出了一个有关分布式锁的redisson客户端工具,直接使用就可以了

7. MQ削峰限流/MQ队列下单

有的秒杀并发量实在是太大了怎么办,可以使用redis,但是如果并发量实在太大了,redis也扛不住,那我们可以redis集群,redis主从,总能抗住的,但是万一redis真的抗不住那?

这个时候,就可以利用rabbitmq的ack机制prefetch_count(限制未处理消息的最大值)来平缓的取出数据来进行数据库的操作,

比如redis一秒可以处理10万个,那么我们就可以设置prefetch_count为10万,收到10万个秒杀请求后我就先不接收了,先处理,处理一个我ack一个,然后秒杀进入一个,总是维持在10万个左右,这样就实现了削峰限流了,反正秒杀,这样用户是无感的,没有感觉,毕竟先到先得

实际上一个完整的秒杀活动是分为三个部分的:
在这里插入图片描述
秒杀主要就是万千个请求过来,后端做好库存不超卖,削峰限流,保证服务正常运行不被打垮,然后通过MQ队列通知下单服务

当秒杀过后,到达下单这部分的用户都是网速相当好的幸运儿,这时,就可以不及不慢的对mysql进行操作,然后通过MQ通知支付服务

当收到MQ消息后,用户支付,秒杀结束,如果在一定时间内没有支付,则库存回滚让给其他用户

8. 恶意攻击

有的用户可能会利用第三方插件去频繁的请求接口,这样会给接口很大的压力,为了避免这种刻意的恶意刷单,我们可以设置一个用户一分钟不能访问超过60次,具体怎么实现,可以使用reids,使用hash类型,key为用户id value为次数,设置key的过期时间为60秒

还可以使用图片验证码、移动滑块等,这样即一定限度上减少了恶意攻击,也减少了并发量

还有一些人注册了很多账号,平时不用,专门用来参见秒杀活动,这样我们就可以通过业务层面来做处理,比如设置等级,只有经常买东西的用户才能提升等级,只有一定等级的用户才能参加秒杀等

9. 其他

比如有些秒杀活动,一个用户只能抢一个,就可以利用redis的set来进行处理

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值