返回八股知识点
八股知识点 / 发布 2026-04-24 10:14 / 更新 2026-06-15 00:31

系统设计

秒杀系统面对的是瞬时高并发,不是普通下单流程。

系统设计高并发

秒杀系统设计

书面笔记版

秒杀系统面对的是瞬时高并发,不是普通下单流程。核心目标是抗高并发、防超卖、防重复下单、削峰填谷和最终一致。

典型链路:

flowchart TD A[用户请求] --> B[网关限流/风控] B --> C[登录校验/活动校验] C --> D[Redis Lua 校验库存和用户资格] D -->|失败| E[返回售罄/重复参与] D -->|成功| F[发送下单消息到 MQ] F --> G[消费者异步创建订单] G --> H[数据库扣库存并写订单] H --> I[用户查询订单结果]

关键设计点:

设计点 说明
流量拦截 前端按钮置灰、验证码、网关限流、活动校验
数据预热 活动页静态化,库存和热点商品信息提前放入 Redis
Redis Lua 原子完成库存判断、扣减和用户去重
MQ 削峰 Redis 扣成功后写 MQ,后端异步创建订单
DB 兜底 stock > 0 条件更新防超卖,唯一索引防重复下单
补偿对账 MQ 发送失败、落单失败、库存不一致时靠重试和对账修复

秒杀入口限流通常放在网关或接入层,常见选型:

场景 推荐算法 说明
网关入口 QPS 令牌桶或滑动窗口 令牌桶控制平均速率并允许短突发;滑动窗口统计最近一段时间,避免固定窗口边界突刺
用户/IP/商品维度 滑动窗口 限制某个用户、IP 或活动商品在最近 N 秒内的请求数
后端写库/下游调用 漏桶思想或 MQ 削峰 请求先排队,消费者按稳定速率处理,保护 DB 和下游

固定窗口计数器实现简单,但秒杀高峰容易在窗口切换瞬间出现双倍流量,一般不作为核心入口限流的唯一方案。

Lua 这里的原子性只指 Redis 内部:Redis 会把脚本当成一个整体命令串行执行,脚本执行期间其他客户端命令不能插到“查库存、判断用户、扣库存、记录用户”中间。但它不保证 Redis、MQ、数据库之间的分布式事务,所以后面仍然需要 MQ 重试、数据库条件更新、唯一索引和补偿对账。

数据库兜底示例:

update product
set stock = stock - 1
where id = #{skuId}
  and stock > 0;

订单表加唯一索引:

uniq_user_sku(user_id, sku_id)

常见坑:

  • Redis 扣了库存但 MQ 发送失败,需要本地消息表、事务消息或失败重试。
  • MQ 可能重复投递,消费者必须幂等。
  • 数据库落单失败,需要库存补偿、对账任务或人工兜底。
  • 热点 key 压力大,可以考虑库存分片、本地缓存和限流。

面试口述版

秒杀系统我会从削峰、防超卖、防重复下单三个点讲。请求先经过网关限流和活动校验,入口限流可以用令牌桶或滑动窗口,避免固定窗口边界突刺;库存提前预热到 Redis,然后用 Lua 脚本原子完成库存判断、扣减和用户去重,扣成功后写 MQ,后端按漏桶思想平滑消费。Lua 原子是因为 Redis 把脚本作为一个整体串行执行,中间不会被其他客户端命令插队;但它只保证 Redis 内部原子,不保证 MQ 和数据库一起原子,所以还要靠 MQ 重试、数据库 stock > 0 条件更新、唯一索引、补偿和对账兜底。

缓存一致性怎么实现

书面笔记版

缓存一致性要先明确一点:缓存是加速层,数据库是事实来源。多数互联网业务追求最终一致,不追求绝对强一致。

最常用模式是旁路缓存(Cache Aside Pattern)。

模式 核心思路 特点
旁路缓存(Cache Aside) 读缓存,未命中查 DB 并写缓存;写时更新 DB 再删缓存 最常用,简单可控
读穿透(Read Through) 应用只访问缓存,缓存层负责回源 DB 应用简单,但缓存层要理解数据加载
写穿透(Write Through) 写缓存时同步写 DB 一致性较好,但写延迟高
写回(Write Back) 先写缓存,异步落 DB 性能高,但丢数据和不一致风险大
绕写(Write Around) 写 DB,不写缓存,下次读再加载 避免冷数据污染缓存
预刷新(Refresh Ahead) key 快过期前提前刷新 适合热点数据

