秒杀系统——秒杀功能设计理念(代码片段)

流楚丶格念 流楚丶格念     2022-10-23     190

关键词:

文章目录

电商系统下单功能概述

现在的电商系统功能繁多,除了最基本的购买商品功能,还有物流跟踪,订单管理,社区交互等功能。不过面试中关注的主要是购买商品功能,我们将其他次要功能归类为其他业务功能,购买商品流程如下:

  1. 客户通过客户端下单
  2. 如果下单成功则进入支付阶段,否则返回购买失败
  3. 进入支付阶段后,如果在一定时间内支付成功则返回购买成功,否则返回购买失败

订单量:从 0 到 1000(架构1)

想象你自己从零搭建一个电商平台,一开始平台里的商品种类以及日订单量都较少,商品种类有 100 款,日订单量只有 1000 条。

根据以上信息,我们可以设计出简单架构 1,下单流程如下所示:

  1. 客户端发送下单请求给服务端
  2. 服务端查询数据库
    • 若该商品库存大于零,将库存减一并且返回下单成功
    • 若该商品库存等于零的话,返回下单失败

架构 1 简单直观,它忽略了系统可用性以及可扩展性,但在日订单量少,不会出现多位客户对同一件商品同时下单的情况下,它很好地完成了我们需要的功能。

订单量:从 1000 到 100万

但是,经过一段时间后,你的电商平台商品种类增加到 1 万款,日订单量飙升到 100 万条,而且在高峰期,例如晚饭后,睡觉前的订单量会特别多。

这是一个好的消息,这说明你要发财了,不过同时你发现了一个问题,某些商品的成功下单量要大于库存量,也就是说出现了商品超卖的情况。这可是个严重的问题,因为没办法及时交货给客户对电商平台的信誉有极大影响。

仔细分析架构 1 后,我们发现了问题的根源:当商品库存只剩下 1 件而有多位客户同时下单的时候,每个下单请求在查询的时候都发现库存大于零,并且将库存减 1 返回下单成功。下图中,在库存只有 1 件的时候,两个请求却都返回下单成功。

这就是我们常说的并发问题,同时我们也知道的是大部分并发问题都可以通过锁机制或者队列服务来解决,下面我们用锁机制来解决这种数据并发问题:

锁机制

悲观锁

我们可以观察到,超卖问题的原因在于事务查询和更新库区期间,库存已经被其他事务修改了。在学习悲观锁之前,我们先了解下什么是两阶段加锁,两阶段加锁是一个典型的悲观锁策略:

两阶段加锁方法类似,但锁的强制性更高。多个事务可以同时读取同一对象,当只要出现任何写操作(包括修改或删除),则必须加锁以独占访问。—《数据密集型应用系统设计》

我们的电商系统中可以应用两阶段加锁,由于下单请求涉及到修改库存,可以先使用排他锁锁定记录,防止被其他事务所修改。大部分关系型数据库都提供这种功能(在 MySQL 里面的语法是 SELECT … FOR UPDATE)。流程如下图:

  1. 红色请求先获取排他锁,查询和更新库存,在此期间蓝色请求等待获取排他锁。
  2. 红色请求更新库存后释放排他锁,返回下单成功
  3. 蓝色请求获取排他锁,发现库存为 0,释放排他锁,返回下单失败

我们可以看到悲观锁成功解决了商品超卖问题,不过它的缺点也比较明显:

  1. 处理性能不高,当一件商品有多位客户同时下单的时候,每个请求需要等待排他锁,也要较长才知道是否下单成功。

  2. 容易发生死锁:在实际工程中,下单操作不只涉及了库存修改,还可能涉及其他业务功能,由于悲观锁下每个请求都轮流持有锁,应用层的代码处理不好的话会更容易发生死锁。

乐观锁

和悲观锁不同,乐观锁策略下事务会记录下查询时的版本号,当事务准备更新库存的时候,如果此时的版本号与查询时的版本号不同,则代表库存被其他事务修改了,这时候就会回滚事务,流程如下图:

  1. 红色请求与蓝色请求查询库存,并记录库存版本号
  2. 红色请求先更新库存为 0,返回下单成功
  3. 蓝色请求更新前发现版本与之前版本号不同,回归事务,返回下单失败

乐观锁因为并不需要等待锁,所以在事务竞争较少的情况下比悲观锁有更好的性能,缺点是事务竞争较多的情况下,由于经常需要回滚事务导致性能反而较差

分布式锁

