一个大型网站应用一般都是从最初小规模网站甚至是单机应用发展而来的,为了让系统能够支持足够大的业务量,从前端到后端也采用了各种各样技术,前端静态资源压缩整合、使用CDN、分布式SOA架构、缓存、数据库加索引、读写分离等等。这些技术是高并发系统所必须的,但是今天先不细说,而先谈谈在这些架构既定的情况下,一些高并发业务/接口实现时应该注意的原则,以及通过工作中一个6万QPS的秒杀活动,来介绍一下秒杀业务的特点以及如何优化。
那么,如何才能做得快呢?有两个需要注意的原则1.做得少,一方面是指在功能特性上有所为,有所不为,另一方面是指一次处理的信息量要少。2.做得巧,根据业务自身的特点,选择合理的业务实现方式,选择合理的缓存类型和缓存调用时机。
世界上最快的程序,是什么都不做的程序。一个接口负责的功能越少,读取信息量越少,速度越快。
对于一个需要承受高并发的接口,在功能上,尽量不涉及一些难以缓存和预热的数据。一个典型的例子,用户维度个性化的数据,用户和用户的信息不同,userId数量又很多,即使加上缓存,缓存命中率依然很低,压力还是会打到数据库,不光接口快不了,高并发的sql也会给数据库带来风险。
如果遇到这样的数据,要怎么解决呢?一个办法是尝试转移数据的维度:刚才说的秒杀活动资格信息,如果以用户userId为key,会出现缓存命中率低,仍要sql读的情况,但是能够秒到的用户数量其实很少,所以如果以这次秒杀活动id为key,存储一个成功秒到用户的userid的list,就能够解决缓存命中率低的问题。
大家在做设计的时候,一般会估算一个接口的量级,如果一看就有几千几万个业务对象,就不会这样设计了,但是需要警惕的是业务对象数量级可变的情况,比如随着业务发展数量会快速增长,或者某些特殊维度下业务对象特别多。设计的时候要按照预估的最大量级来,并且对接口/页面做出数量的限制,如果发现当前返回的业务对象过多,可以继续根据业务维度来拆分,分次分批来处理。
举一个例子,比如一个影院下所有的活动场次,开始的时候一家影院下的场次有限,几十一百场,很好展示,后来随着业务发展,一个影院下各个影院下场次数到了几百一千,一次全部拿完,在高并发时,memcached缓存的multiget会出现很多超时,请求会打到mysql数据库,给系统很大压力。之后我们做了改造项目,每次根据用户的交互按照影片、日期、影院的维度来分批取,一次只有十几个场次,接口响应变快了,服务的压力也小的多。
平时涉及到的业务,总有属于它的特性,比如实时性要求多高,数据一致性要求多高,涉及什么维度的数据,量有多大等等,我们要根据这些特性来选择实现的方案,比如一些统计数据,如某类目下所有商品的最低价,按照逻辑需要遍历商品来获取,但这样每次实时读取所有的对象,涉及读取缓存数据库操作,接口会很耗时,但如果选择作业离线计算,把计算结果写表,加上缓存,搜索直接读取,显然会快很多了。
涉及到业务各阶段特性的例子就是秒杀系统,在第二部分秒杀实践中我会详细介绍。
除了业务特性方面,缓存是业务对抗高并发非常重要的一个环节,合理选择缓存的类型和调用缓存的时机非常重要。
我们知道内存运算速度快于远程连接,所以存储上来说效率如下内存<=ehcache 秒杀业务的典型特点有:1.瞬时流量大2.参与用户多,可秒杀商品数量少3.请求读多写少4.秒杀状态转换实时性要求高 达到的效果是活动期间,加载页面请求都会打到varnish机器直接返回,而不会给web和service带来任何压力。 秒杀请求是一个秒杀系统能不能抗住高并发的关键因为秒杀请求和之前两个请求不同,它是写请求,不能缓存,而且是活动峰值的主力。 一个用户从发出秒杀请求到成功秒杀简单地说需要两个步骤:1.扣库存2.发送秒杀商品这是至少两条数据库操作,而且扣库存的这一步,在mysql的innodb引擎行锁机制下,update的sql到了数据库就开始排队,期间数据库连接是被占用的,当请求足够多时就会造成数据库的拥堵。可以看出,秒杀请求接口是一个耗时相对长的接口,而且并发越高耗时越长,所以首先,一定要限制能够真正进行秒杀的人数。 可抢状态需要第三个因素来决定,那就是当前秒杀的排队人数。加在判断库存剩余之前,挡上一层排队人数的校验,即有库存并且排队人数<限制请求数=可抢,有库存并且排队人数>=限制请求数=抢完 比如2500个名额秒杀名额,目标放过去3000个秒杀请求 那么排队人数记在哪里?这个可以有所选择,如果只记请求个数,可以用memcached的计数,一个用户进入秒杀流程increase一次,判断库存之前先判断队列长度,这样就限制了可参与秒杀的用户数量。 发起秒杀先去问排队队列是不是已满,满了直接秒杀失败,同时可以去更新之前缓存了是否可抢trueorfalse的缓存,直接把前台可抢的状态变为不可抢。没满继续查询库存等后续流程,开始扣库存的时候,把当前用户id入队。这样,就限制了真正进入秒杀的人数。 这种方法,可能会有一个问题,既然限制了请求数,那就必须要保证放过去的用户能够秒完商品,假设有重复提交的用户,如果重复提交的量大,比如放过去的请求中有一半都是重复提交,就会造成最后没秒完的情况,怎么屏蔽重复用户呢?就要有个地方来记参与的用户id,可以使用redis的set结构来保存,这个时候set的size代表当前排队的用户数,扣库存之前add当前用户id到set,根据add是否成功的结果,来判断是否继续处理请求。 最终,把实际上几万个参与数据库操作的用户从减少到秒杀商品的级别,这是一个数据库可控制的范围,即使参与的用户再多,实际上也只处理了秒杀商品数量级的请求。 1.分库存一般这样做就已经能够满足常规秒杀的需求了,但有一个问题依然没有解决,那就是加锁扣库存依然很慢假设的活动秒杀的商品量能够再上一个量级,像小米卖个手机,一次有几W到几十万的时候,数据库也是扛不住这个量的,可以先把库存数放在redis上,然而单一库存加锁排队依然存在,库存这个热点数据会成为扣库存的瓶颈。 这样专门为高并发设计的系统最大的敌人是低流量,在大部分库存都好近,而有几个剩余库存时,用户会看到明明还能抢却总是抢不到,而在高并发下,用户根本就觉察不到。 2.异步消息如果有必要继续优化,就是扣库存和发货这两个费时的流程,可以改为异步,得到秒杀结果后通过短信/push异步通知用户。主要是利用消息系统削峰填谷的特性来增加系统的容量。