旁路缓存(Cache Aside)读流程:

  1. 先读缓存。
  2. 命中直接返回。
  3. 未命中查数据库。
  4. 查到后写缓存。

写流程推荐:

  1. 先更新数据库。
  2. 再删除缓存。
  3. 后续查询回源数据库并重建缓存。

删除缓存失败时,需要兜底:

  • 删除失败重试。
  • MQ 异步删除。
  • 监听 binlog 删除缓存。
  • 设置 TTL 最终过期。

延迟双删流程:

  1. 先删除缓存。
  2. 更新数据库。
  3. 等一小段时间。
  4. 再删除一次缓存。

它主要降低并发读把旧值回写缓存的概率。延迟时间不要死记,通常按读请求 P99/P999 耗时加 DB、Redis、网络抖动 buffer 估算。普通业务可以先从 500ms ~ 2s 估算,再结合压测和监控调整。

强一致业务,比如核心余额、资金流水,不应只依赖缓存一致性方案,可以考虑不走缓存,或用数据库事务和锁保证。

面试口述版

缓存一致性我一般按旁路缓存讲。读的时候先查缓存,未命中查数据库再写缓存;写的时候先更新数据库,再删除缓存。删除缓存失败要有重试、MQ、binlog 监听或 TTL 兜底。延迟双删主要解决并发读可能把旧值写回缓存的问题,延迟时间按读链路高分位耗时加 buffer 估算。它不是强一致方案,只是降低不一致窗口。

幂等性防止重复提交怎么做

书面笔记版

幂等性指同一个业务请求执行一次和执行多次,最终结果一致。重复提交常见来源包括用户连续点击、客户端超时重试、网关或服务重试、MQ 重复投递、第三方回调重复通知。

常见方案:

方案 适合场景 核心
前端按钮置灰 普通表单 只优化体验,不能兜底
Token 防重 表单提交、创建订单 一次性 token,校验后删除
Redis SET NX EX 高并发接口 用业务唯一键抢占处理资格
数据库唯一索引 下单、支付、开户 最终兜底,防重复写
状态机 订单、支付、退款 只有合法状态才能流转
分布式锁 同一资源串行处理 同一业务 key 同时只允许一个处理者
消费记录表 MQ 消费 记录 messageId,消费过就跳过

Redis 幂等 key 示例:

idem:order:{userId}:{requestNo}
idem:pay:{payNo}
idem:callback:{thirdTradeNo}

状态机幂等示例:

update pay_order
set status = 'PAID'
where pay_no = #{payNo}
  and status = 'WAIT_PAY';

只有未支付状态才能变成已支付,重复回调影响行数为 0,可直接认为已经处理过。

常见坑:

  • 只做前端防抖,挡不住绕过前端和服务重试。
  • 幂等 key 设计太粗会误拦截,太细挡不住重复请求。
  • Redis key 没有过期时间,异常后长期残留。
  • 没有数据库唯一索引,上层防重失效后会写重复数据。
  • MQ 消费不做幂等,重复投递会重复执行业务。

面试口述版

幂等防重复提交要前后端一起做,但后端必须兜底。前端可以按钮置灰,后端常用一次性 token 或业务唯一号。高并发下可以用 Redis SET NX EX 判断是不是第一次请求,订单、支付这类核心数据还要用数据库唯一索引兜底。对于订单状态和支付回调,要用状态机控制,只允许从合法状态流转。MQ 消费也必须幂等,因为消息可能重复投递。

access_token、refresh_token 和 OAuth2.0

书面笔记版

access_tokenrefresh_token 是常见登录认证、第三方授权和开放平台接口里的两类令牌。它们经常一起出现,但职责不一样:

Token 中文理解 核心作用
access_token 访问令牌 访问业务接口或资源接口,证明“当前请求有权限访问这个资源”
refresh_token 刷新令牌 access_token 过期后,用来换新的 access_token,不直接访问业务资源

典型用法是:

Authorization: Bearer <access_token>

业务接口一般只认 access_tokenrefresh_token 只应该发给认证服务器的刷新接口,例如:

POST /oauth/token
grant_type=refresh_token
refresh_token=xxx

为什么需要 refresh_token,不能只用 access_token