分布式锁在服务端以及数据库之间加上分布式组件来保证请求的并发安全,国内较常使用 Redis 或者 ZooKeeper。和悲观锁类似,每个请求需要先从组件中获取分布式锁之后才可以继续执行。流程如下图:

  1. 红色请求先获取分布式锁,查询和更新库存,在此期间蓝色请求等待获取分布式锁
  2. 红色请求更新库存后释放分布式锁,返回下单成功
  3. 蓝色请求获取分布式锁,查询库存,发现库存为 0,释放分布式锁,返回下单失败

分布式锁的优点是将功能进行分离,分布式组件负责解决并发安全的问题,数据库负责数据存储。

不过缺点在于:

  1. 分布式锁的正确实现并不简单,错误的实现方式容易引起其他一致性的问题。

  2. 分布式锁在高并发下也会产生锁竞争的问题,性能不佳。

  3. 由于引入了新的组件,要考虑分布式组件的可靠性,以及崩溃之后的恢复机制。

消息队列

另一个直观的解决方法就是使用消息队列,确保每个商品每个时刻只有一个请求,流程如下图:

  1. 红色请求进入队列,蓝色请求进入队列,数据库订阅下单请求
  2. 数据库处理蓝色请求,红色请求查询和更新库存,返回下单成功
  3. 数据库处理蓝色请求,查询库存,发现库存为 0,返回下单失败

消息队列的优点对业务进行了解耦,除了数据库之外,其他对下单请求感兴趣的业务系统,例如数据分析,日志记录等都可以订阅下单请求的消息。缺点在于 1)因为消息队列可能会崩溃,消息发送也可能失败,所以要考虑消息只消费一次,不会因为重复消费导致重复下单。2)由于引入了新的组件,要考虑消息队列的可靠性,以及崩溃之后的恢复机制。

消息队列:架构2

对比两个方案的优缺点之后,队列服务更适合我们的电商系统,架构升级后,最终 架构 2 如下:

  1. 客户端发送下单请求给服务端
  2. 服务端将请求发送到消息队列
  3. 数据库每次从消息队列取出请求
    • 若该商品库存大于零,将库存减一
    • 若该商品库存等于零的话,不做操作
  4. 服务端根据消息队列里的消息状态返回下单结果

从电商系统到秒杀系统

秒杀系统和电商系统有两个核心区别:

  1. 双十一也有极大的流量,但是双十一的商品种类很多,所以流量会分布到不同的商品中。而秒杀系统中,商品的种类和库存都比较少,导致大部分流量集中在少量商品中。
  2. 秒杀系统由于商品稀缺,价值高。同一位客户可能会对同一商品多次提交下单请求,而且恶意刷单的请求比较多,所以系统接收到的无效请求及非法请求较多。

针对这两个区别,我们发现架构 2 有 3 个潜在问题:

  1. 当一款商品库存只有 10 件却有 1 万名用户下单的时候,只有前 10 名客户可以下单成功,其他用户都浪费时间在队列等待以及无意义地查询库存,既牺牲了用户体验也增加了消息队列以及数据库的压力。
  2. 由于库存过少,有大量的请求(例如非法用户的请求,超过秒杀活动开始一定时间的请求)其实是没有机会抢到商品的,所以没有必要到达服务器,更不用说数据库了。
  3. 大量的客户端在下单前同时请求同一个商品的秒杀页面,导致服务器压力骤升。

针对这三个问题我们可以考虑两个方案:流量控制和资源隔离

流量限制

第三个问题相对简单,可以将秒杀页面使用 CDN 缓存起来,客户端就可以直接从 CDN 获取到秒杀页面(那个静态页面),不需要重复请求服务器。另外两个问题可以通过流量限制来解决,可以通过限流器,负载均衡以及安全验证组件实现:

  • 限流器分为前端限流与后端限流:

    • 前端限流包括验证答题,防止重复点击按钮等常见机制。

    • 后端限流使用限流算法进行流量限制,简单情况下可以使用固定限流算法,例如秒杀商品的库存是 10 件,只要限流器接收到 10 * k(k 可以根据业务进行调整)个请求之后,就停止接受该商品的所有请求。这样无论有多少个下单请求,最终到达服务器的单个商品请求数量都不超过 10 * k。实际工程中,因为有客户可能会出现支付超时导致释放库存的情况,系统需要通知限流器接受新的请求。

  • 负载均衡负责将下单请求通过负载均衡算法转发到最合适的服务器。

  • 安全验证组件分为前端安全验证以及后端安全验证:

    • 后端安全验证包括黑名单校验,IP 地址校验等机制。

    • 前端安全验证包括:客户端账户验证(确保客户有资格参考秒杀活动),客户端版本安全验证(防止反编译以及修改客户端代码),秒杀接口动态生成(防止使用刷单脚本)等机制。

