GQ 场景题

如何设计一个秒杀功能?

1.秒杀系统要解决哪几个核心问题?

秒杀本质上要解决 5 类问题:

  • 瞬时高并发的流量
  • 热点数据(商品库存)访问集中
  • 库存扣减的准确性(不能超卖、不能少卖)
  • 防刷、防黄牛、防作弊
  • 保护下游系统、避免把整个系统冲垮

这是所有秒杀系统共同面临的本质问题。
明确完问题后,才能谈架构。

2.前端侧设计(流量提前拦截)

前端是非常关键的一层,因为越早过滤,越能保护后端。

我一般从三个方向处理:

1)CDN 静态资源分发
把活动页、商品页、脚本都提前丢到 CDN,减少对源站的压力。

2)前端随机丢弃/限流
前端可以做一些“轻度限流”,比如随机拒绝一部分请求,直接减少后端压力。

3)按钮防抖、避免频繁点击减少重复请求。

3.Nginx 接入层(第二段防线)

Nginx 是系统的第一道网关,可以做到:

  • IP 限流(限制同一 IP 在某时间窗口内的请求数)
  • 黑白名单过滤
  • 简单的防刷策略(User-Agent、Referer 校验)
  • 负载均衡,将请求分散到多节点

这一层会挡掉大部分恶意流量。

4. 应用服务层(核心:限流 + 缓存 + 削峰)

这里是秒杀系统的关键层,需要重点展开讲。

1)动态限流(Guava / Sentinel
根据实时 QPS 自动限流,避免雪崩。

2)本地缓存 + 分布式缓存
热点商品必须提前做“缓存预热”:

  • 本地缓存(Caffeine 等)速度最快
  • Redis 做分布式缓存
  • 双层缓存避免 Redis 热点 Key 打挂

3)削峰(异步排队)
用户请求先进入「秒杀队列」,后端异步消费,防止请求直接冲数据库。

典型做法:

  • Redis List
  • RocketMQ / Kafka 秒杀队列
  • 内存队列

4.库存扣减(最核心)

库存扣减的关键目标:

不超卖、不少卖、性能高
常见方案 3 选 1:

方案 A:Redis 原子扣减
使用 Decr + 库存预热,性能最好。
缺点:需要兜底保证与数据库最终一致。

方案 B:分段库存(热点拆分)
把一个热点库存拆成多个,分散压力。

方案 C:数据库乐观锁(不推荐)
性能不够,秒杀场景不适合。

6. 防刷、防黄牛(风控层)

一般会分两类说:

  1. 技术防刷
  • token 校验(生成一次性 token)
  • 图形验证码 / 点选验证码
  • 限制同 IP / 设备访问频率
  • 识别脚本请求(User-Agent、Cookie 行为)
  1. 模型风控(大厂经常用)
  • 基于 IP、设备、行为路径做分析
  • 风控规则触发 → 加入黑名单
  • Nginx / 应用端提前拦截

7. 避免重复下单 & 幂等性

防止用户疯狂点按钮下多单。

  1. token 防重(最常见)
    每次下单需要带同一个 token,用后即失效

  2. 限购策略 + 在途订单判断
    同一个用户一分钟内只能成功一次。

  3. 幂等性锁

  • Redis setnx
  • 数据库唯一索引
  • 消息队列幂等消费

8. 保护下游系统 / 降级与兜底

要考虑:一旦秒杀打挂了下游服务,会导致整个网站一起挂掉

所以:

  • 秒杀服务与主交易服务进行隔离(物理隔离最佳)
  • 所有调用必须有降级策略
  • Redis 扣减失败 → 兜底为“库存不足”
  • MQ 异常 → 存入失败队列

9. 业务侧的简化策略(加分项)

技术不一定把所有事都做掉,业务可以帮大忙。

常见的业务策略:

  • 预约制(先预约,才能参与秒杀)
  • 预售
  • 验证码
  • 强制限购(每人只能买一个)

小结

秒杀的核心是抗瞬时高并发、避免超卖、保护下游。我一般从三层做设计:
第一层:前端 + 网关限流,用 CDN 静态化、按钮防抖、Nginx IP/UA 限流,把 80% 的无效流量挡在入口。
第二层:应用层削峰,所有请求先进 Redis 或 MQ 排队,动态限流(Sentinel/Guava),缓存预热 + 本地缓存避免 Redis 打爆。
第三层:库存扣减,用 Redis 原子扣减保证不超卖,再异步写数据库做最终一致,订单侧加幂等、防重、限购。
另外再加上风控防刷(验证码、token、防脚本)、隔离下游 + 降级兜底,整体实现“快速失败、削峰填谷、限流保护、精确扣库存”,确保系统稳住不崩。

面试官:库存最终一致性怎么保证?

我秒杀这块是这样做的:Redis 负责高性能扣减,数据库是最终的准绳,通过消息保证两者最终对上。

  1. 正向流程
    • 请求进来先走 Redis 原子扣减库存,如果扣失败直接返回“抢完了”,这里保证不超卖。
    • 扣减成功后,在同一个本地事务里写订单、写一条“扣库存消息”到消息表 / 事务消息 MQ,事务提交再把消息发出去。
    • 下游消费者收到消息后,给 MySQL 做真实扣减,并把订单状态从“冻结/待支付”改成正常。
    • 消费端做 幂等(比如订单号唯一、扣减前先查是否处理过),保证消息重试不会多扣。
  2. 异常与补偿
    • 消息发不出去有 事务消息 / 本地消息表 + 定时补发
    • 消息消费失败有 重试 + 死信队列,人工或任务做重放。
    • 另外会有一个 对账/补偿任务,定时对比「Redis 库存 + 已扣订单」和数据库真实库存,发现不一致就回滚或补扣一次。

