秒杀系统

1,447 阅读9分钟

简介

秒杀能够以极小的经费撬动巨大的流量,虽然会带来一定的口碑损失,但因为极具性价比,所以经常被运营同学使用。本文介绍如何设计一款能够支撑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。之所以添加这个功能,是因为很多用户投诉这是假秒杀,作为商家,做活动也不容易。

后台

通过对场景的描述,可以分析出后台需要配置的内容。www.processon.com/view/link/5…

  1. 每场活动配置:需要配置每场秒杀活动的开始时间和结束时间,以及参加秒杀的商品信息。还有一些特殊需求,如只有用户分享后才能参与秒杀等。

  2. 对于活动配置,需要有编辑、推送、校验、测试功能

    • 校验功能:主要用于查看推送出去的数据是否和配置的数据一致,主要用来检查系统正确性、运营操作正确性

    • 测试功能:主要用于白名单测试,使测试人员可以在活动页面真正的演练秒杀过程,同时又不影响正常用户。因为一次秒杀活动可能有多个场次(如每天一场秒杀,每场秒杀两个商品,持续7天),为了让测试同学方便配置,只需要设定好第一场的时间,根据每场活动时间间隔,其他场次的秒杀会自动配置好,商品数量只需设置一次,所有场次商品数量都为该值。

  3. 监控后台,必然需要监控线上情况,但是对于测试情况也需要进行监控,主要为了便利测试人员查看。监控一般关注于:中奖用户、秒杀卖出数量是否和配置数量一致、参与用户数、QPS峰值

    最重要的当然是中奖用户数量、秒杀卖出数量与活动配置数量是否一致,如果不一致,那肯定是出问题了,后面面临修数据、补数据。

  4. 黑名单管理:有些地区的用户,十分喜欢用脚本刷,对于这些用户,一部分通过程序自动抓取,一部分分析得出后,使用该管理平台,手动添加。

秒杀系统的后台给大家讲完了,下面我们进入大家感兴趣的高并发处理环节。

高并发系统设计

获取秒杀活动信息

获取秒杀活动信息,相对比较简单,核心是通过goroutine,设置计时器,每过一段时间从Redis拉取数据,同步到本地缓存,这样能大大减小Redis的压力。

目前该接口,在8+32的机器上,qps能支持到3~4W左右,其实仍然有一定的提升空间。

  1. 可以将部分不变的数据放到CDN,库存、当前场次等动态变化的信息提供新接口,这样可以进一步减少后端冗余的逻辑和返回数据量,不过对前端要求会提高。
  2. 即使是在当前的逻辑中,有部分场次的活动,因为并不参与展现,所以可以不参与计算,同时也无需返回,一定程度上也能提高性能。

秒杀接口

秒杀接口为最核心的接口,需要保证指定数量的秒杀商品不超卖,也不少卖。这个接口决定了秒杀系统的最终准确性。本来这个接口也做了流程图,不过一是里面有些内容涉及到隐私,另一方面如果给出流程图,可能大家的设计就都一样,少了很多其他的可能性。所以这里只阐述核心点:

  1. 使用两级限流措施,第一级为随机限流,第二级为令牌桶,限流的比例根据预估流量和商品数量来限制,尽量确保1s内所有商品售卖完成。例如,10个商品,60W请求,如果随机限流设置为千分之二,意味1s只有1200个请求能真正走到逻辑层,逻辑层压力会小很多。而令牌桶能够防止逻辑层过载。
  2. 黑客是需要重点考虑的对象
    • 如果提前请求则标记为黑客,进行记录
    • 如果1s内同一个用户多个请求到达逻辑层,标记为黑客,进行记
  3. 对于走到逻辑层的请求,需要做众多判断,确保系统准确性
    • 该用户或者IP不在黑客列表里
    • 该用户本次活动中没有秒杀成功过
    • 同一个用户不能获得两次秒杀成功的机会
    • 是否仍然有足够的库存
    • 帮用户按照秒杀价添加到购物车

本系统使用Redis来管理库存,虽然使用两级限流后,Redis负载不大,但是仍然有出错的可能性。

在库存管理上,通过一切检查后,如果符合规定,会先扣减库存。这样保证了不会超卖。

有一种情况,如先扣减库存,添加购物车失败,但是归还库存失败,这样会导致少卖。对于这种情况,目前做法为记录日志,活动结束后,如果数据不对,根据日志进行补发。

对于这种情况的优化,我能想到的办法有错误重试、错误写入队列后异步处理、分析日志自动处理错误。这几种方案,在某些极端情况下,仍然会失效,如果大家有更好的方案可以提供一下。

不过因为日志的存在,让我们有了保底的方案,而且如果在如此小流量下,Redis都无法稳定的话,可能问题就不仅仅是这一个服务了。

统计

对于秒杀成功用户的统计,比较容易完成,秒杀成功后写入Redis即可。

但是对于秒杀流量的统计,就无法使用这种方案了,毕竟60W的流量,Redis可能也撑不住。

这里介绍一个比较巧的方案。

  1. 每次请求秒杀接口时,使用golang的原子操作,将统计变量statNow的值加1

  2. 起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边缘计算。当然,如果有钱,不必这么扣扣索索的,堆机器也是可以的。

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客

往期文章回顾:

技术

  1. 秒杀系统
  2. 分布式系统与一致性协议
  3. 微服务之服务框架和注册中心
  4. Beego框架使用
  5. 浅谈微服务
  6. TCP性能优化
  7. 限流实现1
  8. Redis实现分布式锁
  9. Golang源码BUG追查
  10. 事务原子性、一致性、持久性的实现原理
  11. CDN请求过程详解
  12. 常用缓存技巧
  13. 如何高效对接第三方支付
  14. Gin框架简洁版
  15. InnoDB锁与事务简析
  16. 算法总结

读书笔记

  1. 敏捷革命
  2. 如何锻炼自己的记忆力
  3. 简单的逻辑学-读后感
  4. 热风-读后感
  5. 论语-读后感
  6. 孙子兵法-读后感

思考

  1. 对项目管理的一些看法
  2. 对产品经理的一些思考
  3. 关于程序员职业发展的思考
  4. 关于代码review的思考
  5. Markdown编辑器推荐-typora