这时候系统的整体架构如下:

热门资源隔离

既然大部分流量集中在少量商品中,我们能不能针对这些商品进行特殊处理呢?这样既可以防止秒杀活动影响其他业务功能,也可以针对热门商品进行资源分配,答案是可以的,首先我们需要识别出热门商品,这里有两种常见的方法:

  • 静态识别:包括京东在内的一些电商平台,客户在参加秒杀活动之前需要先进行预约,只有预约过的客户才能参考秒杀活动。这样系统可以提早识别热门商品以及进行流量预估。
  • 动态识别:通过实时数据分析系统在秒杀活动前统计出现在较多客户浏览的热门商品,针对预估结果进行资源分配。

识别出热门商品之后,我们可以将热门商品的资源进行隔离,并且设置独立的策略,例如

  • 使用特殊的限流器,由于秒杀系统的库存很少,在下单请求开始阶段就可以随机丢弃大部分请求。
  • 使用单独的数据库,在架构 2 中,下单请求的处理速度受限于消费者的处理速度,也就是数据库的处理速度。我们可以对热门商品进行分库分表,这样可以将请求处理的压力分摊到多个数据库中。下图中,我们将秒杀系统的一些组件独立开来:

最终:架构3

根据以上两个方案,我们可以设计出最后的架构 3:

  1. 客户端从 CDN 获取到秒杀页面
  2. 客户端发送下单请求给网关
  3. 在网关或者服务器前进行流量控制以及负载均衡等策略
  4. 服务端将请求发送到消息队列
  5. 数据库每次从消息队列取出请求
    • 若该商品库存大于零,将库存减一
    • 若该商品库存等于零的话,不做操作
  6. 服务端根据消息队列里的消息状态返回下单结果

总结

1. 秒杀系统特点

秒杀系统的特点是大流量以及流量倾斜,大量流量会集中在少量的几种商品中。

2. 秒杀系统问题

秒杀系统需要保证:

  • 高可用:服务器不因为大流量而崩溃,同时秒杀业务不影响其他业务。
  • 高扩展,架构适合水平扩展,在特殊活动前能够迅速扩容。
  • 一致性:商品不出现超卖和少卖的问题。”

3. 秒杀系统主要方案

要保证上述三个性质,主要方案有三个:

  • 合理使用消息队列,既可以解决并发安全问题,也可以进行业务解耦,方便水平扩展。
  • 前后端的流量限制,将大部分的无效流量拦在服务器之前。
  • 热门资源隔离,针对热门商品进行独立处理以及资源分配。”

4. 其他问题

“应该在什么时候扣除库存,是下单后扣除库存还是支付后扣除库存呢?为什么?”

应该在下单的时候扣除库存,如果在支付成功再扣除库存的话会出现下单请求成功数量大于库存的情况

“对秒杀商品进行分库分表之后可能导致某个分表库存为零,但其他分表还有库存,如何解决这个问题?”

“有三种解决方案:

  • 如果当前分表没有库存的话,到其他分表进行重试,缺点是会放大流量。
  • 通过路由组件记录每个分表的库存情况,将下单请求转发到有库存的分表中。
  • 使用分布式缓存记录每个分表的库存情况,并且每次下单请求只更新缓存,缓存后续再更新到数据库中,缺点是可能出现缓存和数据库不一致的问题。”

“客户下单后可能支付超时并释放库存,这时候有哪些要注意的?”

“服务器能够通知限流器以及前端库存发生变化,限流器能够重新接收请求,前端页面显示可下单的页面,确保后续的用户能继续购买商品。”

“消息队列方案有什么潜在问题吗?”

“秒杀系统下,可能 80% 的流量都指向同一个热门商品,那么消息队列中的分区会特别大,影响了两个方面 1)消息队列本身的稳定性,吞吐量会受单个分区限制,也可能影响其他业务。2)下单请求受到消费者消费能力的限制,即使消息队列每秒可以处理大量消息,但是数据库每秒处理的数量有限。可以使用以下几种方案:

  • 压力测试:在前期压力测试的时候,模拟流量极端分布的情况,确保现有架构能够支持服务。
  • 资源隔离:对秒杀商品使用独立的消息队列,使用特殊的流量限流策略,配置更好的资源。
  • 合并下单请求:将多个下单请求合并成一个请求,再交给数据库处理。不过在实际工程中,下单业务可能比较复杂,不只包含扣减库存。所以合并逻辑会影响后续业务的可扩展性。
  • 合并事务:将多个事务合并成一个事务执行,这样能有效减少数据库压力,缺点是逻辑会比较复杂,而且一个事务执行失败会影响多个订单。

