设计一个秒杀系统可以很难也可以很容易。下面介绍两种思路
一、随机抽取(简单粗暴)
- 第一步:用户点击秒杀后,直接在前端随机丢弃一些请求,返回给抢单失败,另外请求一些进入后端系统。
- 第二步:提前部署redis集群,存储参与用户的ID,这样redis只有存的压力。
- 第三步:然后同步记录去重, 再随机抽100个参与用户。(从秒杀开始后一定时间段内提交请求的用户中抽取)
其实也是公平的 而且应该比“凭手速”更公平 因为如果完全按到达服务器的时间算 有太多用户不可控但短期内不改变的因素会影响成功率 比如网速 设备性能
二、经典设计
总体思路:尽量将请求拦截在系统上游;读多写少的场景多使用缓存。
秒杀系统的普遍问题:
- 高并发
- 超卖
- 恶意请求
- 链接暴露
- 数据库
一般解决思路:
前端层:
前段限流:按钮置灰,活动开始前,前端定时检测当前时间,到点了就取消置灰;js限制用户几秒之内只能点击一次
网络层:
cdn预热:一些秒杀相关的静态文件可以先推到cdn
nginx:负载均很,临时加很多台秒杀服务器;拦截恶意请求(请求次数太夸张,对单个ip每秒访问次数加限制、加黑名单)
网络宽带:临时提高宽带(云服务器)
后端层:
设计系统,总体把握一个单一原则,秒杀系统不影响其他系统的正常运行。
缓存层:
redis部署:集群、主从同步、读写分离、哨兵,开启持久化(单一职责)
redis预热:先把库存商品加载到redis,结束后再异步修改数据库
业务层:
高并发:服务设计单一职责,单独设计一个秒杀服务,有助于扩容。(单一职责)
链接暴露:秒杀链接加盐,MD5之类的加密算法加密随机的字符串去做url,然后通过前端代码获取url后台校验才能通过。
服务降级:用户抢到即为成功,订单具体处理异步去操作。
削峰填谷:MQ(设置定长队列能强制避免超卖)。
持久层:
数据库:为了防止秒杀打挂数据库影响其他业务,单独设计一个秒杀表。(单一职责)
一些补充:
在争抢库存是,会利用到redis-Lua:
Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
使用Lua脚本的好处:
- 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
- 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。这个很关键
- 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
写一个脚本把判断库存扣减库存的操作都写在一个脚本丢给Redis去做,那到0了后面的都Return False了是吧,一个失败了你修改一个开关,直接挡住所有的请求,然后再做后面的事情。