秒杀系统设计

秒杀系统设计

秒杀这一业务场景已经发展多年,有套路可循。另外,秒杀属于极端大流量场景,它的应对经验对Web大流量应对方案有很好的借鉴意义。

秒杀系统本质

秒杀正常的业务流程:查询商品 -> 创建订单 -> 减库存 -> 更新订单 -> 付款 -> 卖家发货。

而业务特性是:(1)低廉价格;(2)大幅推广;(3)瞬时售空;(4)一般是定时上架;(5)时间短、瞬时并发量高。

从技术角度看秒杀系统本质上是一个满足大并发高性能高可用的分布式系统。秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。

总的来说,架构是一种平衡的艺术,而最好的架构一旦脱离了它所适应的场景,一切都将是空谈。具体设计应该参照秒杀预估流量:

  1. QPS 小于1W:只需要把商品购买页面增加一个定时上架功能,仅在秒杀开始时才让用户看到购买按钮,当商品库存卖完了也就结束了。
  2. 随着请求量加大(QPS 1W/s -> 10W/s),这个简单的架构就很快遇到了瓶颈,因此需要做架构改造来提升系统性能。
  3. QPS 100W/s 以上

怎样设计降低服务压力

一、架构设计原则

1.数据要尽量少

  • 用户请求的数据能少就少。具体包含上传给系统的数据和系统返回给用户的数据。原因是首先这些数据在网络传输需要时间,其数据传输都需要服务器做压缩和字符编码,都非常消耗CPU,所以减少传输的数据量可以显著减少CPU的使用。

  • 系统依赖的数据能少就少。依赖的路径越多会增加CPU处理时间(序列化和反序列化),同样会增加延时。

常见设计手段为:动静分离。

动静分离

具体为变刷新整个页面为只点击“秒杀”按钮就够了。动静分离后,客户端大幅度减少了请求的数据量。
分离改造核心:分离出动态数据。 如url唯一化,分离浏览者相关因素,分离时间因素,异步化地域因素,去掉cookie等。

对静态数据缓存:1. 静态数据缓存到离用户最近的地方。浏览器、CDN、服务端Cache。2.静态化改造直接缓存HTTP连接 3. Web服务器流入Nginx缓存静态数据优于Tomcat。
对动态数据缓存:
1.ESI(edge side includes)服务端拼接动静态内容,组装一起返回,服务端性能有影响,但是客户端体验好
2.CSI(client side include)客户端发起异步js请求,服务端性能好,客户端可能会有延时,体验稍差.

部署架构:

需要解决(失效问题,命中率问题,发布更新问题),其他细节:浏览器缓存和cdn缓存差别很大;合并是否用gzip压缩。

2.请求数要尽量少

用户请求的页面返回后,浏览器渲染这个页面还包含其他的额外请求。例如页面依赖的CSS/JS, 图片, Ajax请求等都被定义为“额外请求”,这些额外请求应该尽量少。因为上述每个资源请求都能增加连接(需要做三次握手),可能造成资源串行加载,不同域名还有DNS解析。解决办法:合并CSS/JS文件。

常见设计手段为:流量削峰。

流量削峰

本质上:延缓用户请求的发出。让服务处理更加平稳,节省服务器成本。
削峰基本思路如下:

  • 排队:用MQ来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一段承接瞬时的流量洪峰,在另一端平滑的将消息推送出去。

除了利用MQ,还可以使用线程池加锁方式实现排队,FIFO内存排队。这样就会存在异步返回结果问题:解决方案有两种1. 客户端轮询,例如支付页面,每秒轮询一次; 2.服务端push结果。需要C/S保持长连接。

  • 答题:防作弊,延缓请求。
  • 分层过滤:对请求进行分层过滤,讲请求尽量拦截在系统上游。传统秒杀系统之所以挂,请求都压到在后端数据库层,数据读写锁冲突严重,并发响应高,几乎所有请求都超时。流量虽大,下单成功的有效流量甚小。分层过滤其实就是采用“漏斗式”设计来处理请求。核心思想为:在不同层次尽量过滤掉无效请求[根据库存判断无法抢到商品的人],让“漏斗”最末端的才是有效请求。
    • 读系统尽量减少一致性校验的瓶颈,但尽量将不影响性能的检查条件提前
    • 写系统主要对写数据进行一致性检查

3.路径尽量短

路径:用户发出请求到返回数据这个过程中,需要经过的中间节点数。

这是因为每增加一个连接都会增加新的不确定性。从概率统计上说,假如一个请求经过5个节点,每个节点可用性是99.9%的话,那么整个请求的可用性是:99.9的5次方,约等于99.5%。缩短路径不仅可以增加可用性,同样可以有效提升性能(减少中间节点可以减少数据的序列化和反序列化),并减少延时。

有一种缩短访问路径办法: 多个相互强依赖的应用合并部署在一起,把远程调用RPC 变成JVM内部之间的方法调用。

4.依赖要尽量少,系统分级

展示秒杀页面,这个页面必须强依赖商品信息、用户信息,还有其他如优惠券、成交列表等这些对秒杀不是非要不可的信息(弱依赖),这些弱依赖在紧急情况下就可以去掉。要减少依赖就必须对系统进行分级。0级系统要尽量减少对1级系统的强依赖,防止重要的系统被不重要的系统拖垮。在极端情况下可以把1级系统例如优惠券系统降级。

5.不要有单点