这样设计的结果是:
短时间内允许缓存和数据库有一点点“时间差”,但通过消息 + 幂等 + 补偿,保证最终库存数字是一致的,也不会超卖。

让你设计一个消息队列,怎么设计?

遵循着先搭骨架,再补细节的原则

1. 先把整体角色说清楚

我会先说明一个典型 MQ 里有哪些角色:

  • Producer:负责发消息
  • Broker / MQ 集群:负责存消息、路由消息、做高可用
  • Consumer:负责订阅 & 消费消息
  • Topic + 分区/队列:Topic 做业务分类,分区/队列做物理拆分和并行度提升

简单说:生产者发到某个 Topic 的某个分区,Broker 落盘,消费者按订阅拉/收这个分区的消息。

2. 说一下消息流转过程

按步骤讲一遍“从发送到消费”的链路:

  1. Producer 根据路由规则(Topic + 分区)把消息发给 Broker。
  2. Broker 先写 预写日志 / commit log,成功才回 ACK,保证“不 ACK 就不算发成功”。
  3. Consumer 通过拉(pull)或推(push 模式底层本质也是长轮询拉)从 Broker 拿消息,执行业务。
  4. 业务处理 OK 后回一个 消费确认,Broker 才会把这条标记为可删除或已消费。
  5. Consumer 自己维护 offset,实现「至少一次」,配合幂等可以做到「逻辑上只一次」。

3. 可靠性怎么设计

围绕“不丢、不乱、可恢复”展开:

  • 落盘 + 刷盘策略:消息先写磁盘顺序日志,可以选择同步刷盘(更安全)或异步刷盘(更快)。
  • 主从复制 / 多副本:Broker 做副本,主挂了可以选举新主;消费者只对外提供“已同步到多数副本”的消息。
  • ACK + 重试
    • 生产端:发送失败自动重试,必要时走幂等 key 防止插入多条。
    • 消费端:消费异常不确认,Broker 重新投递,超阈值进 死信队列,后面人工或后台任务重放。
  • 顺序消费:需要顺序时,同一业务 key(比如订单号)路由到同一个队列,消费端单线程 + FIFO 处理。

4. 性能和扩展性

然后说性能的手段,基本照着 Kafka 那一套讲:

  • 分区/多队列并行:一个 Topic 拆很多分区,多个 Broker 分摊,多个 Consumer Group 并发消费。
  • 顺序写 + 批量:消息顺序写磁盘 + 批量刷盘、批量发送,减少系统调用。
  • 零拷贝:发消息时用 sendfile 之类的技术,减少内核态/用户态拷贝。
  • 负载均衡 & rebalance:Consumer 数量变化时,自动 reblance 分区的归属,保证消费能力能随集群扩缩。

5. 功能层面的增强

再补几类常见“加分项”能力:

  • 两种消费模型
    • 队列/点对点:一条消息只被一个消费者处理(典型任务队列)。
    • 发布订阅:同一个 Topic 下多个订阅者都能收到一条消息(广播、通知)。
  • 延时消息 / 定时消息:消息先进一个特殊的延时队列,到点再投递到真实 Topic。
  • 事务消息:先发一条“半消息”,本地事务成功后再 commit 让它对消费者可见;如果不确定,由 Broker 发起事务回查。
  • 去重 & 幂等:给消息一个业务唯一 key(比如 orderId+type),消费端用 DB 唯一索引或 Redis set 去重。
  • 监控 & 运维:可视化看到堆积量、TPS、失败率;支持按 Topic/分区扩容、限流、降级。

6. 最后收个口:整体思路

用一句话收尾:

总结一下,我会把消息队列拆成 Producer / Broker / Consumer 三块,先把存储、协议、ACK 与 offset这条主链路设计好,再在其上叠加多副本、高可用、顺序/延时/事务消息、死信队列与监控,既保证消息不丢不乱,又能靠分区 + 批量 + 顺序写把吞吐拉上去。

这样一套说完,架构、可靠性、性能和高级特性都覆盖到了。

消息队列设计成推消息还是拉消息?推拉模式的优缺点?

消息队列有两种消费模式:推(Push) 和 拉(Pull)。

  • 推模式:Broker 主动把消息推给消费者。
  • 拉模式:消费者主动去 Broker 拉取消息。

RocketMQ 和 Kafka 其实都选择了“拉模式”,只是 RocketMQ 的“推”其实是封装了长轮询的拉模式也就是消费者在后台不断长轮询去拉消息,看起来像是 Broker 在推
所以本质上还是拉,只是体验像推。

优缺点对比一下

  • 推模式优点:消息能实时送达,延迟很低;

    • 缺点是如果消费者处理慢,Broker 一直推消息,容易把消费者压垮,尤其是高并发时不好做流控。
  • 拉模式优点:消费者自己掌握节奏,可以根据自身负载调节拉取速率,避免过载,还能做批量拉取;

    • 缺点就是实时性稍差,拉得太慢可能延迟会高一点。

为什么 RocketMQ 选拉模式

