秒杀面经

84 阅读11分钟

秒杀简介

同一时刻有大量的客户端请求购买同一个商品、并完成交易的过程,瞬时产生大量的并发读、并发写。

本质上是一个满足高并发、高性能、高可用的分布式系统。

特点:

  1. 高性能:大量并发读、并发写
  2. 一致性:通常会使用缓存,需要保证缓存和数据库中库存数据的一致性,保证商品库存的准确性
  3. 高可用:考虑系统容灾
  4. 可扩展性:服务到达瓶颈时,需要支持快速扩容

image-20240317015550412

基本策略

架构设计原则

  1. 数据尽量少(前后端交互数据尽量少)
  2. 前端请求数量尽量少
  3. 路径尽量短(用户发出请求到返回数据中间经过的节点数)
  4. 依赖尽量少(用户下单过程中强依赖的服务)
  5. 不要有单点服务

前端优化策略

  1. 页面静态化:数据源动静分离,静态数据(详情图片、文案等)可以放到CDN,动态数据(价格、数量)放到服务器。可以通过url地址作为key来存储静态数据。
  2. 控制对服务端请求的频率:限流方式:a.秒杀按钮单位时间内只能点击一次;b.答题正确后才能向服务器发起请求;c.验证码
  3. 控制传参数量,减少交互的数据、降低编解码开销