无单点的重点是避免将服务的状态和机器绑定,即把服务无状态化。

应用无状态化是有效避免单点的一种方式,但是像存储服务本身很难无状态话,因为数据要存储在磁盘上,本身就要和机器绑定,那么这种场景只能通过冗余多个备份来解决单点问题。

二、热点数据处理

为什么要处理热点数据?热点请求会大量占用服务器处理资源,虽然这个热点可能只占请求总量的亿分之一,然而却可能抢占90%的服务器资源。

什么是热点:热点分为热点操作和热点数据。

  • 热点操作,例如大量的刷新页面、大量的添加购物车、双十一零点大量的下单。对系统来说,这些操作可以抽象为“读请求”和“写请求”。热点操作中的写操作将下面单独一节讲解。
  • 热点数据:用户的热点请求对应的数据。热点数据分为“静态热点数据”和“动态热点数据”。
    • 静态热点数据:可以提前预测的热点数据。业务场景,通过卖家报名来打标。还可以通过数据分析历史成交记录,用户购物车记录分析出热点商品。
    • 动态热点数据:不能被提前预测的热点数据,系统在运行过程中临时产生的热点。例如上家临时做了广告导致的热点数据。解决方案:构建数据动态发现系统,分析热点Key,数据上报统计。

处理热点数据:
一、优化 : 缓存。热点数据动静分离。
二、限制 : 热点数据限制到一个请求队列里,防止热点数据占用太多服务器资源导致其他请求无法处理。
三、隔离

  • 系统隔离:为避免对现有网站业务的冲击:分组部署,将热点描述请求分到单独的集群。秒杀系统只是一个短时的促销活动,具有时间短、访问量高的特点。如果模块与原业务系统部署在一起,将对现有的业务造成冲击。因此,应当把秒杀模块迁移出去,独立部署。
  • 数据隔离:热点秒杀数据启用单独的Cache/MySQL集群。
  • 业务隔离:卖家报名秒杀提前感知热点,做数据预热。

三、 性能优化

核心:降低CPU消耗。

衡量指标

1
总QPS = (1000ms / RT) * 线程数量

其中线程数量一般默认配置为 2*CPU核数 + 1。

优化方法

  • 减少编码
  • 减少序列化
  • 服务优化(如nginx返回静态数据,框架定制优化)
  • 并发读优化:应用层的LocalCache,在秒杀系统的单机上缓存商品相关的数据.
  • 静态数据(秒杀前全机推静态cache数据)
  • 动态数据(类似库存,一般缓存几秒,被动失效,允许一定的脏数据)
  • 流程:发现数据,减少短板,数据分级,减少中间环节,做好应用基线(性能基线,成本基线,链路基线)不断调整

四、并发写-减库存

秒杀系统设计除了上述的并发读的问题,还有一个难点是如何解决并发写 – 多个用户在同时抢一件商品,也就是并发很高,但集中在同一商品上,造成实质为串行操作。因为在数据库这层本质执行的是对同一件商品扣库存 – 需要合理的减库存。用户的购买过程一般分两步:下单和付款。

1
2
3
BEGIN 
UPDATE stock SET count = count - 1 WHERE skuId = ?
COMMIT

减库存一般有三种方式:

  1. 下单减库存。下单减库存最简单也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖情况。缺点是:恶意下单(有些人下单完不一定会付款,但是库存已经扣了,会影响商家。)

  2. 付款减库存。等到用户付款完后才真正减库存,否则库存一直保留给其他买家。缺点是出现买家下单后无法付款。缺点:可能超卖。

  3. 预扣库存。 买家下单后库存为其保留一段时间,超过这个时间,库存会自动释放。释放后其他买家继续购买。在买家付款前,系统会校验该订单的库存是否还有保留,有保留则尝试预扣,如果预扣失败,则不允许付款;如果预扣成功,则完成付款减库存。缺点:也可能恶意下单(只能结合安全和反作弊,标示用户并限制操作) 。

一般情况下秒杀减库存逻辑复杂,存在SKU库存和总库存联动关系,需要使用MySQL事务. 由于同一数据在数据库里肯定是一行存储,因此会有大量线程来竞争InnoDB行锁,而并发度越高等待线程会越多,TPS会下降,响应事件RT会上升,数据库的吞吐量就会严重手影响。这就会发生单个热点商品影响整个数据库性能,导致0.01%商品影响99.99的商品的售卖。

解决并发锁的问题:

乐观锁/悲观锁

  • 悲观锁:可能会造成大量线程抢锁等待,结果可能会瞬间增大响应时间,造成系统连接数耗尽。
  • 乐观锁:根据版本号的思路,可能会操作操作失败次数增多,需要上层业务重试,或者交给用户重试。
    1
    2
    select * from tab1 where id = ?
    udpate tab1 set col1 = ? where id = ? and version = ?

缺点:在高并发下可能更新失败,所以要通过重试来提高更新效率。

FIFO队列

排队:并行强制改成串行,单机内存队列,如果生产远高于生产可能造成内存爆掉。即使内存没问题,如果消费过慢用户响应时间也会长。

redis watch

如果可以把数据放到内存数据库中,可以考虑redis watch机制,采用乐观锁方式更新。

1
2
3
4
5
6
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

五、高可用建设

参考

  1. 极客时间-如何设计一个秒杀系统
  2. 秒杀系统架构分析与实战
  3. MySQL的并发更新

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×