“消息队列怎么保证消息有且仅生效一次(Exactly Once)?”

  • 为了保证最少一次生效, 消费者需要下单成功后才能返回确认 ACK,否则有可能会丢失消息。
  • 为了防止消息重复消费的问题,需要使下单逻辑变为幂等操作,常见的解决方案是保证下单请求有全局唯一的 ID,并在消息队列中对 ID 进行持久化,在发送给消费者之前先检查 ID 是否已经消费过。要注意中间层的重试机制不要修改这个全局唯一的 ID,不然会导致消息队列误以为该消息没有消费过。

“消息队列如何保证消息有序/分布式事务一致性/高可用?”

请参考国内外云平台文档的使用场景以及最佳实践:https://cloud.tencent.com/product/tdmq

“如何正确地实现分布式锁?”

了解 SETNX 的局限性以及 RedLock 的基本原理,具体请参考 https://redis.io/topics/distlock

“分布式锁和数据库悲观锁相比有什么优势?有什么共同的缺点?”

  • 优点:加锁的操作不依赖数据库,降低数据库资源冲突的概率和压力。
  • 共同缺点:可扩展性差,对于单个商品都是串行操作,假如每个订单执行要 100ms,每秒只能执行 10 个对应的订单,可能会出现大量请求阻塞的情况。

“如何保证缓存和数据库的一致性?”

请参考:https://www.pixelstech.net/article/1562504974-Consistency-between-Redis-Cache-and-SQL-Database

“如果电商系统流量过大,如何进行降级服务?”

  • 暂停非核心业务:例如淘宝在双十一会暂时关闭退款功能。
  • 拒绝服务:当系统压力到达一个阈值的的时候,随机丢弃部分秒杀请求。
  • 减少重试:将重试次数降低甚至设置为0,否则容易造成雪崩效应,系统陷入负反馈循环,无法正常恢复。

“怎么测试你的方案,使用最小的资源实现一个稳定的秒杀系统?”

需要分析系统可能出现的瓶颈,并提出优化手段。

“上面的方案有哪些是需要人工运营的,有没有办法将它自动化?”

可以从自己熟悉的领域回答,例如分库分表,自动扩容,自动化测试等方向。

“你的方案还有哪些可以优化的地方?”

首先需要了解不同方案的优缺点,例如乐观锁与悲观锁的优缺点,锁机制与消息队列的优缺点。然后根据不同的基础架构,流量分布以及业务读写比例调整方案。

第二十篇商城系统-秒杀功能设计与实现(代码片段)

秒杀服务一、商品上架秒杀活动的结构图通过定时任务触发:/***定时上架秒杀商品信息*/@Slf4j@ComponentpublicclassSeckillSkuSchedule@AutowiredSeckillServiceseckillService;@AutowiredRedissonClientredissonClient;/****/@ 查看详情

第二十篇商城系统-秒杀功能设计与实现(代码片段)

秒杀服务一、商品上架秒杀活动的结构图通过定时任务触发:/***定时上架秒杀商品信息*/@Slf4j@ComponentpublicclassSeckillSkuSchedule@AutowiredSeckillServiceseckillService;@AutowiredRedissonClientredissonClient;/****/@ 查看详情

面试实战考核:设计一个高并发下的下单功能(代码片段)

功能需求:设计一个秒杀系统初始方案商品表设计:热销商品提供给用户秒杀,有初始库存。@EntitypublicclassSecKillGoodsimplementsSerializable@IdprivateStringid;/***剩余库存*/privateIntegerremainNum;/***秒杀商品名称*/privateStringgoodsName;秒杀订单表... 查看详情

竞拍系统设计(秒杀系统知识迁移)(代码片段)

自从上次整理了秒杀系统的文章(php+golang商品秒杀)后,知识迁移一新项目,商品竞拍。技术:php、mysql、redis、laravel业务对象:商品、场次、订单竞拍过程:一、实现商品、竞拍场次和订单的CRUD;二、定时将秒杀场次、商品、... 查看详情