方案 问题
只用长期 access_token 用户体验好,但一旦泄露,攻击者可以长期访问接口,风险窗口太大
只用短期 access_token 安全性更好,但过期后用户要频繁重新登录,体验很差
短期 access_token + 长期 refresh_token 用短访问令牌降低泄露影响,用刷新令牌保证用户不用频繁登录

所以这套机制本质是在安全性和用户体验之间做平衡

  • access_token 有效期短,比如 15 分钟、30 分钟、2 小时。
  • refresh_token 有效期长,比如 7 天、30 天,甚至更长。
  • 平时访问接口只带 access_token,减少长期凭证暴露频率。
  • 只有当 access_token 过期时,才使用 refresh_token 去换新令牌。
  • 如果 refresh_token 也过期、被吊销或校验失败,就必须重新登录。

两者使用场景区别:

对比点 access_token refresh_token
主要用途 访问业务接口、资源接口 刷新或续期 access_token
生命周期 短,有效期一般较短 长,有效期一般较长
携带频率 每次调用受保护接口都要带 只在刷新令牌时携带
发送对象 资源服务器或业务服务 授权服务器或认证服务
泄露影响 短期内可访问资源 风险更高,可能持续换取新访问令牌
常见形态 JWT 或不透明随机字符串 通常是不透明随机字符串
服务端校验 JWT 可本地验签,不透明 token 需查库或查 Redis 通常需要服务端存储、校验、吊销
吊销方式 可依赖短过期时间,也可配黑名单 必须支持主动吊销、过期、设备维度管理

典型登录和刷新流程:

  1. 用户登录,认证服务校验账号密码、短信验证码、扫码结果或第三方授权结果。
  2. 登录成功后,认证服务签发 access_tokenrefresh_token
  3. 前端访问业务接口时,在请求头中携带 access_token
  4. 业务服务校验 access_token,通过后放行业务请求。
  5. access_token 过期后,前端调用刷新接口并携带 refresh_token
  6. 认证服务校验 refresh_token,校验通过后签发新的 access_token,也可以同时轮换新的 refresh_token
  7. 如果 refresh_token 过期、被吊销、设备不匹配或账号状态异常,刷新失败,用户需要重新登录。

实际设计时要注意:

  • access_token 不要设置得太长,否则泄露后风险窗口大。
  • refresh_token 权限更敏感,不能每个业务请求都带,避免暴露面变大。
  • 刷新令牌建议服务端保存一份状态,例如 Redis 或数据库中保存用户、设备、过期时间、是否吊销。
  • 可以做 refresh token rotation:每次刷新都签发新的 refresh_token,旧的立即失效,降低重放风险。
  • 登出、改密码、账号封禁、设备下线时,要让对应的 refresh_token 失效。
  • 多端登录时,最好按设备维度管理 refresh_token,避免一个设备登出影响所有设备,或者根据业务要求实现全端下线。
  • 浏览器场景下,refresh_token 更适合放在 HttpOnlySecureSameSite Cookie 中,减少被 JS 读取的风险;access_token 可以放内存中,降低长期存储风险。
  • 如果 access_token 用 JWT,优点是业务服务可以本地验签,减少查库;缺点是签发后不容易立即失效,通常要配合短过期时间、黑名单或版本号。

OAuth2.0 是一个授权框架,重点解决“第三方应用如何在用户授权后访问用户资源”的问题。严格来说,OAuth2.0 本身不是单纯的登录协议;如果要做标准登录身份认证,通常会在 OAuth2.0 之上使用 OpenID Connect(OIDC)。但在日常面试和业务开发里,第三方登录经常会和 OAuth2.0 一起讨论。

OAuth2.0 里有四个核心角色:

角色 说明 例子
资源拥有者 Resource Owner 资源的拥有者,通常是用户本人 用户
客户端 Client 想访问资源的应用 第三方网站、App、小程序
授权服务器 Authorization Server 负责登录、授权、签发 token 微信、GitHub、Google 的授权服务
资源服务器 Resource Server 保存用户资源并校验 access_token 用户信息接口、订单接口、网盘文件接口

OAuth2.0 常见参数:

参数 作用
client_id 标识客户端是谁
client_secret 客户端密钥,只能保存在后端,不能暴露给前端
redirect_uri 授权成功后的回调地址,通常要和平台注册配置完全匹配
response_type=code 表示使用授权码模式,先拿 code,再换 token
scope 授权范围,比如读取头像、邮箱、联系人
state 防 CSRF,也可以携带登录前页面状态,回调时要校验
code 授权码,短期有效、一次性使用,用来换 token
grant_type 换 token 的授权类型,比如 authorization_coderefresh_tokenclient_credentials