RocketMQ 和 Kafka 一样,选择拉模式主要是为了稳定可控
因为现在 MQ 都有“消息持久化”的需求——消息要先存好,再被消费。
所以用拉模式更安全,消费者按能力来拉,不会被推爆;再加上长轮询机制,也能兼顾实时性。

一句话总结:
推模式实时但容易压垮消费者,
拉模式可控但实时性略差。
RocketMQ 实际上是“伪推真拉”,底层用长轮询实现拉取,这样既能稳又能快。

让你设计一个短链系统,怎么设计?

一、先解释一下什么是短链系统

短链系统的本质,就是把一个长 URL 映射为一个短 URL
用户访问短链时,系统会根据短链字符串查到对应的长链,然后 301/302 重定向 去真正的地址。

二、系统整体架构

短链系统一般分三部分:

  • 前端:输入长链、展示生成的短链
  • 后端:生成短链、保存映射、跳转重定向、统计访问
  • 数据库:存储长链与短链的映射关系

这是一个典型的读多写少的系统

三、短链生成方案

例如短链总共只有 24 个字符,域名占掉大部分,实际只剩 6 位可用字符 给短链编码 。
因此需要一个高效高密度的编码方式,最常用的是 Base62(62 进制)编码

下发短链的步骤

  1. 插入一条长链记录(带 md5 去重)
  2. 拿到自增 ID(long 类型)
  3. Base62 算法把 ID 编码成 6 位字符(如 “ABCDEF”)
  4. 拼成短链 https://xxx.xxx/ABCDEF
  5. 返回给用户

为什么 6 位够用?
6 位 Base62 的最大值是 62^6 ≈ 568 亿,完全够用 。

四、短链跳转流程(访问时)

当用户访问短链:

  1. 根据短链字符串查询数据库
  2. 找到对应的长链
  3. 返回 HTTP 302 重定向到真实地址

可以加缓存(Redis)提升跳转速度。

五、数据库设计要点

  • id:自增主键(短链数字 source)
  • long_url:原始长链
  • long_url_md5:便于做长链去重(unique)
  • short_url_str:短链的 6 位字符(unique)

两列做唯一索引保证:

  • 相同长链不重复创建短链
  • 短链字符串绝对唯一

短链系统的本质是 建立长链与短链的双向映射。核心难点在于:

  1. 如何生成紧凑、无冲突的短链(Base62 + unique)
  2. 如何加速跳转(缓存)
  3. 如何保证高并发下不重复生成(数据库唯一约束或分布式 ID)

小结

短链系统的本质就是把一个长链接映射为一个短链接,访问短链时再重定向回原始 URL。

我的设计思路是:

  1. 生成短链
    长链写入数据库得到自增 ID,然后使用 Base62 编码把这个 ID 转成 6 位短链字符串(因为 62⁶ ≈ 568 亿,完全够用)。

  2. 存储映射
    数据库存两类唯一索引:

    • 长链的 MD5(避免重复生成)
    • 短链字符串(保证唯一)
  3. 访问跳转
    用户访问短链 /ABCDEF → 查数据库或 Redis → 找到长链 → 302 重定向跳转。

  4. 性能优化
    热链加 Redis 缓存;高并发可以用分库分表、分布式 ID 或号段模式。

长链入库 → ID Base62 编码 → 生成短链 → 查映射 → 重定向,这就是完整的短链系统。

分布式锁一般都怎样实现?

常见依赖 Redis、ZooKeeper 来实现分布式锁。

如果让你统计每个接口每分钟调用次数怎么统计?

第三方工具库

SkyWalking

Prometheus Grafana

让你设计一个文件上传系统,怎么设计?

1. 先说目标:

“一个文件上传系统,核心要解决三件事:

  • 能稳定传很大的文件;
  • 不浪费存储空间(去重、断点续传);
  • 不把后端和带宽打垮(限流、分片、异步处理)。”

2. 整体架构先给一笔:

“整体上就是:前端 → 上传网关/Nginx → 上传服务 → 存储(对象存储/分布式文件系统),旁边配个 Redis + DB 做状态和元数据。”

3. 大文件怎么传?——分片上传

关键词:切片 + 记录分片 + 合并

  • 前端切片
    比如统一按 2MB 一片,生成 fileId / uploadId + 分片序号 seq。
  • 分片上传
    每个分片带上:用户ID、uploadId、seq、总分片数、md5 等,走普通 HTTP/表单上传。
  • 服务端落地 & 记录
    分片先写到临时存储(本地磁盘 / 临时桶),
    同时在 Redis 记一条:uploadId -> {已收到的分片集合、总分片数、文件大小},设置过期时间。
  • 合并
    当某个接口或最后一个分片上来时,检查 “已收分片 == 总分片数”,
    就顺序把分片按 seq 合并成一个完整文件,写到正式存储,更新 DB 里的文件元数据(fileId、大小、md5、存储路径等),
    然后删掉 Redis 记录和临时分片。

顺带一提,这套结构天然支持 断点续传
客户端凭 uploadId 请求“我已经传过哪些分片”,然后只补传缺的。

4. 怎么避免重复存储?——文件去重

关键词:md5 摘要 + 秒传

  • 上传前/合并后计算一次文件 md5(或别的 hash)。
  • 合并前:先去 DB/缓存里查这个 md5 是否已经存在:
    • 存在:说明别人传过同一份文件了,本次只写一条“引用关系”(用户文件表指向同一个物理文件),可以直接返回 “秒传成功”。
    • 不存在:正常合并、落库,并把 md5 → 文件记录 这个映射写进去。

