秒杀能够以极小的经费撬动巨大的流量,虽然会带来一定的口碑损失,但因为极具性价比,所以经常被运营同学使用。本文介绍如何设计一款能够支撑60W QPS的秒杀系统,希望能够帮助到大家。
这套系统有着漫长的演变历史,从最初利用Nginx、PHP,到后来使用GO,团队慢慢的将系统做的更加稳定。唯一不好的地方是,当年我写的后台还在使用(写前端代码能力有限),运营配置体验上有些瑕疵,后期需要优化一版。
目前14台8+32的机器,可以支撑60W QPS,理论上还能支持的更高,不过单ELB的上限是60W,即使流量再高,在ELB层也会溢出了。
一般大家听到秒杀系统,最可能想到的是高并发,但高并发只是其中的一部分,需要其他的组件一起配合,才算是一个完整的秒杀系统。
本文从这几个方面来讲述该系统
不过在讲述之前,我们先看一下应用场景,让大家对秒杀有一个直观的了解。
在活动页面上会有抢购模块,会展示抢购的时间、商品图片、商品名称、秒杀价、商品价等信息。活动开始之前按钮为Coming soon。
当活动时间到了,按钮会变为Buy now,瞬间服务器压力飙升。点击Buy now时,如果秒杀成功,会跳转到购物车页,这时候只需要按照正常流程支付即可。如果不成功,按钮会变为Out of stock。当然,如果不点击Buy now按钮,该按钮文案不会变更,除非重新刷新页面。
秒杀活动完成后,会在页面上展示秒杀成功用户的id。之所以添加这个功能,是因为很多用户投诉这是假秒杀,作为商家,做活动也不容易。
通过对场景的描述,可以分析出后台需要配置的内容。https://www.processon.com/view/link/5fb0a1e75653bb657c335c60
每场活动配置:需要配置每场秒杀活动的开始时间和结束时间,以及参加秒杀的商品信息。还有一些特殊需求,如只有用户分享后才能参与秒杀等。
对于活动配置,需要有编辑、推送、校验、测试功能
监控后台,必然需要监控线上情况,但是对于测试情况也需要进行监控,主要为了便利测试人员查看。监控一般关注于:中奖用户、秒杀卖出数量是否和配置数量一致、参与用户数、QPS峰值
最重要的当然是中奖用户数量、秒杀卖出数量与活动配置数量是否一致,如果不一致,那肯定是出问题了,后面面临修数据、补数据。
黑名单管理:有些地区的用户,十分喜欢用脚本刷,对于这些用户,一部分通过程序自动抓取,一部分分析得出后,使用该管理平台,手动添加。
秒杀系统的后台给大家讲完了,下面我们进入大家感兴趣的高并发处理环节。
获取秒杀活动信息,相对比较简单,核心是通过goroutine,设置计时器,每过一段时间从Redis拉取数据,同步到本地缓存,这样能大大减小Redis的压力。
目前该接口,在8+32的机器上,qps能支持到3~4W左右,其实仍然有一定的提升空间。
秒杀接口为最核心的接口,需要保证指定数量的秒杀商品不超卖,也不少卖。这个接口决定了秒杀系统的最终准确性。本来这个接口也做了流程图,不过一是里面有些内容涉及到隐私,另一方面如果给出流程图,可能大家的设计就都一样,少了很多其他的可能性。所以这里只阐述核心点:
本系统使用Redis来管理库存,虽然使用两级限流后,Redis负载不大,但是仍然有出错的可能性。
在库存管理上,通过一切检查后,如果符合规定,会先扣减库存。这样保证了不会超卖。
有一种情况,如先扣减库存,添加购物车失败,但是归还库存失败,这样会导致少卖。对于这种情况,目前做法为记录日志,活动结束后,如果数据不对,根据日志进行补发。
对于这种情况的优化,我能想到的办法有错误重试、错误写入队列后异步处理、分析日志自动处理错误。这几种方案,在某些极端情况下,仍然会失效,如果大家有更好的方案可以提供一下。
不过因为日志的存在,让我们有了保底的方案,而且如果在如此小流量下,Redis都无法稳定的话,可能问题就不仅仅是这一个服务了。
对于秒杀成功用户的统计,比较容易完成,秒杀成功后写入Redis即可。
但是对于秒杀流量的统计,就无法使用这种方案了,毕竟60W的流量,Redis可能也撑不住。
这里介绍一个比较巧的方案。
每次请求秒杀接口时,使用golang的原子操作,将统计变量statNow的值加1
起goroutine,设置定时任务,计算当前统计总数与上次统计总数的差值,写到Redis中
ticker := time.NewTicker(time.Millisecond * 100)
go func() {
for range ticker.C {
orig := atomic.LoadUint64(&statOrig)
now := atomic.LoadUint64(&statNow)
num := int64(now - orig)
if num > 0 {
//将增加的数值incr到Redis中
}
atomic.SwapUint64(&statOrig, statNow)
}
}()
Golang是门好语言,帮我们解决了众多问题。单机使用Nginx,并发2W左右,不使用Nginx,直接用go,并发4W,在语言层面上直接解决了高并发问题。
使用两级限流策略,保证服务器压力可控。
灵活运用Go提供的功能,Goroutine、定时器、本地存储、原子操作、读写锁等。
合理使用Redis,保证服务的准确性与稳定性。
虽然还有少许的待完善点,但并不影响使用。
如果后续压力继续增加,一个可行方案是CDN边缘计算。当然,如果有钱,不必这么扣扣索索的,堆机器也是可以的。
大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)
我的个人博客
往期文章回顾:
技术
读书笔记
思考