联系
Knight's Tale » 架构

【Knight技术分享】秒杀架构考虑

2020-04-06 17:36

前沿

来vmate的第一件事情就是做秒杀,秒杀对于高并发的处理非常突出,是比较能体现技术能力的一个业务场景。

问题

用户秒杀(定时领)或者随时领的行为可能由于 太热门,导致请求量过大,担心 引起系统的各种问题

目标

通过技术手段,解决

  • 当前:高并发秒杀问题
  • 未来:构建良好架构,可以适应高并发;通用性,为其它场景做准备

秒杀特点

  • 瞬时并发量大。
    • 大量用户会在同一时间点进行抢购;
    • 网站瞬时访问量激增
  • 库存少
    • 访问请求量远远大于库存数量
    • 只有少数部分用户能够秒杀成功
  • 业务流程比较简单

技术难点

  • 对现在业务的影响
    • 可能造成其它功能模块的当机。秒杀是营销活动的一种,如果和其它营销活动应用部署在同一服务器上,肯定会对现有其它活动造成冲击,极端情况下可能导致整个电商系统服务宕机。
  • 功能难点
    • 流量过大,不够使用,无法访问服务。秒杀活动开始前后,会有很多用户请求对应商品页面,会造成后台服务器的流量突增,同时对应的网络带宽增加,需要控制商品页面的流量不会对后台服务器、DB、redis等组件造成过大的压力 。
    • 读操作过大。不停的刷页面
    • 写操作过大。疯狂点击按钮,如果订单涉及较重业务逻辑,则会响应很慢。

第一版本架构

第一版的逻辑是前端同学做的,这段的问题在于:

  • 业务逻辑不容易维护。由于是前端同学,因此逻辑放在nodejs层,只是把后台服务当成各种api进行调用。
  • 大库存大流量风险问题。没有异步化架构。虽然采用redis进行扛流量,但是一旦库存量和流量变大,则由于同时满足购物条件的人过多,导致后台处理不过来。
  • 一致性问题。秒杀过程涉及到订单、积分、商品等多表的操作。nodejs操作不方便有事务的控制。

改进策略

  • 公平性
    • 防刷机制。限制n秒内,用户访问次数。
    • 倒计时前后端同步
    • 僵尸帐户防刷,在业务侧做实例制。
  • 隔离
    • 业务隔离:秒杀系统,属于短时间内高并发处理,防止影响其它正常业务,应该需要独立 部署
    • 动静隔离:秒杀页面尽可能减少对后台依赖,绝大部分走CDN,只有少部署动态数据请求服务
  • 网络及带宽增长压力
    • 尽可能越在靠近用户侧拦截越多无效请求越好。
    • 减少冗余数据的返回
  • 保证秒杀接口快
    • 秒杀接口足够简化
    • 复杂逻辑异步处理
    • 尽可能越在靠近用户侧拦截越多无效请求越好。
    • 读走缓存
  • 数据一致性
    • 将大量无效请求拦截在数据层之前,有效请求(少量)通过redis/db等中间数据层保证数据一致性。
  • 可扩展性
    • 多级缓存,各个层级,尽可能多的做缓存,以拦截无效请求。 我们的解法
  • 限流
    • 由于活动库存一般比较少,对应的只有少一部分人才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户进入后端服务器
  • 削峰
    • 秒杀一瞬间,会有大量用户进来。一般来说,采用缓存和MQ中间件来解决。
  • 异步
    • 秒杀其实可以当做高并发系统来处理,在这个时间,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,也提高网站的整体可用性
  • 缓存
    • 秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到OLTP数据库,譬如mysql。由于数据库底层采用B+数储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移至内存的缓存或者redis中,会极大的提高效率。
  • 一致性
    • 由于采用异步、缓存等技术,会导致业务数据存在不一致性。我们主要通过最终一致性来解决总问题。譬如,可以通过定时引擎将出错的数据修复。

架构

【秒杀前】前端架构

前端nginx可以采用以下限流方式

  • 秒杀活动页,限制一秒最多刷1-2次
  • 倒计时前无法点击(与后台进行同步)
  • 图片CDN

【秒杀前】商品列表页架构

上一步中依赖的时间校验校准服务,由于为了避免流量拥堵,是开一个新的单独服务来做这件事情。

秒杀前,用户会不停的刷商品列表页,流量较大,将数据先缓存到 redis中,可以档住很大的流量。

在这里所有,所有数据均是内存状态。速度很快。

【秒杀前】商品详情页架构

在商品详情页,还需要计算用户是否能购买这个产品,譬如在积分兑换场景中,则是 计算用户积分是否足够。

在这里,我们将用户的积分变化实时的同步到redis中,最终形成 积分读服务。 通过这种方式,用户是否能购买 在抢购前就已经决定,相当于筛选掉大部分没有资格的用户。

在这里,所有数据还是内存态。

【秒杀中】后台写架构(核心)

秒杀进行时,是非常重要的。原则上,我们应该尽可能快的返回,将复杂的业务尽可能异步化。

因此,

第一步,【限流】先去掉些不良用户。譬如通过反作弊或者限流框架来阻止用户进入。譬如后台崩溃。

第二步,【限流】再去掉一些没有资格的用户。通过redis内存化操作来加快读取。当用户量非常大时,这里需要对redis做分片,做到线性扩展。 如果 用户一天可以抢购多次,在这里可以利用redis来做一些积分扣减操作。避免用户第二次抢购时,由于后面的异步化操作还未完成,发现还可以继续抢购的问题。 如果 用户一天只能抢购一次,那么简单,直接增加用户的抢购次数。 注意,这里直接更改用户的redis数据,由于是分布式操作,如果后面订单处理失败,则可能导致用户无法继续进行第二次操作的问题。下面会解决这个问题。

第三步,【分片】大部分业务情况,均需要马上告诉用户是否抢成功了,并且让用户看到这个抢购记录。因此,我们在这里需要使用MSQL来记录抢购订单。 由于用到了OLTP服务,瓶颈就很明显了。因此,在这里我们需要做分片。并且只做生成订单的操作,尽快的返回。 这是唯一与用户直接有关的OLTP操作了,因此需要特别小心。避免把mysql搞死。

第四步,【削峰】业务处理不单单是生成订单这么简单,还需要很多复杂的业务逻辑,因此,这里我们通过MQ来做削峰。为了做到实时化,我们需要根据业务的流量情况,动态的增大MQ和处理速度。

第五步,【削峰,事务】秒杀带来的其它业务操作,统一在这里,在这里,我们要关注事务的一致性。多种操作需要在事务中进行。必要时,这里对mysql进行分片处理,以提高处理速度。

考虑以下场景:

  • 场景一:在第二步中用户的积分扣了,但是订单生成失败了。需要通过异步任务将积分 被回来。
  • 场景二:特殊情况下,在异步处理过程中,用户的积分突然不够了(可能这个过程 中其它地方也扣减积分了),这里在任务处理时,就需要对这种数据进行特殊标志,运营人员进行特殊处理,这里就看运营策略,是认为用户的抢购无效呢,还是说的抢购仍然成立。

第六步,【重试·事务一致性】由于第五步涉及过多的第三方API操作,某个过程失败是非常常见的。因此,需要有一个重试服务来将失败的数据进行处理。重试过程应该保证幂等性。

【秒杀后】填写抢购信息

用户抢购完成后,需要填写地址。

如果用户抢购过程中,APP重启了,重新进入系统后,由于订单还在,因此,仍然可以看到这个单子。

作者

微信: knightliao