这样同一份视频/图片上传一千次,只占一份物理空间。

5. 怎么控住资源?——限流 + 配额

关键词:网关限流 + 业务配额

  • 网关/负载均衡层限流
    • 限制单个文件最大尺寸;
    • 限制上传 QPS、并发连接数;
    • 可以按 IP/用户做滑动窗口限流(防止恶意刷接口)。
  • 业务级限流
    • 每个用户每天可上传的总大小、总文件数;
    • 超过就拒绝或排队。

6. 其他细节顺带带一下:

  • 安全:校验登录态、白名单文件类型,有需要的话加个病毒扫描/内容审核异步任务。
  • 回调/异步处理
    比如视频上传完要做转码、截图,就发一条 MQ 消息给“多媒体处理服务”,跟“上传成功”解耦。
  • 监控 & 清理
    • 定时任务清理长时间未完成的 uploadId 和临时分片;
    • 监控上传成功率、平均耗时、失败原因等。

一句话总结:
“我会用 ‘前端分片 + Redis 记录状态 + 服务端合并 + md5 去重 + 网关限流’ 这一套来设计,既能扛大文件、支持断点续传,又节省存储、把流量和后端压力控制在可预期范围里,后面要扩展转码、审核也只需要在 MQ 上挂新消费者就行。”

什么是限流?限流算法有哪些?怎么实现的?

限流是一种控制请求量的技术,用于确保系统能在一定的负载下稳定运行,避免因请求过多导致系统崩溃。简单来说,限流的目标是限制单位时间内的请求数量。

常见的限流算法有几种:

  1. 漏桶算法:系统请求先进入一个“漏桶”,然后按固定速率从桶里取出请求并执行。如果请求量大于桶的容量,超出的请求会被丢弃或延迟处理。

  2. 令牌桶算法:系统请求会从一个“令牌桶”中获取令牌来执行,桶里定期生成令牌,控制令牌桶中的令牌数。令牌桶适用于需要突发流量的场景。

  3. 计数器算法:直接通过计数请求数,若请求数超过阈值,则限制请求。这种方法比较简单,适用于流量控制较为松散的场景。

  4. 阻塞算法:当请求数量达到阈值时,系统会拒绝新请求,直到请求数降到阈值以下,再恢复接收新请求。

  5. 令牌环算法:类似于令牌桶,但它将多个令牌桶连接成一个环形结构,适用于平衡不同请求速率的场景。

  6. 最小延迟算法:通过预测每个请求的处理时间,选择最少延迟的请求执行,从而控制请求的处理速率。

  7. 滑动窗口算法:通过设定一个固定大小的时间窗口,窗口内的请求数不能超过设定的阈值。随着时间的推移,窗口不断滑动,这使得不同时间段内的请求量得以灵活控制。

总的来说,限流算法可以帮助你确保系统的稳定性,避免因为流量暴增造成的系统负担过重。选择合适的限流算法,能够根据不同的应用场景和需求平衡性能与资源消耗。

让你设计一个 HashMap ,怎么设计?

我设计 HashMap 会从定位、冲突、扩容、性能四点来说:

1)核心结构:底层是 数组 + 链表/红黑树。先对 key 做 hash,再对数组长度取模定位桶位置。

2)冲突解决:采用拉链法。当链表太长(比如超过 8),转成红黑树,提高查询性能到 O(logN)。

3)扩容机制:当负载因子超过阈值,比如 0.75,触发扩容为原来的 2 倍。扩容时重新计算桶位置,必要时可以学习 Redis 做渐进式扩容避免阻塞。

4)hash 优化:hash 函数要分布均匀

线上 CPU 飙高如何排查?

线上 CPU 飙高其实是一个非常常见、但排查流程非常固定的问题。我一般会按照 “定位进程 → 定位线程 → 定位代码 → 修复问题” 这套方法走。

① 定位哪个进程占用 CPU

第一步用:

1
top

看到哪个进程 CPU 最高,比如 Java 进程占了 180%+,就说明是应用本身的问题。

② 定位哪个线程占用 CPU

继续用:

1
top -Hp <进程ID>

找到 CPU 占用最高的线程,比如线程号 4519。

然后将线程号转成 16 进制:

1
printf "%x\n" 4519

③ 打印线程栈,定位到底是哪段代码造成的

用 jstack 或 Arthas:

1
jstack <pid> | grep -A 200 <16进制线程号>

再查看堆栈最顶层,看代码卡在哪个方法。

④ 修复 CPU 热点代码

  • 把高频创建的对象提前到应用启动时初始化一次
    (如:Sequence、Validator)
  • 避免不必要的反射、正则、序列号获取
  • 检查是否有死循环、大量 JSON 解析、大对象创建等

小结

CPU 飙高我一般按固定流程排查:先找进程、再找线程、最后定位到具体代码。

  1. 定位高 CPU 进程
    top 看是哪个进程占满 CPU。

  2. 定位高 CPU 线程
    top -Hp <pid> 找出最耗 CPU 的线程号。

  3. 定位具体代码
    把线程号转成 16 进制:
    printf "%x\n" <tid>
    再用:
    jstack <pid> | grep -A 200 <16进制tid>
    或 Arthas thread -n 3,直接找到热点方法。

  4. 根据堆栈优化代码
    多数根因都是:频繁创建对象、正则/反射过多、DB/IO 阻塞、死循环等。

一句话总结:

