首页 > 技术文章 > 秒杀系统设计

peterleee 2019-06-23 13:14 原文

1.秒杀系统架构设计优化:

前端浏览器秒杀页面   

    1.CND静态缓存,对于一些静态css文件不用频繁的像数据库请求。

    2.在js层面禁止重复提交,对同一用户限流,在提交达到一定次数把按钮置为灰色不可点击)。

    3.使用验证码,防止机器人,爬虫,从而分散用户请求。

中间代理服务   

    1.反向代理nginx使用负载均衡分担服务器的压力。

    2.限制同一UserID访问频率:尽量拦截浏览器请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问uid,限制访问频率;对于同一个item的查询,比如手机车次,做页面缓存,x秒到达站点的请求,返回同一页面。

后端服务层 

    1.使用Redis动态缓存,对于写操作,把数据库的库存减少操作转移到redis缓存中,当redis中的缓存量变为0的时候统一写入数据库。

    2.读写分离,也可以应用缓存解决,大部分请求都是查询操作,利用缓存缓解数据库压力。

    3.采用消息队列缓存请求,消息队列要设置一定的长度,防止OOM,插入消息队列的过程就像一个生产者的角色,线程池(启动多个工作线程)从消息队列拿出请求的就是一个异步消费者的角色。

    4.业务分离,将秒杀业务放到单独的服务器上面,集中资源对抗请求抗压。

    额外附加: 将UserID存到redis的set里面,也可以限制同一UserID访问频率

数据库层

(多建立索引进行优化)

   1.垂直拆分,比如个人信息和登录信息可以分为两个表,对于一些大字段也可以单独建表,比如图片,markdown大文本,我们专门建表进行查询。

   2.水平拆分,通过主键id取模分表,表的结构相同,数据不同,通过时间分表,每一个月生成一张表。

   3.以上都是在同一台机器上操作,我们还可以通过一致性hash算法来分库操作。

 

优化设计思路:将请求拦截在系统上游,降低下游压力。在一个并发量大,实际需求小的系统中,应当尽量在前端拦截无效流量,降低下游服务器和数据库的压力,不然很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。 

 

2.整体设计思路:

  • 限流:屏蔽掉无用的流量,允许少部分流量流向后端。
  • 削峰:瞬时大流量峰值容易压垮系统,解决这个问题是重中之重。常用的消峰方法有异步处理、缓存和消息中间件等技术。
  • 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
  • 内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
  • 可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。主要针对nginx中间代理服务,针对特殊情况,增加服务器数量。
  • 消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。
  • 充分利用缓存:利用缓存可极大提高系统读写速度。

 

3.整个设计中可能遇到的一些问题

1.高并发的挑战,需要敢于拒绝请求: 

我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

然后,这才是真正的恶梦开始。举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)。

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

在这个时候我们重启服务也不能解决问题,如果检测到系统满负载状态,拒绝请求也是一种保护措施

2.有一些自动化脚本同一账号会发送上百个请求,如何解决这种问题?

上面我们提到,可以分别从前端,代理服务层,应用服务层,去控制一个userId的访问频率,在高并发的情况下,在判断的过程中我们还是会漏掉一些请求,所以我们要尝试在上层给他多做一些阻拦,弹出一些验证码,包括严格一点我们也可以直接禁止这个ip。

3.通过代理不断生成随机ip,2中的方法是否就无法解决这些问题了。

这个时候已经几乎和真人差不多了,我们就要尝试去用业务的方法去解决这个问题了,账号很可能属于同一个号码段甚至是连号的,活跃度不高,等级低,资料不全等等。根据这些特点,适当设置参与门槛,例如限制参与秒杀的账号等级。通过这些业务手段,也是可以过滤掉一些僵尸号

4.超发问题,也就是在高并发的情况下,可能导致抢购的数量大于余量?这就涉及到锁的问题

     方案1:悲观锁,在修改数据的时候采用锁定状态,排斥外部请求的修改,那么大量的外部请求我们就需要放到消息队列里面了(我们需要给这个消息队列设定一个长度(这个长度就可以设置为商品余量的个数,如果请求大于消息队列的长度我们直接返回抢购结束的页面),防止它被撑爆)

     方案2:乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。(redis的watch好像就是这样一种方案)

 

腾讯面试的一个问题?

10万个请求过来抢购1000个商品,我们要保证10万个请求公平分配?

在请求过来之前,我们就把1000个商品分给这些请求,可能按请求的序号吧(然后把这些数据存到一个set里面),当一个请求过来的时候,我们直接用一个计数器去记录请求的顺序,然后直接从set里面判断是否拿到了商品,可以直接返回。

推荐阅读