阶段3(下):RAG、向量数据库专题;接入 BOSS直聘调研(research/、岗位对照和延伸技术栈);新增 cs 领域和 12 个技术栈骨架;面试题全部补齐追问并加入校验
This commit is contained in:
1 parent
7f52513ffc
commit
6650f7eb6d
52 files changed
+2777
-38
No files matched your search
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: backend.data-pipeline.index
|
||||
title: 数据管道与第三方接口对接
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [data-pipeline]
|
||||
tags: []
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# 数据管道与第三方接口对接
|
||||
|
||||
批处理与流处理、失败重试、幂等、数据质量告警、第三方 API 限流与字段变化。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: backend.elasticsearch.index
|
||||
title: Elasticsearch
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [elasticsearch]
|
||||
tags: []
|
||||
week: [6, 14]
|
||||
---
|
||||
|
||||
# Elasticsearch
|
||||
|
||||
倒排索引、中文分词、BM25、与向量的混合检索、数据同步。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
@@ -20,6 +20,8 @@ week: [1, 2, 3, 16]
|
||||
|
||||
**参考答案**:Gin 的 `Engine` 实现了 `http.Handler` 接口,底层仍然由 `net/http` 负责监听和连接管理。Gin 增加了基于前缀树的高性能路由、`gin.Context`(封装请求和响应、参数获取、绑定、中间件间传值)、中间件链和常用工具(JSON 渲染、校验)。所以 Gin 可以和任何标准库中间件、`httptest` 一起使用。
|
||||
|
||||
**追问**:Gin 的 `*gin.Context` 和标准库的 `context.Context` 是什么关系?→ `gin.Context` 是 Gin 自己的请求上下文(参数、响应、中间件链),它也实现了 `context.Context` 接口;向下游传递取消信号时,用 `c.Request.Context()` 更明确。
|
||||
|
||||
### 【基础】Gin 怎么获取路径参数、查询参数和请求体?
|
||||
|
||||
**参考答案**:路径参数 `c.Param("id")`;查询参数 `c.Query("k")` / `c.DefaultQuery("k", "默认")`;请求体用绑定 `c.ShouldBindJSON(&req)`,查询参数也可以 `c.ShouldBindQuery(&q)` 绑定到结构体。绑定时根据结构体的 `binding` 标签做校验。
|
||||
@@ -30,10 +32,14 @@ week: [1, 2, 3, 16]
|
||||
|
||||
**参考答案**:按注册顺序依次执行,形成洋葱模型:`c.Next()` 之前的代码按顺序执行,之后的代码按相反顺序执行。`c.Next()` 立即执行后续的处理函数;`c.Abort()` 阻止后续处理函数执行,但当前函数会继续执行完,所以通常 `Abort` 后紧跟 `return`。
|
||||
|
||||
**追问**:在中间件里 `c.Abort()` 之后还需要 `return` 吗?→ 需要。`Abort` 只是阻止后续处理函数执行,当前函数后面的代码仍会继续执行。
|
||||
|
||||
### 【基础】RESTful 接口的状态码怎么选?
|
||||
|
||||
**参考答案**:查询成功 200、创建成功 201、删除成功 204、参数错误 400、未认证 401、已认证但无权限 403、不存在 404、状态冲突(如重复创建)409、请求过多 429、服务器错误 500、下游不可用 502/503。
|
||||
|
||||
**追问**:参数校验失败用 400 还是 422?→ 两种都常见,团队统一即可;关键是错误响应格式统一、带上具体的字段错误信息。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】JWT 和 Session 有什么区别?JWT 怎么实现退出登录?
|
||||
@@ -46,24 +52,36 @@ week: [1, 2, 3, 16]
|
||||
|
||||
**参考答案**:Gin 用对象池复用 `Context`,请求处理结束后它会被重置给下一个请求使用。如果后台 goroutine 还持有原来的 `c`,读到的可能是别的请求的数据。`c.Copy()` 生成只读副本。更好的做法是只把需要的值(用户 ID、请求 ID、`c.Request.Context()` 派生的 context)传给后台任务。
|
||||
|
||||
**追问**:goroutine 里需要请求的取消信号怎么办?→ 后台任务通常不应该跟随请求取消(请求结束就被取消),用 `context.WithoutCancel(c.Request.Context())` 或新的 context 并设置独立超时。
|
||||
|
||||
### 【进阶】如何设计统一的错误处理?
|
||||
|
||||
**参考答案**:定义业务错误类型(HTTP 状态 + 业务码 + 用户提示),处理函数通过 `c.Error(err)` 记录错误并返回,由一个错误中间件统一转换成标准响应;未知错误和 panic 统一返回通用提示,详细信息带请求 ID 写日志。详见本技术栈的场景文档。
|
||||
|
||||
**追问**:错误码和 HTTP 状态码是什么关系?→ HTTP 状态码表达大类(客户端错误 / 服务端错误),业务错误码表达具体原因,方便前端展示和排查;不要所有错误都返回 200。
|
||||
|
||||
### 【进阶】Gin 的路由为什么快?
|
||||
|
||||
**参考答案**:Gin 使用 httprouter 风格的前缀树(radix tree),每个 HTTP 方法一棵树,按路径公共前缀压缩存储,匹配时间与路径长度相关,基本不受路由数量影响;同时 `Context` 用对象池减少内存分配。
|
||||
|
||||
**追问**:路由冲突(例如 `/users/:id` 和 `/users/new`)会怎样?→ 新版本的 Gin 已经支持静态路由和参数路由共存,静态路由优先;旧版本会在注册时 panic。以使用的版本为准,写测试验证。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】接口被恶意刷,怎么在 Gin 里做限流?
|
||||
|
||||
**参考答案**:单实例用令牌桶(`golang.org/x/time/rate`),按用户 ID 或 IP 维护一个限流器 map(注意定期清理);多实例部署需要全局限流,用 Redis(固定窗口 `INCR+EXPIRE` 或滑动窗口 ZSet、Lua 脚本保证原子)。超限返回 429 并带 `Retry-After`。更外层可以在 Nginx 或网关做 IP 级限流,应用层做用户级、接口级限流。本计划第 3 周项目一实现过。
|
||||
|
||||
**追问**:多实例部署时限流怎么做?→ 单机限流(`x/time/rate`)只能限制单个实例;全局限流要用 Redis(滑动窗口 / 令牌桶 + Lua 原子执行)或在网关层统一做。
|
||||
|
||||
### 【场景题】一个上传接口偶尔导致服务内存暴涨,怎么处理?
|
||||
|
||||
**参考答案**:限制请求体大小(`http.MaxBytesReader` 或 Gin 的 `MaxMultipartMemory`,Nginx 的 `client_max_body_size`);大文件流式处理而不是一次读入内存;校验文件类型和大小;上传改成前端直传对象存储(预签名 URL),后端只接收回调。
|
||||
|
||||
**追问**:大文件上传怎么做更好?→ 前端直传对象存储(服务端签发预签名 URL),服务端只处理元数据;需要服务端处理时用流式读取,不要整个读进内存。
|
||||
|
||||
### 【场景题】怎么给 Gin 服务写接口测试?
|
||||
|
||||
**参考答案**:把路由组装放在一个函数里(如 `New(deps) *gin.Engine`),测试时注入内存版依赖;用 `httptest.NewRecorder()` 和 `r.ServeHTTP(w, req)` 直接调用,不启动端口;用表格驱动覆盖成功、参数错误、未登录、无权限、不存在、内部错误等情况;数据库相关的用集成测试单独跑。示例见 [examples/unified-api](examples/unified-api/)。
|
||||
|
||||
**追问**:测试需要数据库怎么办?→ 业务逻辑通过接口依赖存储层,单元测试用假实现;集成测试用真实数据库(测试库或容器),每个测试使用独立的数据并清理。
|
||||
@@ -32,6 +32,8 @@ week: [1, 3, 16]
|
||||
|
||||
**参考答案**:需要修改接收者、或者结构体很大(避免拷贝)时用指针接收者;小的、不可变的值类型可以用值接收者。同一个类型的方法风格保持一致。注意:指针接收者的方法只属于 `*T` 的方法集,所以只有 `*T` 实现了相关接口。
|
||||
|
||||
**追问**:同一个类型的方法接收者可以混用吗?→ 语法上可以,但一般建议统一:只要有一个方法需要指针接收者,就全部用指针接收者,避免实现接口时出现困惑。
|
||||
|
||||
### 【基础】Go 为什么没有异常?错误怎么处理?
|
||||
|
||||
**参考答案**:Go 把错误当成普通返回值,强制调用方显式处理,控制流清楚。用 `fmt.Errorf("...: %w", err)` 包装错误加上下文,用 `errors.Is` / `errors.As` 判断。`panic` 只用于不可恢复的错误,HTTP 服务用中间件 `recover` 兜底。
|
||||
@@ -62,10 +64,14 @@ week: [1, 3, 16]
|
||||
|
||||
**参考答案**:在调用链上传递取消信号、截止时间和请求范围的值。HTTP 请求结束或超时,所有带这个 context 的数据库查询、下游调用都会及时停止,避免资源浪费。规范:作为第一个参数传递、不要存进结构体、不要传 nil(用 `context.TODO()`)、`WithValue` 只放请求范围的元数据(如请求 ID),不放业务参数。
|
||||
|
||||
**追问**:context 里适合存什么值?→ 只存请求范围的元数据(请求 ID、用户身份、追踪信息),不要用它传递业务参数;key 用自定义的未导出类型,避免冲突。
|
||||
|
||||
### 【进阶】什么是数据竞争?怎么发现和避免?
|
||||
|
||||
**参考答案**:多个 goroutine 同时访问同一变量且至少一个是写,又没有同步。用 `go test -race` / `go run -race` 发现(只能发现实际执行到的路径)。避免:用 channel 传递所有权、`sync.Mutex` / `RWMutex` 加锁、`sync/atomic` 原子操作;并发读写 map 会直接 fatal。
|
||||
|
||||
**追问**:`-race` 能在生产环境开吗?→ 一般不开:会显著增加 CPU 和内存开销。在测试和预发环境开启,CI 中跑 `go test -race`。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】一个接口要并发调用 3 个下游服务,整体 2 秒超时,任一失败就返回错误,怎么写?
|
||||
@@ -83,3 +89,5 @@ week: [1, 3, 16]
|
||||
### 【场景题】服务发布时如何做到不丢请求?
|
||||
|
||||
**参考答案**:优雅关机:监听 SIGTERM,调用 `http.Server.Shutdown(ctx)` 停止接收新连接并等待处理中的请求完成(带超时);后台 worker 通过 context 收到取消信号,处理完当前任务再退出;最后关闭数据库连接池。部署侧配合负载均衡的健康检查,先摘流量再停进程。在本计划第 1 周第 7 天实现过。
|
||||
|
||||
**追问**:Kubernetes 环境下还要注意什么?→ 收到 SIGTERM 后先让就绪探针失败,等待负载均衡摘掉流量(或配置 preStop 等待),再关闭服务;`terminationGracePeriodSeconds` 要大于程序的优雅退出超时。
|
||||
@@ -18,46 +18,68 @@ week: [14, 16]
|
||||
|
||||
**参考答案**:gRPC 基于 HTTP/2,用 Protobuf 二进制序列化,有 IDL 和代码生成,强类型契约,支持流式调用;REST 通常基于 HTTP/1.1 + JSON,可读性好、浏览器原生支持、生态通用。内部服务之间、追求性能和契约时用 gRPC;对外开放、前端直接调用用 REST。
|
||||
|
||||
**追问**:浏览器能直接调用 gRPC 吗?→ 不能直接调用(浏览器无法完全控制 HTTP/2 帧),需要 gRPC-Web + 代理,或者网关把 gRPC 转成 HTTP/JSON(如 grpc-gateway)。
|
||||
|
||||
### 【基础】gRPC 有哪几种调用方式?
|
||||
|
||||
**参考答案**:一元调用(一请求一响应)、服务端流(一请求多响应,如推送进度)、客户端流(多请求一响应,如分片上传)、双向流(如聊天)。
|
||||
|
||||
**追问**:流式调用适合什么场景?→ 服务端流适合推送进度、大模型逐字输出;客户端流适合上传大量数据;双向流适合实时交互。流式调用的负载均衡和重试比一元调用复杂。
|
||||
|
||||
### 【基础】Protobuf 字段编号有什么规则?
|
||||
|
||||
**参考答案**:编号决定序列化后的字段标识,发布后不能修改、不能复用;删除字段用 `reserved` 保留编号和名字;新增字段使用新编号,对老客户端兼容(老客户端会忽略未知字段)。1~15 编码只占一个字节,留给高频字段。
|
||||
|
||||
**追问**:删除一个字段要怎么做?→ 不要复用它的编号和名称,用 `reserved` 声明保留,防止以后被误用导致新旧版本解析错乱。
|
||||
|
||||
### 【基础】微服务相比单体有什么优缺点?
|
||||
|
||||
**参考答案**:优点是独立部署、独立扩容、技术栈可以不同、故障隔离、团队可以独立开发。缺点是网络调用带来的延迟和失败、分布式事务和数据一致性、运维和监控复杂度、调试和排查困难。团队小、业务早期通常模块化单体更合适。
|
||||
|
||||
**追问**:团队只有几个人,适合微服务吗?→ 一般不适合。先做模块化单体(模块边界清晰、独立的数据访问层),等团队和业务规模需要独立部署和扩展时再拆。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】什么是服务雪崩?怎么防止?
|
||||
|
||||
**参考答案**:一个下游服务变慢,调用方的线程 / 连接被占住,进而调用方也变慢,最终一层层传导导致整个系统不可用。防止:所有调用设超时;熔断故障的下游;限流保护自己;降级非核心功能;隔离资源(不同下游用不同的连接池 / 并发限制);重试要有限次数并退避。
|
||||
|
||||
**追问**:限流和熔断的区别?→ 限流保护自己,限制进入的请求量;熔断保护调用方,下游故障时快速失败,不再持续调用。
|
||||
|
||||
### 【进阶】讲一下熔断器的状态机。
|
||||
|
||||
**参考答案**:关闭状态正常放行并统计失败;失败达到阈值(次数或比例)后打开,直接返回失败;冷却时间后进入半开,放少量请求试探;试探成功则关闭,失败则重新打开。只把下游故障(超时、不可用)计为失败,业务错误不算。
|
||||
|
||||
**追问**:半开状态放行多少请求?→ 少量探测请求(一个或几个),成功率达标才关闭熔断器,否则重新打开;具体阈值根据业务配置。
|
||||
|
||||
### 【进阶】重试要注意什么?
|
||||
|
||||
**参考答案**:只重试可重试的错误(如 `Unavailable`)和幂等操作;指数退避加随机抖动;限制次数,并受整体截止时间约束;只在一层做重试,避免逐层放大;熔断打开时不重试;非幂等操作需要幂等键。
|
||||
|
||||
**追问**:什么样的请求可以重试?→ 幂等的请求,以及明确可重试的错误(超时、连接失败、`UNAVAILABLE`);非幂等请求要配合幂等键才能重试。
|
||||
|
||||
### 【进阶】分布式事务有哪些方案?怎么选?
|
||||
|
||||
**参考答案**:2PC 强一致但性能差、协调者单点,很少用于互联网业务;TCC 通过 Try/Confirm/Cancel 预留资源,一致性较好但业务侵入大;Saga 把长事务拆成本地事务 + 补偿,适合流程长的业务;可靠消息(发件箱 + MQ + 幂等)实现最终一致,最常用。一般优先“可靠消息 + 对账”,对资金等强一致场景再考虑 TCC。
|
||||
|
||||
**追问**:Saga 的补偿失败怎么办?→ 补偿操作本身要幂等、可重试;多次失败后进入人工处理队列并告警,所有步骤记录状态便于对账。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】订单服务调用库存服务,库存服务偶尔超时,应该怎么处理?
|
||||
|
||||
**参考答案**:设置合理的超时(小于订单接口自身的超时);扣库存接口要求幂等(带订单号作为幂等键),这样超时后可以安全重试一两次;超时结果不确定时,不能直接认为失败——通过查询接口或后续对账确认;持续超时时熔断,下单接口返回“系统繁忙”;排查库存服务本身的慢查询或资源问题。
|
||||
|
||||
**追问**:超时时间怎么设置?→ 根据下游的 P99 延迟设定,并且上游总超时要大于下游超时之和(或通过 deadline 传递剩余时间),避免上游已经放弃,下游还在执行。
|
||||
|
||||
### 【场景题】团队想把单体系统改成微服务,你怎么推进?
|
||||
|
||||
**参考答案**:先明确要解决的问题,能用模块化、读写分离、缓存解决的不急于拆;选边界清晰、需求明确的模块先拆;用绞杀者模式:先在单体内抽接口,再实现远程服务,用开关灰度切流,随时可回退;数据分库通过双写或 CDC 迁移并校验;同时建设超时熔断、链路追踪、集中日志等基础设施。详见[单体拆分场景](scenarios/monolith-split.md)。
|
||||
|
||||
**追问**:拆分时数据怎么迁移?→ 先拆代码不拆库,再拆库:新服务通过接口访问数据;数据迁移用双写或 CDC 同步,校验一致后切换读,再切换写。
|
||||
|
||||
### 【场景题】一个请求经过 5 个服务,用户反馈偶尔很慢,怎么定位?
|
||||
|
||||
**参考答案**:靠链路追踪:入口生成 trace ID,各服务通过 OpenTelemetry 记录 span 并传递上下文,在追踪系统里按耗时筛选慢请求,看是哪一个 span 慢;再结合该服务的日志(带 trace ID)和指标(CPU、GC、连接池、数据库慢查询)找原因。没有追踪系统时,至少要在日志里统一打印请求 ID。
|
||||
|
||||
**追问**:没有链路追踪系统怎么办?→ 至少在入口生成请求 ID,所有服务透传并写进日志,用日志平台按请求 ID 搜索,并在每个服务记录调用耗时。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: backend.java-spring.index
|
||||
title: Java / Spring Boot(了解级)
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [java-spring]
|
||||
tags: []
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# Java / Spring Boot(了解级)
|
||||
|
||||
了解级:全栈岗位常见要求,能读懂代码、知道分层和常用注解。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: backend.mongodb.index
|
||||
title: MongoDB
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [mongodb]
|
||||
tags: []
|
||||
week: [14]
|
||||
---
|
||||
|
||||
# MongoDB
|
||||
|
||||
文档模型、查询与聚合、索引、何时选 MongoDB 而不是关系库。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
@@ -18,46 +18,68 @@ week: [14, 16]
|
||||
|
||||
**参考答案**:异步(非核心步骤不阻塞主流程,接口更快)、解耦(新增下游不用改上游)、削峰(突发流量先进队列,按能力消费)。问题:系统更复杂,多了一个要维护的组件;消息可能重复、丢失、乱序,要处理;数据变成最终一致;排查链路变长。
|
||||
|
||||
**追问**:什么情况下不该用消息队列?→ 调用方需要立即拿到结果、链路简单、数据量小的场景,用同步调用更简单;引入 MQ 的复杂度要和收益匹配。
|
||||
|
||||
### 【基础】RabbitMQ 有哪几种交换机?
|
||||
|
||||
**参考答案**:direct(routing key 完全匹配)、topic(通配符匹配,`*` 匹配一个词、`#` 匹配零个或多个词)、fanout(广播给所有绑定队列)、headers(按消息头匹配)。
|
||||
|
||||
**追问**:topic 交换机中 `order.*` 和 `order.#` 有什么区别?→ `*` 只匹配一个词,`order.*` 匹配 `order.created`,不匹配 `order.paid.notify`;`#` 匹配零个或多个词,两者都匹配。
|
||||
|
||||
### 【基础】什么是死信队列?
|
||||
|
||||
**参考答案**:消息被拒绝且不重新入队、TTL 过期、队列达到长度上限时,会被转发到队列配置的死信交换机,再路由到死信队列。用于集中处理失败消息,也可以和 TTL 组合实现延迟消息。
|
||||
|
||||
**追问**:死信队列里的消息怎么处理?→ 告警 + 人工分析原因;修复后由运维工具重新投递到原队列;记录处理结果,避免无限循环投递。
|
||||
|
||||
### 【基础】Kafka 的分区和消费组是什么关系?
|
||||
|
||||
**参考答案**:主题分成多个分区;同一个消费组里,一个分区同一时间只能被一个消费者消费,一个消费者可以消费多个分区;不同消费组互不影响、各自消费全部消息。所以组内并行度上限是分区数。
|
||||
|
||||
**追问**:消费者数量多于分区数会怎样?→ 多出来的消费者空闲,不会分到分区;想提高并行度要增加分区数(Kafka 分区只能增加不能减少,且会影响按 key 的顺序)。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】怎么保证消息不丢失?
|
||||
|
||||
**参考答案**:分三段看。生产端:开启确认(RabbitMQ publisher confirm、Kafka `acks=all`),失败重试,必要时用发件箱模式保证和数据库事务一致。broker:持久化 + 多副本(RabbitMQ quorum 队列;Kafka 副本数 >=3、`min.insync.replicas>=2`)。消费端:处理完成后再手动 ack / 提交位移。这样是“至少一次”,消费端还要幂等。
|
||||
|
||||
**追问**:发件箱模式的轮询会不会重复投递?→ 会,所以是“至少一次”;消费端必须幂等。多实例轮询时用 `FOR UPDATE SKIP LOCKED` 避免重复取同一批。
|
||||
|
||||
### 【进阶】怎么保证消费幂等?
|
||||
|
||||
**参考答案**:利用业务状态的条件更新(如 `WHERE status='pending'`);或者用消息 ID 去重表,在同一个事务里插入去重记录(唯一约束)并执行业务;或者在业务表上建唯一约束(如同一订单只发一次积分)。Redis 去重可以作为前置过滤,但关键业务要以数据库约束为准。
|
||||
|
||||
**追问**:唯一约束和 Redis 去重怎么选?→ 数据库唯一约束(和业务操作同一个事务)最可靠;Redis 去重性能高但和业务操作不在一个事务里,要处理“记录了去重但业务失败”的情况。
|
||||
|
||||
### 【进阶】怎么保证消息顺序?
|
||||
|
||||
**参考答案**:Kafka 只保证分区内有序,把需要有序的消息用同一个 key(如订单 ID)发到同一分区,消费端单线程处理该分区或按 key 分发到固定线程。RabbitMQ 同一个队列单个消费者时有序,多个消费者并发时无序,可按 key 拆到多个队列。另外也可以在消息里带版本号 / 状态,消费端丢弃过期的消息,降低对顺序的依赖。
|
||||
|
||||
**追问**:全局严格有序需要怎么做?→ 只能用单分区、单消费者,吞吐会受限;绝大多数业务只需要“同一订单 / 同一用户”内有序,按 key 分区即可。
|
||||
|
||||
### 【进阶】Kafka 为什么吞吐量高?
|
||||
|
||||
**参考答案**:顺序写磁盘(追加日志);利用操作系统页缓存;零拷贝(sendfile)发送数据给消费者;生产和消费都支持批量和压缩;分区让读写可以分布到多台 broker 并行。
|
||||
|
||||
**追问**:Kafka 的消息会丢吗?→ 配置不当会:`acks=1` 时 leader 宕机可能丢;消费端先提交位移再处理,处理失败会丢。可靠配置见“怎么保证消息不丢失”。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】下单后要扣库存、发积分、发通知,怎么保证订单和消息的一致性?
|
||||
|
||||
**参考答案**:用发件箱模式:订单和待发消息在同一事务里写入数据库,后台程序(或 CDC)把消息投递到 MQ,成功后标记;下游消费者幂等。这样订单提交了消息一定会发出(至少一次)。如果是强一致的跨服务操作(比如扣库存必须和下单同时成功),可以用 TCC 或 Saga 补偿,但复杂度高,大部分业务用“可靠消息 + 最终一致 + 对账”。
|
||||
|
||||
**追问**:为什么不直接在业务事务里发消息?→ 发消息是网络调用,事务提交和消息发送无法原子化:先发后提交可能发了消息事务却回滚,先提交后发可能发送失败。发件箱表把消息和业务数据放在同一个事务里。
|
||||
|
||||
### 【场景题】线上消息积压了几百万条,怎么处理?
|
||||
|
||||
**参考答案**:先看消费者是否正常(是否卡在某条毒消息、下游是否变慢、是否频繁再均衡)。修复问题后扩容消费者(Kafka 要看分区数是否够),提高消费并发、批量处理。积压特别大时,写一个临时程序把消息快速转存到分区更多的新主题,用大量消费者并行处理。同时评估:过期没有意义的消息(如过时的通知)可以跳过。事后加上积压监控告警。
|
||||
|
||||
**追问**:临时扩容消费者没用是为什么?→ 消费者数量已达到分区数上限,或者瓶颈在下游(数据库、第三方接口)。要先找到真正的瓶颈。
|
||||
|
||||
### 【场景题】订单 30 分钟未支付自动取消,怎么设计?
|
||||
|
||||
**参考答案**:RabbitMQ 用 TTL + 死信队列(或延迟插件)发送延迟消息;消费者用条件更新 `WHERE status='pending'` 取消并在同一事务回补库存,和支付回调天然互斥、重复消息幂等;保留低频的兜底扫描任务防止消息丢失。详见[延迟取消场景](scenarios/delayed-order-cancel.md)。
|
||||
|
||||
**追问**:订单在取消前被支付了怎么办?→ 取消时用条件更新(`WHERE status = '待支付'`),状态已变则什么都不做;支付回调同样用条件更新,两者竞争时只有一个成功。
|
||||
@@ -20,6 +20,8 @@ week: [2, 14, 16]
|
||||
|
||||
**参考答案**:B+ 树的非叶子节点只存键,一个页能放更多键,树更矮,磁盘 IO 次数少;所有数据在叶子节点且叶子之间用链表相连,范围查询和排序高效。红黑树是二叉树,数据多时太高;哈希只支持等值查询,不支持范围和排序。
|
||||
|
||||
**追问**:一棵 3 层的 B+ 树大约能存多少行?→ 粗略估算:16KB 页,非叶子节点每条约十几字节,可放一千多个指针;叶子页按每行 1KB 放 16 行,约 1000 × 1000 × 16 ≈ 两千万行。这是数量级估算,实际取决于行大小。
|
||||
|
||||
### 【基础】什么是聚簇索引、回表和覆盖索引?
|
||||
|
||||
**参考答案**:InnoDB 的主键索引叶子节点存整行数据,叫聚簇索引;二级索引叶子节点存索引列和主键值。通过二级索引找到主键再去主键索引取整行叫回表。如果查询需要的列都在二级索引里,就不用回表,叫覆盖索引(EXPLAIN 的 Extra 显示 Using index)。
|
||||
@@ -30,42 +32,62 @@ week: [2, 14, 16]
|
||||
|
||||
**参考答案**:联合索引 `(a, b, c)` 先按 a 排序,a 相同再按 b,依此类推。查询条件要从 a 开始连续使用才能利用索引定位;只用 b 或 c 用不上;中间遇到范围条件(如 `a > 1`)后,后面的列一般不能再用于定位(可以靠索引下推过滤)。
|
||||
|
||||
**追问**:`WHERE b = 1 AND a = 2` 能用上 (a, b) 索引吗?→ 能,优化器会调整条件顺序;最左前缀看的是条件中是否包含最左列,不看书写顺序。
|
||||
|
||||
### 【基础】有哪些常见的索引失效情况?
|
||||
|
||||
**参考答案**:索引列上用函数或运算;隐式类型转换(字符串列用数字比较);`LIKE` 以 `%` 开头;不满足最左前缀;`OR` 连接的列有的没索引;优化器判断走索引不如全表扫描(区分度低、要扫的行太多)。
|
||||
|
||||
**追问**:怎么确认索引有没有用上?→ 用 `EXPLAIN` 看 `type`、`key`、`rows`、`Extra`;MySQL 8.0.18+ 可以用 `EXPLAIN ANALYZE` 看实际执行情况。
|
||||
|
||||
### 【基础】redo log、undo log、binlog 分别是什么?
|
||||
|
||||
**参考答案**:redo log 是 InnoDB 的物理日志,记录“某页做了什么修改”,保证崩溃后已提交的事务不丢(持久性);undo log 记录修改前的值,用于回滚和 MVCC 读旧版本(原子性、隔离性);binlog 是 Server 层的逻辑日志,用于主从复制和按时间点恢复。redo log 和 binlog 用两阶段提交保证一致。
|
||||
|
||||
**追问**:为什么需要两阶段提交?→ redo log 和 binlog 是两份日志,必须保证一致:否则崩溃恢复后主库数据和从库(通过 binlog 复制)不一致。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】InnoDB 的可重复读是怎么实现的?能解决幻读吗?
|
||||
|
||||
**参考答案**:普通 SELECT 是快照读,事务第一次读时生成 Read View,之后一直用它判断数据版本是否可见,所以多次读结果一致,也看不到别人新插入的行。当前读(`FOR UPDATE`、`UPDATE`)读最新数据,通过临键锁(记录锁 + 间隙锁)阻止其他事务在范围内插入,从而避免幻读。但如果事务里先快照读、再当前读,仍可能看到不一致,所以不能说“完全解决”。
|
||||
|
||||
**追问**:间隙锁会带来什么问题?→ 锁住的范围比实际修改的行大,并发插入会被阻塞,也更容易死锁;如果业务能接受,可以使用读已提交隔离级别减少间隙锁。
|
||||
|
||||
### 【进阶】什么情况下会死锁?怎么排查和避免?
|
||||
|
||||
**参考答案**:两个事务以不同顺序锁定相同的资源,互相等待。InnoDB 会检测并回滚代价小的那个事务(错误 1213)。排查:`SHOW ENGINE INNODB STATUS` 的 LATEST DETECTED DEADLOCK 段,看两个事务各自持有和等待的锁。避免:固定加锁顺序(如按 id 排序)、缩小事务、更新条件走索引减少锁范围、必要时降低到读已提交减少间隙锁;应用层捕获死锁并重试。
|
||||
|
||||
**追问**:死锁发生后 MySQL 怎么处理?→ InnoDB 检测到死锁后回滚代价较小的事务,另一个继续执行;应用层要捕获死锁错误并重试整个事务。
|
||||
|
||||
### 【进阶】主从延迟的原因和处理方式?
|
||||
|
||||
**参考答案**:原因:大事务、从库重放并行度低、从库负载高或配置差、网络。处理:拆分大事务、开启并行复制、关键读走主库(写后一段时间内读主库)、监控延迟超过阈值时把读流量切回主库。
|
||||
|
||||
**追问**:刚写入就读不到,业务上怎么处理?→ 写后立即读的请求走主库(或一段时间内该用户的读走主库);或者前端直接使用写接口的返回结果。
|
||||
|
||||
### 【进阶】什么时候需要分库分表?分片键怎么选?
|
||||
|
||||
**参考答案**:单表数据量大到索引优化后仍然慢、或单库写入成为瓶颈,并且归档、缓存、读写分离都用过之后再考虑。分片键选最常用的查询维度(如用户 ID),让大部分请求只访问一个分片,并且数据分布均匀。代价是跨分片查询、分页、事务、全局 ID 和扩容迁移都变复杂。
|
||||
|
||||
**追问**:分库分表后跨分片查询怎么办?→ 尽量通过分片键路由;后台统计类查询同步到 ES 或数仓;避免跨分片 JOIN 和分布式事务。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】“先查库存再扣减”在高并发下超卖了,怎么改?
|
||||
|
||||
**参考答案**:查和扣两步之间有并发窗口。改成一条条件更新:`UPDATE stock SET num = num - 1 WHERE sku_id = ? AND num >= 1`,判断影响行数;或者 `SELECT ... FOR UPDATE` 加锁后再扣。并发非常高时(秒杀),数据库行锁会成为瓶颈,在 Redis 里预扣库存、消息队列异步落库(见 Redis 专题的库存场景和跨栈场景“秒杀”)。
|
||||
|
||||
**追问**:用乐观锁还是 `UPDATE ... WHERE stock >= 1`?→ 扣库存这种只需要一个条件的场景,直接条件更新最简单;需要读出数据做复杂计算后再写入的,用版本号乐观锁。
|
||||
|
||||
### 【场景题】一张几千万行的表,分页越往后越慢,怎么优化?
|
||||
|
||||
**参考答案**:`LIMIT offset` 需要扫描并丢弃前面的行。改为游标分页(记住上一页最后一条的排序键和 id);需要跳页时用延迟关联并限制最大页数;导出走异步任务;历史数据归档。详见[深分页场景](scenarios/deep-pagination.md)。
|
||||
|
||||
**追问**:用户要求可以跳转到任意页,怎么办?→ 深分页限制最大页数;或者用延迟关联(先在覆盖索引上查主键,再回表);超深分页的需求通常可以引导为搜索 / 筛选。
|
||||
|
||||
### 【场景题】线上一个接口突然变慢,数据库 CPU 很高,你怎么排查?
|
||||
|
||||
**参考答案**:先 `SHOW PROCESSLIST` 看正在执行的慢语句和状态(是否大量 Waiting for lock);查慢查询日志按总耗时排序;对可疑 SQL 做 EXPLAIN,看是不是最近上线的新语句、索引被删、统计信息不准导致走错索引;有锁等待时查 `information_schema` / `performance_schema` 中的锁信息找到阻塞源。处理上可以先限流或下线问题功能止血,再加索引或改 SQL,最后复盘加监控。
|
||||
|
||||
**追问**:怎么防止慢查询再次发生?→ 上线前审查 SQL 和 `EXPLAIN`;开启慢查询日志并告警;对大表的变更进行评审。
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
id: backend.nodejs.index
|
||||
title: Node.js 后端(Express / NestJS)
|
||||
type: index
|
||||
domain: backend
|
||||
stack: [nodejs]
|
||||
tags: []
|
||||
week: [7, 14]
|
||||
---
|
||||
|
||||
# Node.js 后端(Express / NestJS)
|
||||
|
||||
了解级:前端出身的后端选项,Express 中间件、NestJS 模块与依赖注入。
|
||||
|
||||
> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。
|
||||
@@ -20,22 +20,32 @@ week: [2, 16]
|
||||
|
||||
**参考答案**:`WHERE` 在分组前过滤行,不能使用聚合函数;`HAVING` 在 `GROUP BY` 之后过滤分组,可以使用聚合函数。能用 `WHERE` 过滤的尽量用 `WHERE`,减少参与分组的数据量。
|
||||
|
||||
**追问**:`WHERE` 中能使用 SELECT 里定义的别名吗?→ 不能,因为 `WHERE` 在 `SELECT` 之前执行;`ORDER BY` 可以使用别名。
|
||||
|
||||
### 【基础】`INNER JOIN` 和 `LEFT JOIN` 的区别?LEFT JOIN 的条件写在 ON 和 WHERE 里有什么不同?
|
||||
|
||||
**参考答案**:内连接只保留两边都匹配的行;左连接保留左表所有行,右表没匹配的列为 NULL。右表的过滤条件写在 `ON` 里时,不匹配的左表行仍然保留;写在 `WHERE` 里会把右表为 NULL 的行过滤掉,效果等同内连接。
|
||||
|
||||
**追问**:怎么查询“没有下过订单的用户”?→ `LEFT JOIN orders ... WHERE orders.id IS NULL`,或者 `NOT EXISTS (SELECT 1 FROM orders WHERE ...)`。
|
||||
|
||||
### 【基础】为什么 `NOT IN` 遇到 NULL 会出问题?
|
||||
|
||||
**参考答案**:`x NOT IN (a, b, NULL)` 等价于 `x <> a AND x <> b AND x <> NULL`,而 `x <> NULL` 的结果是 UNKNOWN,整个条件永远不为真,查询返回空。用 `NOT EXISTS` 或先过滤掉 NULL。
|
||||
|
||||
**追问**:`NOT EXISTS` 为什么没有这个问题?→ `NOT EXISTS` 只判断子查询是否有行,不对值做三值逻辑比较,NULL 不会影响结果。
|
||||
|
||||
### 【基础】金额应该用什么类型?时间呢?
|
||||
|
||||
**参考答案**:金额用 `NUMERIC(p, s)` 或以“分”为单位的整数,不用浮点数(有精度误差)。时间用 `TIMESTAMPTZ`,统一存 UTC,展示时按用户时区转换。
|
||||
|
||||
**追问**:`timestamptz` 存储的是什么?→ 存储的是 UTC 时间点,显示时按会话时区转换;`timestamp`(不带时区)只是一个“墙上时间”,不知道是哪个时区。
|
||||
|
||||
### 【基础】什么是 UPSERT?为什么比“先查再插”安全?
|
||||
|
||||
**参考答案**:`INSERT ... ON CONFLICT (唯一键) DO UPDATE / DO NOTHING`,一条语句完成“有则更新、无则插入”。它依赖唯一约束在数据库内部原子判断;“先查再插”在并发下两个请求都可能查不到然后都插入。
|
||||
|
||||
**追问**:`ON CONFLICT DO NOTHING` 怎么知道是否插入了?→ 加 `RETURNING id`,没有返回行就说明冲突了。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】PostgreSQL 的 MVCC 是怎么实现的?VACUUM 是干什么的?
|
||||
@@ -48,24 +58,36 @@ week: [2, 16]
|
||||
|
||||
**参考答案**:默认读已提交。可重复读下,事务看到的是开始时的快照;如果要更新的行在快照之后被其他事务修改并提交了,会报 `could not serialize access due to concurrent update`(SQLSTATE 40001),应用需要重试整个事务。
|
||||
|
||||
**追问**:可重复读下序列化失败怎么处理?→ 应用层捕获错误码 `40001`,重试整个事务;事务尽量短,减少冲突。
|
||||
|
||||
### 【进阶】一条 SQL 很慢,你怎么排查?
|
||||
|
||||
**参考答案**:先用 `EXPLAIN (ANALYZE, BUFFERS)` 看执行计划:是不是全表扫描、预估行数和实际行数是否差距大(统计信息过时就 `ANALYZE`)、哪个节点耗时最多。常见优化:按查询条件建联合索引(注意最左前缀和排序方向)、避免在索引列上用函数、用覆盖索引、改写子查询、分页改成游标(按上一页最后一个 ID 继续)。线上用 `pg_stat_statements` 找出总耗时最高的语句。
|
||||
|
||||
**追问**:`EXPLAIN` 和 `EXPLAIN ANALYZE` 的区别?→ 前者只显示计划和估算;后者真正执行 SQL 并显示实际行数和耗时。对写操作使用 `EXPLAIN ANALYZE` 要放在事务里回滚。
|
||||
|
||||
### 【进阶】`FOR UPDATE SKIP LOCKED` 有什么用?
|
||||
|
||||
**参考答案**:选中并锁定行,遇到已被其他事务锁住的行直接跳过。适合“多个 worker 并发从任务表取任务”:每个 worker 拿到不同的任务,互不阻塞,不需要额外的消息队列。
|
||||
|
||||
**追问**:除了任务队列还能用在哪?→ 多个工作进程分配待处理的数据(如待发送通知、待同步记录),每个进程取到不同的行,互不阻塞。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】订单表有几千万行,按用户查最近订单的分页越往后越慢,怎么优化?
|
||||
|
||||
**参考答案**:`OFFSET` 越大,数据库要扫描并丢弃的行越多。改成游标分页(keyset):`WHERE user_id = ? AND (created_at, id) < (上一页最后一条的值) ORDER BY created_at DESC, id DESC LIMIT 20`,配合 `(user_id, created_at DESC, id DESC)` 联合索引,每页都是索引范围扫描。代价是不能直接跳到第 N 页,通常用“加载更多”交互。更大规模时按时间分区,历史数据归档。
|
||||
|
||||
**追问**:游标分页怎么支持“上一页”?→ 用当前页第一条的排序值反向查询(条件和排序方向都反过来),再把结果倒序。
|
||||
|
||||
### 【场景题】线上要给一张大表加索引和加字段,怎么做才不影响业务?
|
||||
|
||||
**参考答案**:索引用 `CREATE INDEX CONCURRENTLY`(不阻塞写,不能在事务里执行,失败要删掉无效索引重建)。加字段:PostgreSQL 11 起加带常量默认值的列不需要重写表,很快;但要注意 DDL 需要短暂的排他锁,如果前面有长事务会排队并阻塞后续查询,所以设置 `lock_timeout`,在低峰期执行。迁移脚本要向后兼容:先加字段、发新代码,再清理旧字段。
|
||||
|
||||
**追问**:`CREATE INDEX CONCURRENTLY` 失败了怎么办?→ 会留下一个标记为 INVALID 的索引,要先删除再重新创建;它不能在事务块里执行。
|
||||
|
||||
### 【场景题】用户和支付回调同时修改订单状态,怎么保证状态正确?
|
||||
|
||||
**参考答案**:状态流转规则集中定义,修改用带版本号(或旧状态)的条件 UPDATE 做乐观锁,影响行数为 0 时视为冲突,重新读取最新状态再决定;审计日志与状态修改放在同一事务。冲突频繁的场景可以用 `SELECT ... FOR UPDATE` 排队。详见本技术栈的[订单状态机场景](scenarios/order-state-audit.md)。
|
||||
|
||||
**追问**:为什么不用 `SELECT ... FOR UPDATE` 悲观锁?→ 也可以,适合冲突很多的场景;冲突少时乐观锁或条件更新不需要持有锁,并发更好。关键是状态迁移要有条件判断。
|
||||
@@ -18,24 +18,34 @@ week: [3, 14, 16]
|
||||
|
||||
**参考答案**:数据在内存里;核心命令执行是单线程的,没有锁竞争和线程切换;使用 IO 多路复用(epoll)同时处理大量连接;数据结构针对场景做了优化(如压缩列表、跳表)。Redis 6 起网络读写可以多线程,命令执行仍是单线程。
|
||||
|
||||
**追问**:Redis 6 的多线程是怎么回事?→ 多线程只用于网络读写(解析请求、写回响应),命令执行仍然是单线程,所以命令的原子性不变。
|
||||
|
||||
### 【基础】Redis 有哪些数据类型?各举一个使用场景。
|
||||
|
||||
**参考答案**:String(缓存、计数器、验证码)、Hash(对象、购物车)、List(最新消息列表)、Set(标签、去重、共同关注)、ZSet(排行榜、延迟队列)、Stream(轻量消息队列);另有 Bitmap(签到)、HyperLogLog(UV 统计)、Geo(附近的人)。
|
||||
|
||||
**追问**:排行榜为什么用 ZSet?→ 有序集合按分数自动排序,插入和更新都是 O(log N),可以直接按排名范围查询。
|
||||
|
||||
### 【基础】过期的 key 是怎么删除的?内存满了怎么办?
|
||||
|
||||
**参考答案**:惰性删除(访问时发现过期就删)+ 定期删除(周期性抽样检查过期 key)。内存达到 `maxmemory` 时按淘汰策略处理:`noeviction`(拒绝写入)、`allkeys-lru`、`allkeys-lfu`、`volatile-lru`(只淘汰设置了过期的 key)等,缓存场景常用 `allkeys-lru` / `allkeys-lfu`。
|
||||
|
||||
**追问**:大 key 有什么危害?→ 读写和删除都会阻塞主线程、占用网络带宽、集群中导致数据倾斜。删除大 key 用 `UNLINK`(异步删除),并拆分数据结构。
|
||||
|
||||
### 【基础】RDB 和 AOF 的区别?
|
||||
|
||||
**参考答案**:RDB 定时生成内存快照,文件小、恢复快,但可能丢失最后一次快照之后的数据;AOF 追加记录写命令,可配置每秒刷盘,丢数据更少,但文件更大,需要重写压缩。生产常两者都开,Redis 4 起支持混合持久化。
|
||||
|
||||
**追问**:生产环境一般怎么配置持久化?→ 常见是开启 AOF(`everysec`)+ RDB 备份,Redis 4.0+ 可以使用混合持久化;只做缓存的场景可以不开持久化。
|
||||
|
||||
## 进阶
|
||||
|
||||
### 【进阶】缓存穿透、击穿、雪崩分别是什么?怎么解决?
|
||||
|
||||
**参考答案**:穿透是查询不存在的数据,每次都打到数据库——缓存空值、参数校验、布隆过滤器。击穿是热点 key 过期瞬间大量请求同时回源——互斥锁 / singleflight 只让一个请求回源,或逻辑过期后台刷新。雪崩是大量 key 同时过期或 Redis 不可用——过期时间加随机值、Redis 高可用、服务限流降级。
|
||||
|
||||
**追问**:布隆过滤器有什么缺点?→ 存在误判(判断存在时可能不存在),不能删除元素(普通布隆过滤器);数据变化时需要重建。
|
||||
|
||||
### 【进阶】怎么保证缓存和数据库的一致性?
|
||||
|
||||
**参考答案**:常用旁路缓存:先更新数据库,再删除缓存。删除而不是更新,是为了避免并发写导致缓存写入旧值。仍有极小概率不一致,可以用延迟双删、订阅 binlog 异步删除、短 TTL 兜底。缓存只能做到最终一致,要求强一致的数据(余额、库存最终值)以数据库为准。
|
||||
@@ -46,20 +56,30 @@ week: [3, 14, 16]
|
||||
|
||||
**参考答案**:`SET key 随机令牌 NX PX 过期时间` 加锁;用 Lua 脚本比较令牌后删除来解锁,防止误删别人的锁。问题:业务执行时间超过过期时间会导致两个持有者同时存在(续期 / 看门狗);主从异步复制,主节点宕机时锁可能丢失(Redlock 有争议);所以关键业务还要用数据库唯一约束、乐观锁等做最终保证。
|
||||
|
||||
**追问**:锁过期了业务还没执行完怎么办?→ 自动续期(看门狗);业务本身做好幂等和数据库层面的条件更新,不能完全依赖锁的正确性。
|
||||
|
||||
### 【进阶】Redis 的主从、哨兵、集群分别解决什么问题?
|
||||
|
||||
**参考答案**:主从复制解决读扩展和数据冗余;哨兵(Sentinel)监控主节点,故障时自动选新主,解决高可用;Cluster 把数据按 16384 个槽分到多个主节点,解决容量和写扩展,每个主节点也可以有从节点。Cluster 下多 key 操作(包括 Lua)要求 key 在同一个槽,可以用哈希标签 `{}`。
|
||||
|
||||
**追问**:集群模式下多个 key 的操作有什么限制?→ 多 key 命令和 Lua 脚本要求 key 在同一个槽,用哈希标签 `{order}:1` 让相关 key 落在同一个槽。
|
||||
|
||||
## 场景题
|
||||
|
||||
### 【场景题】秒杀场景怎么防止超卖和重复购买?
|
||||
|
||||
**参考答案**:库存提前加载到 Redis;Lua 脚本原子完成“是否已购买 → 库存是否充足 → 扣减 → 记录用户”;成功后发 MQ 异步创建订单,数据库唯一约束兜底;失败或超时未支付回补库存;前端、网关层层限流。详见[库存预扣场景](scenarios/stock-reservation.md)。
|
||||
|
||||
**追问**:Redis 扣了库存但创建订单失败怎么办?→ 回补库存(补偿);或者 Redis 只做预扣,以数据库为准,异步对账修正。
|
||||
|
||||
### 【场景题】线上 Redis 突然变慢,怎么排查?
|
||||
|
||||
**参考答案**:`SLOWLOG GET` 看慢命令(`KEYS *`、大 key 的 `HGETALL` / `DEL`、复杂 Lua);`INFO` 看内存、连接数、命中率、是否在做 RDB / AOF 重写(fork 导致的卡顿);`--bigkeys` / `MEMORY USAGE` 找大 key;检查是否有热点 key 集中在一个节点。处理:改用 `SCAN`、`UNLINK`,拆分大 key,热点 key 加本地缓存。
|
||||
|
||||
**追问**:怎么找出慢命令?→ `SLOWLOG GET` 查看慢日志;`--bigkeys` / `MEMORY USAGE` 找大 key;`INFO` 看内存、客户端数、持久化状态。
|
||||
|
||||
### 【场景题】一个接口要按用户限流,每分钟最多 60 次,多实例部署,怎么做?
|
||||
|
||||
**参考答案**:用 Redis 共享计数。简单做法是固定窗口 `INCR` + 过期时间;要求精确时用 ZSET 滑动窗口(Lua 脚本里删除过期记录、计数、写入);允许突发用令牌桶。返回 429 并带上重试时间。要考虑 Redis 故障时的策略(放行还是拒绝)。详见[分布式锁和限流](03-lock-ratelimit.md)。
|
||||
|
||||
**追问**:固定窗口有什么问题?→ 窗口边界处可能出现 2 倍的突发请求(上一个窗口末尾和下一个窗口开头各 60 次),需要平滑时用滑动窗口或令牌桶。
|
||||
Reference in new issue
Block a user