在设计秒杀业务的时候我们通常会使用漏斗模型来层层过滤流量,减少最终达到下游的流量,从而保护下游服务。
由于秒杀活动的流量会远超平常,一般在流量入口,系统就要把那些非法的、无资格的、优先级低的流量过滤掉,减轻系统的并发压力。为了实现这个过滤功能,就需要我们设计流量拦截器。
通常,流量拦截器有多层,就像一个漏斗或者倒金字塔。在容量上,你可以联想到缓存金字塔,流量拦截器跟它正好相反。比如,位于上层的流量拦截器可能会负责过滤掉 40% 的流量,位于中间层的可能过滤掉 30% 的流量,而位于底层的则可能过滤掉 20% 的流量。
最上层流量入口是网关和 WAF(Web Application Firewall,Web 应用防火墙),它们会拦截大部分非法请求,比如一些恶意攻击的请求,一些用秒杀器疯狂刷接口的请求。
在设计上,这一层通常采用封禁攻击者来源 IP、拒绝带有非法参数的请求、按来源 IP 限流、按用户 ID 限流等方法,在顶层入口处就拦截掉这些请求。这样获得的收益也是最大的,能为下游业务系统节省大量资源。
经过上层拦截器处理后,还是会有一些漏网之鱼,比如“黄牛”。于是就有了中间层拦截器,它主要是为了识别出不具备抢购资格的用户,并拦截他们的流量。
反黄牛的前提是需要先识别出谁是黄牛,这就需要一份黄牛名单了。那么,这份黄牛名单是如何产生的呢?通常它会由数据分析系统根据大量订单信息和用户信息生成,然后提供给秒杀接口服务使用。
像多个账号每次都在一个 IP 下参与秒杀,每次抢到的商品都不是给自己账号用,或者通过自制秒杀工具抢到商品后快速支付,等等。虽然对于后端服务来说有些行为看着像正常用户,但是,在大数据分析下,还是能抓到一些蛛丝马迹。
一般数据分析系统会定期生成黄牛名单,比如每天凌晨 3 点钟。然后秒杀接口服务会将黄牛名单更新到内存中。在秒杀活动进行时,秒杀接口服务会从请求中拿到账号信息后进行匹配。如果匹配到了,说明该账号是黄牛账号,需要拦截掉。
除了黄牛外,还有两部分不具备资格的流量需要拦截掉:
由未登录或者登录态过期的用户产生的流量,当他们点击秒杀购买时,我们可以让这些用户跳转到登录页进行登录;
如果用户购买数量已经达到该场次商品数量限制,此时需要提醒用户已经参与过该场次,请勿重复参与。
那位于下层的拦截器负责做什么呢?我们知道,秒杀活动中库存数量远低于参与秒杀的用户数,于是如何快速判断哪些用户抢不到库存,就是个非常关键的问题。而这,正是下层拦截器的核心工作。虽然前面两层拦截器已经拦截了大量请求,但下层拦截器面临的流量还是很大,单节点 QPS 至少上万。因此,下游拦截器判断库存的时候,对性能要求非常高。
那位于下层的拦截器负责做什么呢?我们知道,秒杀活动中库存数量远低于参与秒杀的用户数,于是如何快速判断哪些用户抢不到库存,就是个非常关键的问题。而这,正是下层拦截器的核心工作。虽然前面两层拦截器已经拦截了大量请求,但下层拦截器面临的流量还是很大,单节点 QPS 至少上万。因此,下游拦截器判断库存的时候,对性能要求非常高。
需要怎么做呢?通常是由秒杀服务将库存数据在本地内存中缓存一份,用于初步判断库存资格。在 Go 语言中,我们可以用 map 来缓存库存数据,利用锁来控制并发扣减库存。由于完全是在本地内存中操作,性能要比访问 Redis 好很多。
要注意的是,本地内存缓存中的库存数据是比较粗略的,时间长了也容易出现误差,不能作为最终的扣减依据。所以通常需要有个定时任务,从 Redis 中定时拉取最新的库存数据,并更新到本地内存缓存中。
这个更新速度不能太快,也不能太慢。太快的话,可能导致内存中已扣减的库存还原成 Redis 中未扣减的库存;如果太慢,因超时关单归还的库存会无法及时同步到内存缓存中。我们可以根据流量大小设定一个合理的值,比如 100 毫秒同步一次。
另外,内存缓存中的库存大小也需要注意按比例缩小。如果总共有 1000 个库存、50 个秒杀节点,平均分摊的话每个节点分到 20 个库存。实际上,每个节点需要略微高于平均值,以确保足够多的请求漏下去,将 Redis 中的库存扣减完,达到最大的活动效果。
参考资料
《打造千万级流量秒杀系统 》