常见授权模式:

模式 适合场景 说明
授权码模式 Authorization Code Web 服务端应用、第三方登录 用户授权后返回 code,后端用 code 换 token,安全性最好,最常用
授权码 + PKCE App、SPA、小程序等公开客户端 没有安全保存 client_secret 的能力,用 PKCE 防止授权码被截获
客户端模式 Client Credentials 服务调用服务 没有用户参与,客户端用自己的身份换 token
刷新令牌 Refresh Token token 续期 access_token 过期后,用 refresh_token 换新的 token
密码模式 Password 早期自家高度可信客户端 用户把账号密码交给客户端,风险大,新系统一般不推荐
隐式模式 Implicit 早期浏览器前端应用 token 直接暴露在浏览器 URL,风险高,现在一般不推荐

授权码模式主流程:

1. 用户访问第三方应用,点击“使用某平台登录”。
2. 第三方应用把用户重定向到授权服务器。
3. 用户在授权服务器完成登录,并确认授权。
4. 授权服务器重定向回第三方应用,并带上 code。
5. 第三方应用后端拿 code + client_id + client_secret 去授权服务器换 token。
6. 授权服务器返回 access_token,可选返回 refresh_token。
7. 第三方应用用 access_token 调用资源服务器,获取用户信息或访问用户资源。

面试时要特别区分两个概念:

  • 认证 Authentication:你是谁,比如账号密码登录、扫码登录、OIDC 返回身份信息。
  • 授权 Authorization:你能访问什么,比如用户授权第三方应用读取头像、邮箱、联系人。

OAuth2.0 更偏授权;access_token 是访问资源的凭证;refresh_token 是续期访问令牌的凭证。

面试口述版

access_token 是访问令牌,用来访问业务接口或资源接口,一般有效期比较短,请求时放在 Authorization: Bearer xxxrefresh_token 是刷新令牌,不直接访问业务接口,只在 access_token 过期后去认证服务换新的访问令牌。之所以需要 refresh token,是因为只用长期 access token 泄露风险太大,只用短期 access token 又会导致用户频繁登录,所以常用“短 access token + 长 refresh token”平衡安全和体验。OAuth2.0 本质是授权框架,常见授权码模式是用户授权后返回 code,后端用 code 换 token,再用 access token 访问资源;如果 refresh token 也过期或被吊销,就需要重新登录。

为什么选择 MinIO

书面笔记版

MinIO 是一个 S3 兼容的对象存储服务,常用于私有化部署和内网部署场景。

常见用途:

  • 图片、头像、商品图、合同附件。
  • 音视频、压缩包、安装包、导入导出文件。
  • 数据备份、归档文件、数据库备份包。
  • 日志归档、审计文件、报表文件。
  • AI/RAG 场景中的原始文档、PDF 和解析中间文件。

常用能力:

能力 说明
S3 兼容 可使用 AWS S3 SDK,生态成熟
Bucket 管理 按业务或环境隔离文件空间
预签名 URL 临时授权上传下载,避免业务服务中转大文件
分片上传 适合大文件,提高稳定性
权限策略 可按 bucket、路径、账号控制访问
版本控制 同一对象保留多个版本
生命周期 自动清理过期文件或转归档
事件通知 上传后触发 MQ、Webhook 等处理

不用本地磁盘的原因:

  • 多实例之间文件不共享。
  • 扩容、迁移、备份麻烦。
  • 应用服务和文件存储耦合。
  • 权限、生命周期、临时 URL 等能力要自己实现。

相比 FastDFS,MinIO 的 S3 生态、SDK、云迁移能力和对象存储能力更完整;如果已经上公有云,OSS/COS/S3 运维更省心;如果私有化、内网部署、数据不能出本地,MinIO 更合适。

面试口述版

选择 MinIO 主要因为它是 S3 兼容的对象存储,生态成熟,适合私有化部署。它不只是存图片,还能存附件、音视频、导入导出文件、备份、日志归档和 AI 文档。相比本地磁盘,它更适合多实例共享、权限控制、预签名上传下载和生命周期管理;相比 FastDFS,它的 S3 生态和云迁移能力更好。如果已经在公有云上,可以优先用云厂商 OSS;如果要私有化和数据本地可控,MinIO 更合适。

