一个秒杀系统的设计,涵盖若干要素。比如:
- 读写请求分离;
- 流量筛选;
- 读缓存;
- 写批量;
- 预处理;
- 前端界面操作优化;
- dns优化;
- 自我保护;
每一个要素,都是解决一个具体的问题场景。
读写请求分离:
秒杀一般分为2个阶段,秒杀准备阶段,秒杀阶段。
在秒杀准备阶段,一般流量会逐渐上升,用户会不断的查询秒杀商品等待开始。
在这个阶段,按资源类型的区别,分为静态资源&动态接口。静态资源提前走cdn部署,可以忽略这部分请求的压力。动态接口需要做好数据的读缓存,有效期可以相对放长一些,可以防止查询流量进入db。
在秒杀阶段,一般写入流量会多于查询流量。
在这个阶段的写请求,一般会经历如下步骤:
- 秒杀写请求经过限流器,筛除部分重复请求;
- 对请求进行预处理,检查前置条件,秒杀目标现状等。此时秒杀目标现状数据可以不要求实时精确,拦截掉部分流量即可;
- 合格的用户请求下,秒杀目标有容量的情况下,筛出来合格的请求进入排队队列,同时,将请求线程挂起等待消费者唤醒,队列消费者进行批量并发的消费,成功则继续循环,不成功则全量返回失败;
读请求,则直接从缓存读即可,少部分mis的再请求db。
流量筛选:
秒杀品一般性价比比较高,容易招来黑产刷单。为了尽量避免被褥羊毛,一般会在网关侧实时清洗流量,比如按IP,uid等维度识别时间跨度内的访问频率,命中规则的放入黑名单直接拒绝流量进入秒杀系统。如果刷单严重,会在秒杀系统中留存账号请求记录,导入实时计算引擎结合风控模型分析后,进一步按用户行为分析筛除羊毛党,一般到这一步基本的羊毛党都能识别出来。
读缓存:
读请求走缓存,一般的问题在于缓存的正确性。比如,秒杀开始前的读请求,查询商品详情的数据需要缓存时间较长,到达秒杀时刻时,要及时更新状态等。这种情况下一般是在缓存数据中存储上了生效时间,代码中只是在获取到后进行一次判断。
在写请求成功后,一般要更新缓存的数据。在并发密集的情况下,一般走异步队列批量进行。缩减主流程涉及环节。
写批量:
在合适的写请求进入队列后,为了提升消费者的处理效率,可以看条件批量提取数据,提升消费者的处理能力。比如抢券场景下,产品可能设计成用户可以多次领取多张,多种券,大部分用户的领券,动作、时机、内容高度相似,完全具备整合批量执行的能力。这种情况下,消费端批量消费写请求,可以降低对DB的压力,减少请求的等待时间,提升QPS。
预处理:
在执行消费前,可以再进行一次预检查,尽量提前发现不满足条件的写请求,避免将压力传导到下层环节。
前端界面优化:
这部分优化得看产品设计。有些秒杀品仅允许购买一次,但是有些允许购买多次。限制条件往往丰富且多维。比如:最近购买次数少于2次;最多允许购买2份等。这种情况下的秒杀,前端界面只能跟随后端的逻辑来判断是否允许按钮点击与否。故为了支持前端优化,需要后端支持准实时的分析,并将分析结果&秒杀品条件进行关联,然后缓存住分析结果。方便业务方查询使用。这种情况下,要看秒杀规模与效益,需要申请计算资源支持实时计算。
dns优化:
dns是为了寻找域名对应真实ip用的,一般业务域名为了防攻击会经过一层高防,然后再由高防转slb进行流量分发。高防的成本相对较高,可以尝试结合客户端本地dns,走httpdns的模式,后端聚合若干个小容量的slb进行流量的客户端负载均衡,可以在一定程度上避免高防成本过快增长。
自我保护:
系统必须能支持幂等逻辑。不能因为高频并发产生超卖,超发的情况。比如多个同样的请求一起发起,不能成功多次,只能允许成功一个。这种情况下,可以借助redis的分布式锁来控制,筛除用户针对指定秒杀对象的重复请求。