八股文场景题
如何预估接口上线后的 QPS
问题引入
这个问题其实是一个非常实际的问题,因为我们在开发需求后,例如:新增了一个接口
有一个步骤是值得做的,那就是预估这个接口的QPS
因为我们是可以去调配对应服务器的数量和运行配置的
- 例如我可以从2个节点新增为4个节点
- 例如我可以将节点的内存从2G变成4G,2核CPU变成4核CPU
如果因为 没有正确的预估 以及 没有正确的调整,导致接口的QPS过高,扛不住系统崩溃,那就是严重的外网事故了
注意,这里说的预估QPS,是假设有一个真实的项目、真实的环境,预估这个接口上线后会有多少 QPS而不是在预估我们的系统接口本身能抗住多少QPS而不崩溃
如何预估
预估一个接口的 QPS,本质上是在考虑这个接口被调用的频繁程度
1.分析业务
- 业务类型: 不同的业务类型的QPS是不太一样的,再细化一些,接口的做的事情是不同的,QPS是不同的,例如这个接口是 获取商城首页数据(QPS估计会高)、删除好友(QPS估计会低)
- 活跃用户: 有多少活跃用户,活跃用户多(QPS估计会高),活跃用户少(QPS估计会低)
2.计算平均的QPS:根据活跃用户数和业务类型估算
- 假设有100w个活跃用户(数值由活跃用户决定)
- 假设经过历史数据的统计,每个用户平均每天产生10次请求(数值由业务类型决定)
- 则可估算出平均每秒请求量(QPS)为 (10 * 100w)/ 86400= 116
3.计算峰值的QPS:假设业务类型是有峰值情况(特殊事件带来的流量波动)的,即我们的接口可能会达到更高的QPS
- 峰值情况通常是的平均值的数倍,假设峰值为平均值的3倍,则峰值QPS约为348
通过预估接口上限后可能的峰值QPS后,就可以相对应的调整服务器节点的数量、服务器的运行配置需要能抗住峰值QPS,记得给自己留点缓冲,即把值设置的略大一些
如何预估/回答接口本身能抗住多少 QPS
与哪些因素有关
后端服务器集群节点数量,数量越多,QPS越高
后端服务器节点的运行配置:运行内存、Cpu核数等等,硬件资源决定单节点处理能力
接口本身做的事情
- 做的事情多耗时长(预估QPS会相对应低)
- 做的事情少耗时短(预估QPS会相对应高)
系统架构:
- 完善的流量负载均衡架构,实现流量的有效分发和负载均衡,避免成为QPS瓶颈
- 缓存技术,减轻数据库的压力,避免数据库成为QPS瓶颈
- 集群模式的数据库,避免数据库成为QPS瓶颈
- 静态资源通过CDN加速,避免成为QPS瓶颈
如何预估?
首先需要知道一个请求处理完毕的时间(这个请求里可能做很多事情,但是这个我们暂时不管),一个请求处理完毕的时间我们是可以知道的。(我们去调用这个接口多次得到平均值即可)
理论计算公式
QPS(单节点) = 线程数(如 Tomcat 线程池)/ 平均请求处理时间(秒)
- 例如:若线程池大小为 200,单个请求处理时间为 50ms(0.05秒)
- SprigBoot 默认使用Tomcat,而Tomcat线程池的最大线程数就是200,所以在默认配置下,SprigBoot 应用可以并发处理 200 请求则 QPS(单节点) = 200 / 0.05= 4000
QPS(集群)= QPS(单节点)* 节点数
- 例如集群有4个节点,则 QPS(集群)= 4000 * 4 = 16000
单个请求处理的时间是受到 “系统架构”、“接口本身做的事情”“后端服务器节点运行配置”“后端服务器节点数量”等等因素共同影响的 但这实际上是一个近似已知值(我们去调用这个接口多次得到平均值即可)
QPS瓶颈
我们想想QPS的瓶颈在哪?其实很多方面,就像前面说的QPS与哪些因素有关但一般来说 QPS 最可能的瓶颈在于数据库 (例如MySQL)
应该说多少QPS
面试官如果问接口的QPS多少,如何回答比较合理?
首先我们一定可以知道这个接口处理的平均耗时是多久?假设是200ms
一些项目背景暂时假设为如下情况,因个人而定
- 假设有 n 台后端服务器
- 假设后端服务器的配置是 2核4G内存
- 假设接口做了一些事情
- 假设对于一些系统架构上,做了一些优化、或者没有做优化
那么
- QPS(单节点) = 线程数(如 Tomcat 线程池)/ 平均请求处理时间(秒)若线程池大小为 200,单个请求处理时间为 100ms(0.01秒)则 QPS(单节点) = 200 / 0.01= 2000
- QPS(集群)= QPS(单节点)* 节点数 = 2000 * n
没有压测的情况下,我们就可以说这个理论计算得出的值。但是理论仅仅是理论如果有压测,那其实是另外更好的一种情况,因为压测得到的数据往往是更加准确的。例如压测过程:使用 JMeter 进行了压力测试,逐步增加并发线程数,直到接口的响应时间超过预设阈值(如 200ms)或错误率超过 1%。此时就可以知道接口可以抗住的QPS
示例:
面试官:“你在项目中负责的订单查询接口,QPS 是多少?”
你的回答: “订单查询接口的 QPS 在‘双11’大促期间峰值达到 5,000,日常平均 QPS 约为 1,200。我们通过以下步骤得出这一数据:
- 压测阶段:JMeter 进行了压力测试,逐步增加并发线程数,发现当 QPS 达到 2,000 时,响应时间达到300ms。
- 瓶颈分析:通过 Arthas 追踪发现,80% 的请求耗时在数据库查询上。
- 优化措施(仅为举例,具体情况依据个人而定):引入 Redis 缓存热点商品数据,缓存命中率提升至 90%;对订单表按用户 ID 进行分库分表,单表数据量从 1 亿减少到 1 千万;将日志记录改为异步写入 Kafka,减少主线程耗时。
- 优化结果:最终压测 QPS 提升到 5,000,且响应时间稳定在 80ms 以内。 此外,线上监控显示,实际高峰期的 QPS 与压测结果基本一致,系统未出现超时或宕机。”
如何设计一个秒杀系统
秒杀系统场景特点
- 秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功
- 秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增
- 秒杀业务流程比较简单,一般就是下订单减库存
秒杀架构设计理念
- 限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进⼊服务后端秒杀程序。
- 削峰:对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰
的常用的方法有前端添加一定难度的验证码,后端利用缓存和消息中间件等技术。 - 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。
- 内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
- 可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。
设计思路
- 将请求拦截在系统上游,降低下游压力:秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时
- 充分利用缓存:利⽤缓存预减库存,拦截掉大部分请求
- 消息队列:这是⼀个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理
前端方案
- 页面静态化:将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
- 禁止重复提交:用户提交之后按钮置灰,禁止重复提交
- 用户限流:在某一时间段内只允许用户提交一次请求,比如可以采取IP限流
后端方案
服务端控制器层(网关层)
限制uid(UserID)访问频率:我们上面拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同⼀个访问uid,限制访问频率。
服务层
上面只拦截了一部分访问请求,当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。比如我们有100W用户同时抢100台手机,服务层并发请求压力至少为100W。
- 把需要秒杀的商品的主要信息以及库存初始化到redis缓存中
- 做请求合法性的校验(比如是否登录),如果请求非法,直接给前端返回错误码,进行相应的提示
- 进行内存标识的判断 (true 已经秒杀结束,false 未秒杀结束,下面第4步会写入),如果内存标识为true,直接返回秒杀结束
- redis中使用 decr 进行预减库存操作,判断:如果decr后库存量小于0,则把内存标记置为true (已经秒杀结束,第3步会用到),且返回秒杀结束
- 用redis的布隆过滤器来判断是否已经秒杀到了(下面第7步会写入),防止重复秒杀,如果重复秒杀,直接返回重复秒杀的错误码。具体做法是:先用redis的布隆过滤器来判断是否秒杀过,如果布隆过滤器判断已经秒杀过了, 则再次查库确认是否秒杀过了,之所以再次查库确认是因为布隆过滤器对可能存在的数据是有误判率的;但是它对不存在的数据的判断是百分百准确的,所以如果redis的布隆过滤器判断没秒杀过,就直接放过去进行秒杀
- 发送成功秒杀到的MQ消息给相应的业务端进行处理,并给用户端返回排队中,如果客户端收到排队中的消息,则自动进行轮询查询,直到返回秒杀成功或者秒杀失败为止
- 相应的业务端进行处理:真正处理秒杀的业务端,再次进行校验(比如秒杀是否结束,库存是否充足等)、将用户和商品id作为key存入redis的布隆过滤器来标识该用户秒杀该商品成功(第5步会用到)、减库存(这里的是真正的减库存,操作数据库的库存)、生成秒杀订单、返回秒杀成功
注意:就算请求走到了真正处理业务的这一端,也有可能秒杀失败,比如秒杀结束,库存不足,真正减库存失败,秒杀单生成失败等等,一旦失败,则返回秒杀结束
优化:将秒杀接口隐藏:用户点击秒杀按钮的时候,根据用户id生成唯一的加密串存入缓存并返回给客户端,然后客户端再次请求的时候带着加密串过来,后端进行校验是否合法,若不合法,直接返回请求非法
限制某个接口的访问频率:可以用拦截器配合自定义注解来实现,这么做可以和具体的业务分离减少⼊侵,使用起来也非常方便
数据库层
- 数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧
- 为防止秒杀出现负数订单数大于真正的库存数,所以在真正减库存,update 库存的时候应该加上 where 库存 > 0,而且需要给秒杀订单表加上用户id和商品id联合的唯一索引
10亿订单表如何做分库分表?
场景痛点:某电商平台的MySQL订单表达到7亿行时,出现致命问题:
-- 简单查询竟需12秒!
SELECT * FROM orders WHERE user_id=10086 LIMIT 10;
-- 统计全表耗时278秒
SELECT COUNT(*) FROM orders;
核心矛盾:
-
B+树索引深度达到5层,磁盘IO暴增。
-
单表超200GB导致备份时间窗突破6小时。
-
写并发量达8000QPS,主从延迟高达15分钟。
分库分表核心策略
垂直拆分:先给数据做减法
• 按列拆分:将一张宽表的字段按业务属性分散到不同的表中
优化效果: - 核心表体积减少60% - 高频查询字段集中提升缓存命中率
水平拆分:终极解决方案
按行拆分:将表数据按某种规则(如ID范围、哈希值)分散到多个结构相同的表中。
分片键选择三原则**:
1.离散性:避免数据热点(如user_id优于status)
2.业务相关性:80%查询需携带该字段
3.稳定性:值不随业务变更(避免使用手机号)
基因嵌入:将用户的后几位id编号(gene)嵌入到订单ID的生成 ==>比较有名的就是淘宝19位订单编号,后六位是用户ID的后六位 然后再根据订单编号的用户ID进行分库分表==>分8个库 每个库16张表 注意这种建立起来的库是用户库
关键突破**:通过基因嵌入,使相同用户的订单始终落在同一分片,同时支持通过订单ID直接定位分片
**跨分片查询**
三个高频场景对应三种查询方式 :(缺少订商家发货)
- 核心矛盾:商家与订单是多对多关系,一个商家的订单会分散在所有分片中。
- 方案:Elasticsearch二级索引
- 思想:创建一个辅助索引,也就是附录==>通俗来讲,就好比字典的拼音查询(主查询)以及偏旁笔画查询(二级查询)
实施步骤:
将订单数据同步到ES:
{"mappings": {"properties": {"merchant_id": { "type": "keyword" },"order_id": { "type": "keyword" },"user_gene": { "type": "integer" }}}
}
查询实现:
SearchResponse response = client.prepareSearch("orders").setQuery(QueryBuilders.termQuery("merchant_id", merchantId)).addSort("create_time", SortOrder.DESC).setSize(100).get();
数据迁移
双写迁移方案:
灰度切换步骤:
- 开启双写(新库写失败需回滚旧库)
- 全量迁移历史数据(采用分页批处理)
- 增量数据实时校验(校验不一致自动修复)
- 按用户ID灰度流量切换(从1%到100%)
性能指标:
场景 | 拆分前 | 拆分后 |
---|---|---|
用户订单查询 | 3200ms | 68ms |
商家订单导出 | 超时失败 | 8s完成 |
全表统计 | 不可用 | 1.2s(近似) |
总结
- 分片键选择大于努力:基因分片是订单系统的最佳拍档。
- 扩容预留空间:建议初始设计支持2年数据增长。
- 避免过度设计:小表关联查询远比分布式Join高。效
- 监控驱动优化:重点关注分片倾斜率>15%的库。
如何实现A网站登录后,B网站自动登录
有这么个场景,公司下有多个不同域名的站点,我们期望用户在任意一个站点下登录后,在打开另外几个站点时,也是已经登录的状态,这么一过程就是单点登录。因为多个站点都是用的同一套用户体系,所以单点登录可以免去用户重复登录,让用户在站点切换的时候更加流畅,甚至是无感知。
题述问题,本质上是 单点登录 的问题
单点登录(Single Sign-On,简称 SSO) :是一种用户认证机制,允许用户在一次登录后,访问多个系统或应用程序而无需再次登录
基于Cookie
适用情况: A网站和B网站必须是同一域名下的两个网站,如 a.example.com 和 b.example.com
可以通过共享 Cookie 的方式来实现 SSO
- 优点是实现起来简单,浏览器天然就支持
- 缺点是限制必须不能跨域,如果是两个不相关的网站,就不能通过Cookie实现SSO
基于Token
使用标准化的令牌(如 JWT)来携带用户认证信息。用户登录后,服务端颁发一个令牌(Token)给到前端,前端在访问其他的网站的时候,把这个 Token 放到 HTTP Header 中带过去,然后就可以基于这个Tokne来验证用户身份了
这个方案可以解决跨域的问题,只要支持HTTP的协议即可。但是缺点是Token可能会被泄漏。存在一定的安全隐患
基于共享Session
用户登录后,系统将用户信息存储到公共的第三方存储,如Redis或数据库
例如使用Redis,那么所有业务系统都必须知道该公共Redis的实例信息,才能够访问获取用户信息
通过CAS
CAS 是 Central Auth Service,也就是认证中心服务
注意和并发编程中的 Compare And Swap 区分
多个需要实现单点登录的系统,都接入这个统一认证中心,所有的登录、认证都通过这个认证中心来实现
- 用户访问子系统时重定向到 CAS 服务器登录
- CAS 服务器验证用户身份并返回一个票据(Ticket)
- 子系统使用票据向 CAS 验证,获取用户信息
这个方案一般适合于企业内部做集成,可以让业务系统不再关心登录、授权,而是只关注业务逻辑,只需要做一次接入就行了。比如说公司的很多内部平台之间,就可能是用了同一个登录服务。
优点就是接入成本低,缺点就是这个认证中心本身的开发成本还是挺高的。