Docker 容器化部署和物理机部署区别

书面笔记版

Docker 容器化部署的核心价值是把应用和运行环境一起打包成镜像,让开发、测试、生产尽量使用同一套运行环境。

对比点 物理机 / 直接部署 Docker 容器化部署
交付物 JAR/WAR + 环境安装文档 镜像,包含应用和基础运行环境
环境一致性 依赖人工安装,容易不一致 镜像一致,环境差异更小
启停方式 手动脚本或 systemd docker run/ Compose / K8s
扩缩容 新机器要重新装环境 拉镜像启动新容器
回滚 替换包和配置,操作较重 切回旧镜像版本
隔离性 多应用共享系统环境 进程、文件系统、网络隔离
数据持久化 直接写本机目录 需要 volume 或外部存储

典型部署流程:

  1. 编写 Dockerfile
  2. 构建镜像。
  3. 推送镜像仓库。
  4. 服务器拉取镜像。
  5. 启动容器并挂载配置、日志目录和数据卷。
  6. 查看状态和日志。
  7. 新版本发布时替换镜像,回滚时切回旧镜像。

注意点:

  • 容器本身不适合保存重要数据,数据库、文件、日志要挂载卷或使用外部存储。
  • 生产镜像版本要固定,不要长期使用 latest
  • 配置和密钥不要写死在镜像里。
  • 容器不是虚拟机,多个容器共享宿主机内核。
  • 生产环境通常进一步用 Kubernetes、Docker Compose 或 CI/CD 平台管理。

面试口述版

物理机部署需要先装 JDK、中间件、配置目录和日志目录,再把应用包放上去运行,环境容易不一致。Docker 是把应用和基础运行环境打成镜像,部署时拉镜像启动容器,扩容、回滚和迁移都更方便。典型流程是写 Dockerfile、构建镜像、推送镜像仓库、服务器拉镜像、挂载配置和日志目录、启动容器。要注意容器无状态优先,重要数据要放 volume 或外部存储。

三分钟串讲版

书面笔记版

系统设计串讲可以按秒杀、缓存一致性、幂等、认证授权几个高频问题组织。

秒杀系统的核心是削峰、防超卖、防重复下单。活动页静态化,库存预热到 Redis,请求经过网关令牌桶或滑动窗口限流后,用 Redis Lua 原子完成库存判断、扣减和用户去重;Lua 原子性来自 Redis 对脚本的串行整体执行,但只覆盖 Redis 内部;扣减成功后写 MQ,后端按漏桶思想异步创建订单;数据库通过 stock > 0 条件更新和唯一索引兜底。

缓存一致性一般采用旁路缓存。读请求先查缓存,未命中查数据库再写缓存;写请求先更新数据库,再删除缓存。删除失败要有重试、MQ、binlog 监听或 TTL 兜底。延迟双删可以降低旧值回写缓存概率,但不是强一致方案。

幂等性解决重复提交和重复执行问题。前端按钮置灰只是体验优化,后端要通过 token、业务唯一号、Redis SET NX EX、数据库唯一索引、状态机来兜底。MQ 消费也要幂等。

认证授权常用短 access_token 加长 refresh_tokenaccess_token 负责访问业务接口,短期有效,降低泄露影响;refresh_token 只负责续期,长期有效但要支持服务端吊销、过期和设备维度管理。OAuth2.0 本质是授权框架,常见授权码模式是用户授权后返回 code,后端用 code 换 token,再用 access_token 访问资源。

面试口述版

秒杀系统我会先讲削峰、防超卖、防重复下单:入口用令牌桶或滑动窗口限流,库存预热 Redis,Lua 原子扣减和去重,成功后写 MQ 按漏桶思想异步下单,最后数据库用条件更新和唯一索引兜底。缓存一致性按旁路缓存讲,读缓存未命中查 DB 再写缓存,写时先更新 DB 再删缓存,失败靠重试、MQ、binlog 和 TTL 兜底。幂等则强调后端兜底,token、业务唯一号、Redis SET NX EX、唯一索引和状态机一起用,核心链路不能只靠 Redis。认证授权可以讲短 access token 加长 refresh token,前者访问接口,后者负责续期;OAuth2.0 更偏授权框架,典型是授权码换 token 后访问资源。