流量控制手段

  1. 账户验证(确保有资格
  2. 客户端版本安全校验(防止反编译、修改客户端代码等等)
  3. 秒杀接口动态生成(防止刷单

常见高可用策略:

  1. 容灾设计(异地多活)
  2. 降级:容量达到一定程度时,限制或关闭系统的某些非核心功能,把资源留给核心业务。需要前后端配合执行。例如:QPS达到某一个阈值时,将原有分页50个数据量改成10个
  3. 限流:容量达到瓶颈时,限制一部分流量来保护系统,可以时接口级别、服务级别、ip级别。阈值通过压测结果设置
  4. 拒绝服务:兜底

限流手段

  1. 可以采用固定限流算法:例如秒杀商品库存10件,限流器接收到10n个请求后就停止该商品的所有请求,这样最终到达服务端的单个商品请求数量都不会超过10n。可能存在客户支付超时、释放库存的情况,系统需要能通知限流器接收新的请求。
  2. 安全验证:黑名单校验、IP地址校验
  3. 单独的限流器:下单阶段随机舍弃秒杀请求

资源隔离

发现热点数据

静态热点数据:商家报名商品、用户预约商品

动态热点数据

  1. 异步系统收集交易链路上的热点key,最好采用异步采集日志的形式
  2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,通过交易链路上各个系统(包括详情、购物车、交易、优惠等)访问的时间差,把上游已经发现的热点透传给下游
  3. 把上游系统收集的热点数据发送到热点服务台,下游系统订阅热点数据,尽量做到实时,为下游提供保护

处理热点数据

独立部署秒杀服务:秒杀持续时间短,并发量高。为了影响原有业务,可以单独部署一套秒杀服务,进行物理级别隔离

优化热点数据:进行缓存,使用LRU淘汰算法替换

限制热点数据:堆被访问的商品ID做一致性Hash,根据Hash做分桶,每个分桶设置一个处理队列、把热点商品限制在一个请求队列里,不会占用过多服务器资源

隔离热点数据

  1. 业务隔离:把秒杀作为营销活动,卖家需要单独报名
  2. 系统隔离:让请求落到不同的集群中,运行时隔离
  3. 数据隔离:单独的数据库存放热点数据,热门商品分库分表,压力分摊到多个数据库中

可能会出现某个分表库存为0,其他表有库存的情况。1.到其他表重试,会放大流量;2.通过路由组件记录每个分表的库存情况,把请求转发到有库存的表中;3.分布式缓存记录每个分表的库存情况,每次下单时只更新缓存,缓存后续再更新到数据库中

流量削峰

服务流量过大时,可以将请求存入MQ处理,客户端采用轮询方式获取结果

排队:使用消息队列缓冲瞬时流量

答题作用:a. 增加购买复杂度,防止作弊;b.延缓请求

image-20240318230841754

  1. 题库生成模块,生成问题和答案,防止机器计算即可
  2. 题库推送模块,在秒杀答题前,把题目提前推送给详情系统和交易系统。推送机制主要是为了保证每次用户请求的题目唯一,防止答题作弊
  3. 题目的图片生成模块,把题目生成为图片格式、图片中增加干扰因素,防止机器答题,提前把题目的图片推送到CDN进行预热
  • 问题key:userId+itemId+question_Id+time+PK
  • 答案key:userId+itemId+answer+PK
  • 提交答案也需要限制,过短时间可能是机器

分层过滤

image-20240318231329311

对请求进行分层过滤,过滤无效请求

  1. 大部分数据和流量在用户浏览器或CDN上获取,拦截大部分数据的读取
  2. 一些读取请求尽量走缓存
  3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求
  4. 后台写系统做数据的二次校验,写请求需要做限流保护
  5. 最后在数据层完成数据的强一致性校验

分层校验的目的是:

  • 在读系统中,尽量减少由于一致性校验带来的系统瓶颈,尽量将不影响性能的检查条件提前,如用户是否具有秒杀资格、商品状态是否正常、用户答题是否正确、秒杀是否已经结束、是否非法请求、营销等价物是否充足等;
  • 在写数据系统中,主要对写的数据(如“库存”)做一致性检查,最后在数据库层保证数据的最终准确性(如“库存”不能减为负数)。

减库存设计

用户实际购买流程:下单-->付款。

库存扣减方式

  1. 下单减库存:买家下单后,扣减商品总库存。下单时通过数据库的事务机制控制商品库存。潜在问题:恶意刷单、下单后占用库存不付款
  2. 付款减库存:买家付款后、扣减商品总库存。潜在问题:库存超卖
  3. 预扣库存:下单后、预扣库存,一定时间内没有付款,库存自动释放。买家付款前需要再次检验库存是否保留、如果没有保留、再次尝试预扣;如果库存不足则不允许继续付款;如果预扣成功,则完成付款并实际减去库存。

预扣库存还是存在一些问题:恶意刷单等等。这需要结合反作弊策略,标识经常下单不付款的买家、限制重复下单不付款的操作次数等等

大型秒杀活动减库存方式:秒杀场景下,卖家对商品库存有严格限制,采用“下单减库存”更合理,性能也更优,为了保证库存数据不能为负数:a.事务判断,不满足则回滚;b.库存字段为无符号整数,异常操作时会报错;

服务端库存处理

将库存操作的逻辑放在lua脚本中,通过redis的单线程特性,保证脚本执行不会被打断,从而保证库存操作的原子性。

秒杀减库存的极致优化:

  1. 如果秒杀商品减库存逻辑简单,联动关系少,可以在缓存中减少数据,然后同步到数据库
  2. 热点数据分库分表,按照商品维度区分,防止热点商品占用太多数据库连接

涉及技术

数据存储方式

缓存

秒杀场景读多写少,可以在服务端增加缓存应对高并发读。缓存可以设置2层:1.本地缓存,失效时间秒级别,属于jvm级别,每次失效后可以从redis缓存中加载;redis缓存要注意缓存失效、缓存击穿、缓存雪崩问题。

  • 缓存击穿:缓存中不存在、数据库中存在。解决方式:1.缓存永不过期;2.同步返回null,异步加锁查询数据库、更新缓存
  • 缓存穿透:数据在缓存和数据库中都不存在。解决方法:1.业务层进行合法校验、拦截大部分不合法的请求;2.布隆过滤器;3.对空的结果进行缓存、设置较短的过期时间,数据库有变更时,同步刷新缓存
  • 缓存雪崩:缓存同时失效。解决方法:1.永不过期;2.失效时间随机;3.多级缓存,A->B,A有过期时间,B没有过期时间,但是更新的时候要同时更新

可以将更新频繁的商品库存信息放在redis缓存处理,最好分成多分放入不同key缓存中,达到更好的读写性能

消息队列

悲观锁

查询数据库的时候获取排他锁,查询、更新库存,此时其他线程等待获取排他锁。前一个线程释放排他锁后,后面线程获取到锁,查询到库存为0.

缺点:

  1. 处理性能不高、多个用户同时下单同一个商品,每个请求都要获取排他锁,较长时间后才能知道是否下单成功;
  2. 容易设计死锁,实际上整个下单操作不仅涉及修改库存,还有其他业务,悲观锁每个请求轮流持有锁,更容易发生死锁

乐观锁

乐观锁策略下事务会记录查询时的版本号,事务准备更新时,如果发现版本号不一致,就会回滚事务。

乐观锁不需要等待锁,在事务竞争较少的情况下比悲观锁性能好,在事务竞争多的时候,经常需要回滚事务、性能反而更差

分布式锁

在服务端和数据库之间加上分布式组件保证请求的并发安全,例如redis, zookeeper。每个请求都需要从组件中获取分布式锁后才能继续执行。

优点:

  1. 功能分离,分布式组件负责解决高并发问题,数据库负责数据存储

缺点:

  1. 容易引发一致性问题
  2. 高并发下也会产生锁竞争问题
  3. 需要维护分布式组件的可靠性

消息队列

请求都进入消息队列,数据库每次从消息队列中取出请求。

服务端根据消息队列里的消息状态返回下单结果

为了保证消息最少一次生效,消费者需要下单成功后才能返回确认ACK,否则有可能会丢失消息

为了防止消息重复消费的情况,需要让下单逻辑变为幂等操作。常见解决方案:1.保证下单请求有全局唯一ID,并在消息队列中对ID进行持久化,在发送给消费者之前检查ID是否已经消费过,重试过程中不能修改。

潜在问题

  1. 库存少,请求多的时候,大部分用户都浪费时间在队列等待和无意义查询库存上,牺牲了用户体验、增加了服务端压力
  2. 库存过少,会有大量请求(非法请求,超过一定时间后的请求)都是没机会抢到的,没必要达到服务器
  3. 大量客户端在下单前同时请求同一个商品的秒杀页面,导致服务器压力升高

实际经验

后端服务器部署采用读写分离方式,库存计算采用lua脚本保证原子性,主库采用redis集群,写入redis成功后将数据转发到MQ,最终写入mysql书库,实现数据的最终一致性。

增加对账系统,校验redis和mysql数据的差异性。