问题描述
如何设计并实现一个秒杀/抢购系统
过去都说台上十分钟,台下十年功,而秒杀系统更有意思,瞬时的流量峰值可能就三两分钟,但你却必须为此做大量的准备工作。容量评估是否做好了,带宽是否ready,前后端截流是否完备,是否需要队列化请求等等。
设计难点
瞬时峰值
瞬时峰值会挑战服务器带宽
秒杀的一瞬间,带宽可能是平常时的几倍几十倍,一瞬间带宽可能就跑满了。
瞬时峰值会挑战应用服务器资源
几十倍的流量,如果后端架构没有足额的设计。会在极短的时间内雪崩,秒杀类的业务,活动结束的时候流量又会断崖式的下跌,没有前期良好的设计,几乎不可能在出现峰值的短短三两分钟里给出有效的应急方案。另外,如果服务间没有良好的隔离,也会影响其他业务服务的运作。
瞬时峰值会挑战DB负荷能力
如果大量的请求落到DB,海量的请求,读写一份库存数据,读写冲突,会出现大量的锁与等待,接下来就是龟速响应跟崩溃了。
思路
越早拦截,成本越低,吞吐量越大
简单抽象,常见的应用架构是这样的
接口应用层(APP/浏览器等)-> 服务层 -> 存储层
核心思路是要把请求到达存储层之前尽可能多地拦截掉,越多的请求走到后面,架构与硬件成本就越高。搭建百万并发又支持事务的DB集群,可比一个百万级并发读的静态HTTP服务集群难多了。
如何拦截
接口应用层(APP/浏览器等客户端)
按钮置灰,防止重复点击
//####
// 防止一个页面中重复点击
//####
if button_is_clicked
return
button_is_clicked = yes
post_data()
//####
// 校验是否5秒内点击过,
// 防止多个页面重复点击
//####
if clicked_within_5_seconds
return
//跨页面存储在本地,如cookie
clicked_within_5_seconds = yes
post_data()
错峰提交
抢购开始后,为避免大量流量极短时间内涌入服务端,可以在客户端要求用户执行一些操作后才能点击购按钮,分流压力。譬如,
- 计算一个简单的数学题
- 输入验证码
- 输入一串中文
- 回答一个调皮的问题
异步显示抢购结果
抢购与抢购结果的显示,产品上应避免设计进一个同步流程里,这样一方面可以为服务端赢得喘息的机会,也可以第一时间响应信息给用户,提高体验。
同步流程:
点击抢购 -> 同步等待服务端结果 -> 显示结果
异步流程:
点击抢购 -> 服务端响应201 -> 显示抢购中的页面
-> 【若干秒】后异步拉取抢购结果 -> 显示结果
此处的【若干秒】也有很多想象空间。
譬如说,50%的用户是5秒后到服务端拉取结果,
50%的用户是10秒后。这样也同样实现了错峰。
服务层
基于user_id去重,防止刷量
前端保护是非常重要的一环,可以有效拦截普通用户,但也是最不可控的一环,有许多可以绕过的方法。需要服务端根据一个唯一标识再进行一次单用户去重拦截。伪代码与客户端类似
//####
// 校验该用户是否5秒内点击过
//####
if user_clicked_within_5_seconds
return
限制并发连接数
以IP为条件限制并发连接数,会出现一定的误杀概率。一般会冗余一定的量,在误杀与有效拦截间取一个平衡。
使用MQ,拦截到DB的量
-
维护一个请求计数,只通过比实际库存量稍大的请求到MQ里,其余请求响应已抢完
-
使用若干个worker更新库存抢购
//请求数小于库存量130%
if mq_count < quantity * 1.3
push_to_mq()
mq_count++
return 201
response('抢光啦')
//worker更新库存
while mq_has_data
//乐观并发锁
update_quantity_where_version_is_1()
缓存的使用
扛多读少写的并发,缓存的合理使用尤为关键。
1,客户端缓存:静态资源放到CDN里,回源请求要尽可能少。
2,服务端缓存
(1)热点读取的数据,需要预热好放到redis/memcache,甚至本地缓存中
(2)超量的请求响应,避免在运行时拼装,可以建立一套网关机制,直接响应本地缓存。(如:Nginx-Lua)
常见问题
如何点亮抢购
1,客户端计时,时间到之前置灰按钮
优点:实现简单
缺点:客户限制很容易被绕过
2,服务端维护一个计时服务器,当计时完成,推送结果到各个服务器,秒杀开始。
优点:实现简单,worker只需要监听推送结果即可。
缺点:计时服务器有单点问题,且推送到各个服务器时间上有先后,容易出现有些请求落下来抢购开始,有些没有开始。瞬间压力会撑爆抢购开始的机子上。
3,Redis中存储一个ttl为抢购开始时间的key,各个服务器通过校验key是否过期,来判别活动开始。
推荐使用:一个高可用的Redis集群,能轻松扛过10万+的并发读
如何托底
最外层的LB,如果是基于硬件的。需要有一个崩溃阈值,一旦超量,要么直接抛弃连接。要么路由到一个CDN里的文件,提示“抢光啦”等。
超卖问题
加锁,通过前面的拦截,DB层的量已经所剩无几。果断加乐观并发锁。
带宽/服务器扩容
活动前需要进行容量评估,秒杀系统的部署也需要独立于其他的应用服务器。类似阿里云/腾讯云的按量付费服务器是个不错的选择,活动结束后再把数据同步回自己的服务器。
肉鸡问题
这是最让人头疼的问题,职业羊毛一般有大量的账号。往往从各个维度上看,都是正常的用户,防不胜防。但依然有一些方式可以防范
1,IP风险评估
2,实名认证
3,根据账号过往的交易进行风险评估,过滤高风险的账户
4,如果依赖于第三方平台,可以使用他们系统本身的风控功能,比如:腾讯的天御 https://cloud.tencent.com/document/api/295/1774