top 找进程 → top -Hp 找线程 → jstack/Arthas 找热点方法 → 优化代码,这是排查 CPU 飙高最标准、最高效的流程。

让你设计一个线程池,怎么设计?

线程池本质就是复用一批线程来执行大量任务,减少频繁创建/销毁线程的开销。

  1. 核心参数
    • 核心线程数 coreSize(常驻工作线程),
    • 最大线程数 maxSize,
    • 任务队列(有界队列),
    • 线程空闲存活时间 keepAliveTime,
    • 线程工厂(起名、设置守护/优先级),
    • 拒绝策略(丢弃、抛异常、调用方执行、丢最旧等)。
  2. 调度流程
    • 提交任务时,先看核心线程是否满;
    • 未满就新建工作线程执行;
    • 满了就进队排队
    • 队列也满了再看是否还能扩到 maxSize,能扩就新建线程;
    • 都满了就按照拒绝策略处理。
  3. 扩缩容与监控
    • 支持动态调 coreSize / maxSize,空闲线程超时自动回收;
    • 暴露指标:队列长度、活动线程数、任务耗时、拒绝次数,用于监控和报警。
  4. 队列选择
    • CPU 密集型用无界队列 + 较小线程数
    • IO 密集型用有界队列 + 较大线程数,防止把内存打爆。

一句话总结:我会按「核心/最大线程数 + 有界队列 + 拒绝策略 + 动态扩缩容 + 监控」这套框架去设计线程池,既兼顾性能也能控风险。

如何实现数据库的不停服迁移?

使用双写方案
双写方案意味着同时向原数据库和目标数据库写入数据,确保数据的迁移不会中断业务。
1.搞一个云上数据库,作为从库,同步数据
2.改写业务代码,使写数据的操作,要同时写入主库和从库。要加个开关,可以随时开启以及关闭双写。
3.在业务低峰期,主从完全同步的时候,开启双写,这时候读数据还是在主库上
4.进行数据核对,写代码进行抽样调查,看主库和从库数据是否一致,不一致就告警
5.如果数据核对一致,则进行灰度切流,将一部分比例的用户的读请求切到从库上,逐渐的增加比例到100%
6.跑一段时间,如果没有问题了,关闭双写,只写新库

MySQL 中如何进行 SQL 调优?

MySQL 中如何进行 SQL 调优?

答:SQL 调优我一般分两步走:“先定位问题,再针对性优化”。

  1. 第一步:定位性能瓶颈
  • 开启慢查询日志(slow query log)
    用来记录执行时间超过设定阈值的 SQL,快速找出慢查询语句。

    set global slow_query_log = 'ON';

  • 使用 EXPLAIN 分析执行计划
    查看 SQL 是否使用了索引、扫描了多少行、有没有出现 Using filesort 或 Using temporary 这些低效操作。

  1. 第二步:优化 SQL 语句
  • 合理设计索引:
    建立合适的单列索引或联合索引,满足“最左前缀匹配原则”;常用字段可使用覆盖索引减少回表。

  • 避免索引失效:

    1.避免 SELECT *(只查必要字段)

    2.避免 LIKE ‘%xxx’ 模糊匹配

    3.避免在索引字段上进行函数或计算

    4.注意不同字符集、类型转换导致索引失效

  • 减少不必要的操作:

    1.减少多表 JOIN

    2.避免复杂的 OR 条件(可以用 UNION ALL 替代)

    3.GROUP BY、ORDER BY 尽量排序字段建索引

  • 优化表结构与缓存:

    1.字段类型尽量精简

    2.避免大字段频繁查询

    3.适当使用缓存机制(如 Redis)减轻数据库压力

总结:
“我会先通过慢查询日志和 EXPLAIN 找出性能瓶颈,然后从索引设计、SQL写法和表结构三方面去优化。
比如合理用联合索引、避免 SELECT *、避免索引字段函数计算,让查询走索引、少回表、少排序,从而显著提升性能。”

速答版:
我做 SQL 调优一般分两步:先定位问题,再优化语句。
首先我会开启 慢查询日志 找出执行慢的 SQL,然后用 EXPLAIN 查看执行计划,看是否走了索引、有没有全表扫描或 filesort。
优化时我主要关注 索引设计和写法,比如建立合适的联合索引、遵守最左前缀原则、避免 SELECT *、LIKE ‘%xx’、函数计算等导致索引失效的情况。
同时控制多表 join、group by、order by 的使用,必要时加缓存或优化表结构,让查询更高效。

MySQL 中如何解决深度分页的问题?

MYSQL中深度分页常常遇到的一个问题是,随着分页的深入,查询的效率会变得非常低,因为每次需要跳过很多数据行,尤其是当 LIMIT 和 OFFSET 结合使用时,OFFSET 会导致数据库扫描大量无用的记录,造成性能下降。
为了优化这一点,通常可以采用以下两种方法

  1. 子查询优化分页

比如,如果我们要查询第100000页的数据,每页10条记录,我们可以先通过一个子查询找到第100000页的第一条记录的ID。然后,在实际的查询中,使用这个ID作为起始点来查询接下来的10条记录。这样我们就避免了从头开始扫描所有记录,查询效率大大提升。
例子:

1
2
3
4
SELECT * FROM users WHERE id >= (
SELECT id FROM users ORDER BY id LIMIT 99999990, 1
) ORDER BY id LIMIT 10;

  1. 记录最后查询的ID
    每次分页查询时,我们记录下上一次查询结果的最后一条记录的ID,下一次查询时,直接从这个ID开始查询。这样每次分页查询都避免了大范围的跳过操作,直接从上一次查询的位置开始,显著提高了查询性能。