电商 秒杀系统 设计思路和实现方法(代码片段)

电商 秒杀系统 设计思路和实现方法2017年05月26日00:06:35阅读数:36621秒杀业务分析正常电子商务流程(1)查询商品;(2)创建订单;(3)扣减库存;(4)更新订单;(5)付款;(6)卖家发货秒杀业务的特性(1)低廉价格... 查看详情

一个秒杀系统的设计思考,超详细!(代码片段)

前言秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是12306抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性... 查看详情

如何设计一个秒杀系统(代码片段)

什么是秒杀秒杀场景一般会在电商网站举行一些活动或者节假日在12306网站上抢票时遇到。对于电商网站中一些稀缺或者特价商品,电商网站一般会在约定时间点对其进行限量销售,因为这些商品的特殊性,会吸引大... 查看详情

秒杀系统设计(代码片段)

高并发下如何设计秒杀系统?这是一个高频面试题。这个问题看似简单,但是里面的水很深,它考查的是高并发场景下,从前端到后端多方面的知识。秒杀一般出现在商城的促销活动中,指定了一定数量(... 查看详情

十万级低成本超详细的秒杀高并发设计,快收藏起来(代码片段)

秒杀系统相信很多人见过,比如京东或者淘宝的秒杀,小米手机的秒杀,那么秒杀系统的后台是如何实现的呢?我们如何设计一个秒杀系统呢?对于秒杀系统应该考虑哪些问题?如何设计出健壮的秒杀系统... 查看详情

秒杀微服务实现抢购代金券功能(代码片段)

文章目录需求分析秒杀场景的解决方案数据库表设计代金券表抢购活动表订单表创建秒杀服务pom依赖配置文件关系型数据库实现代金券秒杀相关实体引入抢购代金券活动信息代金券订单信息Rest配置类全局异常处理添加代金券秒... 查看详情

八个维度讲解秒杀系统架构分析与实战(代码片段)

..."书",获取后台回复“k8s”,可领取k8s资料1秒杀业务分析2秒杀技术挑战3秒杀架构原则4秒杀架构设计4.1前端层设计4.2站点层设计4.4数据库设计5大并发带来的挑战5.1请求接口的合理设计5.2高并发的挑战:一定要“... 查看详情

优惠卷秒杀系统设计秒杀优化——基于阻塞队实现异步秒杀优化及基于lua脚本判断秒杀库存一人一单(代码片段)

(目录)秒杀优化1、秒杀优化-异步秒杀思路回顾一下下单流程:分成如下几个步骤:在这六步操作中,又有很多操作是要去操作数据库的,而且还是一个线程串行执行,这样就会导致我们的程序执行的很慢,所以我们需要异步程... 查看详情

不是吧,阿sir,你竟然三分钟就解释了高性能秒杀系统的设计思考(代码片段)

前言秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是12306抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性... 查看详情

解密秒杀系统架构,不是所有的系统都能做秒杀!(代码片段)

摘要:教你如何设计一个秒杀系统架构:从电商系统架构到秒杀系统、从高并发“黑科技”与致胜奇招到服务器硬件优化,全方位立体掌握秒杀系统架构!!本文分享自华为云社区《实践出真知:全网最强... 查看详情

秒杀系统架构分析与实战(代码片段)

1秒杀业务分析2秒杀技术挑战3秒杀架构原则4秒杀架构设计5大并发带来的挑战6作弊的手段:进攻与防守7高并发下的数据安全8总结 1秒杀业务分析正常电子商务流程(1)查询商品;(2)创建订单;࿰... 查看详情

秒杀系统设计优化(代码片段)

...相同的库存,读写冲突,锁非常严重;小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万;这是秒杀业务难的地方。那我们怎么优化秒杀系统呢?一、难点(1)高并发用户在秒杀开始前,通过不... 查看详情

秒杀系统的设计与实现(限时抢购抢救接口单用户限制实现)(代码片段)

...设计的系统还有一些问题:我们应该在一定的时间内执行秒杀处理,不能再任意时间都接受秒杀请求。如何加入时间验证?对于稍微懂点电脑的,又会动歪脑筋的人来说开始通过抓包方式获取我们的接口地址。然后通过脚本进行... 查看详情

redis轻松实现秒杀系统(代码片段)

...务流程比较简单,一般就是下订单减库存。秒杀架构设计理念限流:鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。削峰:对于秒杀系统瞬时会有大量用户涌入&#... 查看详情