1
2
SELECT * FROM users WHERE id > last_id ORDER BY id LIMIT 10;

通过这两种方法,我们可以避免传统的 LIMIT + OFFSET 的性能问题,尤其是在数据量大的时候,查询速度会更快,系统的负担也会减轻。

怎么分析 JVM 当前的内存占用情况?OOM 后怎么分析?

我一般会分两步:先看实时内存情况,再分析 OOM dump 文件 去定位根因。

一、如何分析 JVM 当前的内存占用情况?

主要用自带工具:

1)jstat —— 看 JVM 内存实时情况

jstat -gc <pid>
可以看到:

  • Eden、Survivor、Old 区的使用情况
  • GC 次数和耗时
    方便判断是不是某块内存持续上涨、GC 频繁等问题。

2)jmap —— 查看堆的详细结构

jmap -heap <pid> 查看:

  • 堆大小配置
  • 当前各区占用情况
  • GC 类别

jmap -histo <pid> 可以看到:

  • 哪些类实例最多、占用最大
    用来判断是否有往堆里塞大量对象的风险。

二、OOM 后怎么分析?

OOM 时最关键的是 拿到 heap dump 文件

1)开启自动 dump

在启动参数加:

1
2
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/tmp/heapdump.hprof

发生 OOM 时 JVM 会自动生成 .hprof 文件。

2)用专业工具分析 dump

常用工具:

  • MAT(Eclipse Memory Analyzer)
  • VisualVM
  • GCeasy
  • YourKit

这些工具可以告诉你:

  • 哪些对象占了最多内存
  • 哪些对象存在强引用链无法被清理
  • 是否存在缓存未清理、连接未关闭、集合无限膨胀等问题
    最后定位到具体代码并修复。

小结

分析 JVM 内存我主要用两类工具:在线看实时内存 + OOM 后看 dump 文件

① 在线分析(实时内存占用)

  • jstat -gc <pid> 看 Eden、Survivor、Old 区的使用情况和 GC 情况。
  • jmap -heap <pid> 看堆大小、配置、当前占用情况。
  • jmap -histo <pid> 找占内存最多的类。

② OOM 之后怎么分析

  • 启动参数加上:
    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
    OOM 时自动生成 dump。
  • MAT / VisualVM / GCeasy 打开 dump,找占用最大的对象、引用链,定位内存泄漏或对象膨胀的代码。

一句话总结:

在线靠 jstat/jmap 看趋势,OOM 靠 dump 文件找大对象和引用链,这两步就能定位大部分 JVM 内存问题。

如果发现 Redis 内存溢出了?你会怎么做?请给出排查思路和解决方案

如果线上 Redis 内存溢出,我的第一反应是:先扩容,保证业务不受影响;然后再排查根因,做长期优化

一、第一步:止血(确保服务正常)

如果错误提示是 used memory > maxmemory,说明 Redis 达到最大内存限制,继续写入被拒绝。

所以第一时间我会:

  • 扩容 Redis 实例内存 或
  • 增加新的 Redis 节点分担压力
  • 先保证线上业务不挂,再排查根因。

二、第二步:排查 Redis 内存为什么涨满?

常见的几种情况

  1. 数据量太多
  • 存入 Redis 的 key/数据本身太大,超过内存容量。
  1. 大量 key 没有设置过期时间
  • 数据长期堆积不释放,导致内存持续增长。
  1. 大对象 / 大型数据结构滥用
  • 比如 hash、list、set 里塞了大量内容,占用极大空间。
  1. 持久化策略影响
  • RDB fork 时会额外占一份内存(写时复制机制),高峰容易炸。
  1. 业务代码误用 Redis 当数据库
  • 比如把日志、明细表全写进 Redis。

第三步:解决方案(短期 + 长期)

(1)短期优化

  • 启用合适的淘汰策略
    如 allkeys-lru、volatile-lru,让 Redis 自动删除不常用 key。

  • 为 key 设置合理的 TTL
    避免无过期数据永远占内存。

(2)长期优化

  • 优化数据结构设计

    • 大 hash 拆分成小 hash,减少单个结构的内存占用。
  • 水平扩展 Redis

    • 分库分槽,将数据打散到多个 Redis 节点降低单机压力。
  • 优化持久化策略

    • 减少 RDB 在高峰期触发并导致额外内存开销。

下面是 30 秒极速口语版,面试时直接说就能拿高分👇

小结

如果发现 Redis 内存溢出,我的第一反应是先扩容保证业务不挂,然后再排查原因。

常见根因主要有三类:

  1. 数据太多:写入的数据量超出 Redis 内存限制。
  2. 无过期时间:大量 key 没 TTL,越积越多。
  3. 大对象滥用:比如一个 hash、list、set 塞几十万条数据。

解决也很简单:

  • 开启合适的淘汰策略(allkeys-lru 等)
  • 给 key 设置合理 TTL
  • 拆分大对象,优化数据结构
  • 必要时做 Redis 水平扩容(分片/集群)
  • 优化 RDB/AOF 配置,避免高峰期 fork 占用额外内存

一句话总结:

先扩容止血,再找无 TTL、大对象、数据堆积等根因;最后通过淘汰策略、TTL、拆分数据结构和扩容彻底解决。

让你设计一个购物车功能,怎么设计?

为了设计一个购物车功能,我们首先要理解其核心需求:在用户选购商品后,允许用户暂时将商品存放在购物车中,直到完成购买。接下来,我会根据已登录与未登录的用户分别设计存储方案

未登录用户购物车

  • 存储方式未登录的用户信息一般不存储在后端,而是使用客户端技术,比如CookieLocalStorage来临时保存。

  • 数据格式:保存的信息可以是一个JSON对象,包括SKUIDcount(数量)、timestamp(添加时间)。

    示例:

    1
    2
    3
    4
    5
    6
    {
    "cart": [
    { "SKUID": 10086, "timestamp": 1666513990, "count": 2 },
    { "SKUID": 10010, "timestamp": 1666513990, "count": 10 }
    ]
    }

已登录用户购物车

  • 存储方式:对于已登录用户购物车数据需要存储在后端,避免用户更换设备后购物车数据丢失。

  • 数据库:可以使用数据库表来存储每个用户的购物车信息,包括user_idsku_idcounttimestamp等。

  • Redis缓存:为了提升性能,使用Redis缓存也很常见。将user_id作为Redis的key,购物车的商品信息作为value存储。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
      {
    "KEY": 12343123,
    "VALUE": [
    { "SKUID": 10086, "timestamp": 1666513990, "count": 2 },
    { "SKUID": 10010, "timestamp": 1666513990, "count": 10 }
    ]
    }


    ### 存储方案对比

    * **Redis**:快速响应,适合高并发访问。性能较好,但数据丢失的风险较大。
    * **MySQL**:数据可靠性高,支持事务和复杂查询,适用于需要数据持久化的场景,但性能相对较低,处理并发的能力有限。

    总结来说,对于未登录用户,数据使用临时存储,减少对后端的依赖;对于已登录用户,使用数据库或Redis结合的方式,保证数据的持久化和高效访问。



    ## 商家想要知道自己店铺卖的最好的 top 50 商品,如何实现这个功能?

    **商家想知道自己店铺卖得最好的 Top 50 商品,怎么实现?”**

    如果要做一个店铺销量 Top50 排行榜,为了兼顾实时性和性能,我会用 **Redis 的 ZSet 来做动态排行榜**。每个商品卖出一次,就对它对应的 score 做一次 ZINCRBY,value 存商品 ID,这样写入是 O(1),非常高效。

    查前 50 名时,用 `ZREVRANGE key 0 49` 就能直接按销量倒序取出,时间复杂度是 `O(logN + 50)`,对百万级数据也能快速返回。

    如果商品非常多,我会加一些优化:

    - **按店铺维度拆分 ZSet**,避免 big key;
    - 高频写入可以走 **异步/队列批量更新**,减轻 Redis 压力;
    - 热门榜单可以做 **本地缓存 + 定时刷新**,进一步提升查询速度;
    - 如果要全国榜或多维榜,可以用 **Top-K 合并**策略,比如“先取各分片前100,再综合排序”。

    整体方案简单、高效、可横向扩展,也易于应对大流量场景。



    ## MySQL 中 如果我 select * from 一个有 1000 万行的表,内存会飙升么?



    不会的。
    MySQL 执行 SELECT * FROM ... 时,并不会把 1000 万行一次性全部加载到内存里,而是 **边查询、边取数据、边通过网络发给客户端**,属于 **流式输出**。



    ### 为什么不会把 1000 万行一次性放进内存?

    MySQL 是按照 **批次**(batch)从磁盘读取数据的,每批的数据量由 `net_buffer_length` 控制,默认只有 **16KB**。

    实际流程是:

    1. MySQL 从磁盘取一行数据
    2. 写入 `net_buffer`(最多 16KB)
    3. buffer 满了就发送给客户端
    4. 继续下一批直到结束

    所以服务器端内存始终是小批小批地用,不会因为 1000 万行而爆炸。


    ### 什么时候可能卡住?

    **不是 MySQL 内存飙升,而是客户端读得太慢。**

    如果客户端处理得慢,导致服务器端的 **socket send buffer 写满了**,那么 MySQL 会暂时停住发送数据,但依然不会导致 MySQL 内存暴涨。



    ### 总结

    > `SELECT *` 大表不会导致 MySQL 内存飙升,因为 MySQL 是**流式传输、批量发送数据**,不会把所有行一次性加载到内存。

    ## 让你实现一个订单超时取消功能,怎么设计?

    ### 1. 先说本质

    订单超时取消,本质就是一个**延迟任务**问题:

    > 创建订单时记录一个“到期时间”,到点后触发一次关单逻辑(改状态、回库存、记日志),并且要**可持久、可扩展、不丢不重**。

    设计时我会先问 3 个约束:

    1. 允许多大误差?(几秒 / 几十秒 / 几分钟)
    2. 订单量级多大?(几十万 / 几千万 / 上亿)
    3. 是单体还是分布式、有没有 Redis / MQ 这类基础设施。



    ### 2. 给几种典型方案 + 评价

    #### 方案一:定时任务扫表(最常用、易实现)

    **做法:**

    - 下单时写入订单表:`order_status=未支付、expire_time=now+15min`。
    - 用 xxl-job / Quartz / 自写 ScheduledThreadPoolExecutor 做一个**周期任务**:
    每分钟扫一次 `WHERE status='未支付' AND expire_time <= now` 的订单,批量关闭。

    **优点:**

    - 实现最简单,依赖最少;对时间精度要求不高时很好用。
    - 扩展到分布式也简单:任务只在一台调度机跑就行。

    **缺点 & 优化点:**

    - 时间不够精确(可能晚几秒~几十秒)。
    - 大量订单集中扫表,会对 DB 有压力,分库分表时要做**分片扫描**,可以按 `order_id hash`、`expire_time 分段` 分批查。
    - 需要做好**幂等**:重复扫同一订单只会从“未支付→已关闭”一次。

    > 结论:**业务量中等、时间精确度要求不高**时,定时任务是首选。



    #### 方案二:Redis / Redisson 延迟队列(分布式、精度更高)

    **做法(Redis ZSet 思路):**

    - 下单时,往 Redis 的 zset 写一条:
    `zadd order_timeout_zset (expire_timestamp) orderId`。
    - 起一个后台任务,定期从 zset 里拿出 `score <= 当前时间` 的订单号,做关单逻辑。

    **问题:**

    - 多个消费者同时取任务会有**重复关单**,通常用分布式锁 + 幂等解决。

    **Redisson 优化:**

    - Redisson 提供了 `RDelayedQueue`,内部就是 **zset + 内存延时队列**:
    - 你只需要 `queue.offer(orderId, 15, MINUTES)`;
    - 到点后 Redisson 自动把数据投到真正的队列,你的消费者监听队列做关单即可。
    - 帮你处理了并发、重复、调度等细节,天然支持持久化、高可用。

    > 结论:**分布式 + 订单量较大 + 业务已经依赖 Redis** 时,
    > 我会优先考虑 **Redisson 延迟队列**,代码简洁、伸缩性好。



    #### 方案三:MQ 延迟消息 / 死信队列(能用,但我会慎选)

    常见做法有:

    - RocketMQ 延迟消息:下单发一条“延迟 15 分钟”的关单消息,消费者收到后关单。
    - RabbitMQ 死信队列或延迟插件:设置 TTL,到期后进入死信队列,消费者处理。

    **优点:**

    - 天然支持分布式和高并发,扩机器就能扛量。
    - 时间精度比较高。

    **缺点:**

    - **订单关闭是高“空跑率”的场景**:大部分订单都会按时支付,延迟消息却还是要发、要路由、要持久化,造成大量无效调度。
    - RocketMQ 自带延迟级别有限制(1s、5s、10s…20m、30m…),不一定匹配业务;
    RabbitMQ 死信队列会有队头阻塞等问题,需要额外治理。

    > 所以文档里建议:**订单到期关闭这类业务,一般不优先用 MQ**,更多是用在已经强依赖 MQ、且量级适中时作为备选。



    ### 3. 我会怎么选?

    如果让我设计,我通常会这样跟面试官收尾:

    1. **小体量 / 单体应用**
    - 直接用 **定时任务扫表**,实现简单、足够稳定。
    2. **分布式 + 中等体量**
    - 优先 **定时任务 + 分库分表分片扫描**;
    - 若已有 Redis 集群,会考虑 **Redisson 延迟队列** 做更精细的触发。
    3. **大体量 / 高并发**
    - 订单写 DB,**定时任务按分片分段扫表关单**,
    - 或者 **Redisson 延迟队列 + 多消费者集群**,同时在关单接口上做好状态机 + 幂等控制。

    同时还会强调 3 点工程细节:

    - **幂等**:关单接口按 `orderId + 状态` 控制,多次执行结果一致。
    - **状态机**:订单状态流转只允许 `未支付→已关闭` 这条路径。
    - **监控告警**:统计超时订单量、失败量、任务延迟时间,有问题能快速定位。

    这样答,一方面说明你知道各种**实现方式和 trade-off**,另一方面也给出了一套自己会落地的**工程方案**。

    ## 朋友圈点赞功能如何实现,简单说说?

    实现朋友圈点赞,其实就围绕三个核心动作:**点赞、取消点赞、查询点赞列表**。
    在项目上最关键的问题是:**如何存储点赞数据、如何按时间顺序展示、如何避免重复点赞**。

    我一般会用 Redis 的 **ZSet** 来做,因为它天然支持去重、排序,性能也非常高。

    ### 1.数据结构设计
    - key:用朋友圈的动态 ID
    - value:点赞用户的 userId
    - score:点赞时间的时间戳

    这样有几个好处:
    1 用户重复点赞不会插入新记录(天然去重)
    2 可以按时间排序,显示谁先点的赞
    3 删除用户也很简单,直接 zrem 即可

    ### 2.核心操作
    #### **点赞**

    ```redis
    ZADD like:{postId} 时间戳 userId

取消点赞

1
ZREM like:{postId} userId

查询点赞列表(按时间倒序)

1
ZREVRANGE like:{postId} 0 -1

3.这样设计的优点

  • 速度快:ZSet 的增删查都是 O(logN),非常适合高并发
  • 顺序明确:前端可以直接展示按时间排序的点赞列表
  • 天然去重:用户重复点赞不会造成脏数据
  • 可扩展性高:以后想做点赞数排行榜也很容易(比如按用户做 top K)

小结

“朋友圈点赞我一般用 Redis 的 ZSet 实现。每条动态用一个 ZSet 存点赞用户,value 是 userId,score 用时间戳。点赞就是 zadd,取消点赞是 zrem,查询按时间排序直接 zrevrange。这样能保证点赞天然去重、按时间有序,增删查都特别快,也方便以后扩展点赞排行榜。”