From 0e67673d7a2ae87b2e36ab96da01355cb5293840 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Fri, 15 May 2026 16:26:14 +0800 Subject: [PATCH] vault backup: 2026-05-15 16:26:14 --- hhs/EXAM/Week01.md | 678 +++++++++ hhs/EXAM/Week02.md | 539 ++++++++ hhs/EXAM/Week03.md | 811 +++++++++++ hhs/EXAM/Week04.md | 804 +++++++++++ hhs/EXAM/Week05.md | 1228 +++++++++++++++++ hhs/Redis/一文吃透/README.md | 601 ++++++++ hhs/finalhomework/hhs执行计划.md | 592 -------- .../3. 服务端实现/08-Server 搭建与注册.md | 4 +- .../00-Unimplemented 零-Stub 模式.md | 197 ++- hzh/DEV/跨域问题调试.md | 3 + .../跨域问题调试/nginx-reverse-proxy-cors.md | 244 ++++ hzh/GIN/11-static-files.md | 28 +- hzh/GIN/12-server-config.md | 2 +- .../{API网关/README.md => 01-API网关.md} | 4 +- .../{安全机制/README.md => 02-安全机制.md} | 4 +- .../{分布式追踪/README.md => 03-分布式追踪.md} | 6 +- .../{服务发现/README.md => 04-服务发现.md} | 6 +- .../{服务间通信/README.md => 05-服务间通信.md} | 6 +- .../{容错模式/README.md => 06-容错模式.md} | 6 +- .../{配置管理/README.md => 07-配置管理.md} | 4 +- .../{流量治理/README.md => 08-流量治理.md} | 12 +- .../{网关鉴权策略.md => 09-网关鉴权策略.md} | 0 hzh/MS/02-服务治理/README.md | 16 +- .../{数据库拆分/README.md => 01-数据库拆分.md} | 2 +- .../{分布式事务/README.md => 02-分布式事务.md} | 6 +- .../{ID生成/README.md => 03-ID生成.md} | 4 +- hzh/MS/03-数据一致性/README.md | 6 +- .../{Metrics监控/README.md => 01-Metrics监控.md} | 6 +- .../{日志系统/README.md => 02-日志系统.md} | 6 +- .../{链路追踪/README.md => 03-链路追踪.md} | 6 +- .../{告警管理/README.md => 04-告警管理.md} | 6 +- hzh/MS/04-可观测性/README.md | 10 +- .../{容器化/README.md => 01-容器化.md} | 4 +- .../{Kubernetes/README.md => 02-Kubernetes.md} | 6 +- .../README.md => 03-CICD与GitOps.md} | 6 +- .../{SRE实践/README.md => 04-SRE实践.md} | 6 +- hzh/MS/05-部署运维/README.md | 8 +- hzh/MS/06-gRPC/01-协议与架构.md | 6 +- hzh/MS/06-gRPC/02-Proto设计.md | 2 +- hzh/MS/06-gRPC/04-拦截器.md | 6 +- hzh/MS/06-gRPC/05-错误处理.md | 4 +- hzh/MS/06-gRPC/06-连接管理.md | 4 +- hzh/MS/06-gRPC/07-最佳实践.md | 4 +- hzh/MS/06-gRPC/README.md | 12 +- hzh/MS/README.md | 38 +- 45 files changed, 5165 insertions(+), 788 deletions(-) create mode 100644 hhs/EXAM/Week01.md create mode 100644 hhs/EXAM/Week02.md create mode 100644 hhs/EXAM/Week03.md create mode 100644 hhs/EXAM/Week04.md create mode 100644 hhs/EXAM/Week05.md create mode 100644 hhs/Redis/一文吃透/README.md delete mode 100644 hhs/finalhomework/hhs执行计划.md create mode 100644 hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors.md rename hzh/MS/02-服务治理/{API网关/README.md => 01-API网关.md} (96%) rename hzh/MS/02-服务治理/{安全机制/README.md => 02-安全机制.md} (96%) rename hzh/MS/02-服务治理/{分布式追踪/README.md => 03-分布式追踪.md} (96%) rename hzh/MS/02-服务治理/{服务发现/README.md => 04-服务发现.md} (96%) rename hzh/MS/02-服务治理/{服务间通信/README.md => 05-服务间通信.md} (96%) rename hzh/MS/02-服务治理/{容错模式/README.md => 06-容错模式.md} (98%) rename hzh/MS/02-服务治理/{配置管理/README.md => 07-配置管理.md} (96%) rename hzh/MS/02-服务治理/{流量治理/README.md => 08-流量治理.md} (93%) rename hzh/MS/02-服务治理/{网关鉴权策略.md => 09-网关鉴权策略.md} (100%) rename hzh/MS/03-数据一致性/{数据库拆分/README.md => 01-数据库拆分.md} (98%) rename hzh/MS/03-数据一致性/{分布式事务/README.md => 02-分布式事务.md} (96%) rename hzh/MS/03-数据一致性/{ID生成/README.md => 03-ID生成.md} (96%) rename hzh/MS/04-可观测性/{Metrics监控/README.md => 01-Metrics监控.md} (95%) rename hzh/MS/04-可观测性/{日志系统/README.md => 02-日志系统.md} (94%) rename hzh/MS/04-可观测性/{链路追踪/README.md => 03-链路追踪.md} (95%) rename hzh/MS/04-可观测性/{告警管理/README.md => 04-告警管理.md} (96%) rename hzh/MS/05-部署运维/{容器化/README.md => 01-容器化.md} (95%) rename hzh/MS/05-部署运维/{Kubernetes/README.md => 02-Kubernetes.md} (96%) rename hzh/MS/05-部署运维/{CI-CD与GitOps/README.md => 03-CICD与GitOps.md} (95%) rename hzh/MS/05-部署运维/{SRE实践/README.md => 04-SRE实践.md} (95%) diff --git a/hhs/EXAM/Week01.md b/hhs/EXAM/Week01.md new file mode 100644 index 0000000..ac040d3 --- /dev/null +++ b/hhs/EXAM/Week01.md @@ -0,0 +1,678 @@ +--- +tags: [Go, 考试, Week01, 武科大服务端] +create time: 2026-05-14 12:00 +--- + +# 金山考试 - 武科大服务端-第一次课 + +## 概述 + +本次考试覆盖 Go 语言基础核心知识点,包括 Git 版本控制基础、字符编码、变量声明、流程控制(`if` / `for` / `switch`)、函数与 `defer`、`panic` / `recover` 错误处理、字符串不可变性等。适合用于检验 Go 入门学习成果。 + +**知识点地图:** + +```mermaid +graph LR + A[Go 语言基础] --> B[Git 基础] + A --> C[字符编码 Unicode / UTF-8] + A --> D[变量与类型 rune / string] + A --> E[流程控制 if for switch] + A --> F[函数 init / defer] + A --> G[错误处理 panic recover] + A --> H[包管理 go mod] +``` + +## 正文 + +### 一、单选题 + +#### 1. Git 查看状态命令 + +【单选题】在 Git 中,如何查看当前状态的最简命令是什么?(5分) + +A. `git log` +B. `git status` +C. `git diff` +D. `git commit` + +**正确答案:**B +**作答答案:**B + +--- + +#### 2. Go 注释方式 + +【单选题】在Go语言中,以下哪种注释方式是正确的?(5分) + +Go 语言支持 `//` 形式的单行注释和 `/* */` 形式的多行注释。 + +A. `// 这是单行注释` +B. `/* 这是多行注释 */` +C. `# 这是单行注释` +D. A和B都正确 + +**正确答案:**D +**作答答案:**D + +> [!tip] 注意 +> Go 不像 Python 或 Shell 脚本使用 `#` 做注释。这是很多初学者容易混淆的地方。 + +--- + +#### 3. 函数可见性(Export) + +【单选题】在Go语言中,如果一个函数需要被外部包访问,函数名应该如何书写?(5分) + +A. 首字母小写并使用 public 关键字 +B. 首字母大写 +C. 完全小写 +D. 使用 func 关键字作为前缀 + +**正确答案:**B +**作答答案:**B + +> [!info] Go 没有 public / private 关键字 +> Go 的包可见性遵循简单规则:**首字母大写 = 导出(公共),首字母小写 = 不导出(私有)**。同样适用于变量、常量、类型等。 + +--- + +#### 4. UTF-8 英文字符字节数 + +【单选题】在UTF-8编码中,一个英文字符通常占用多少字节?(5分) + +A. 1字节 +B. 2字节 +C. 3字节 +D. 4字节 + +**正确答案:**A +**作答答案:**A + +--- + +#### 5. Unicode 与 UTF-8 的关系 + +【单选题】Unicode和UTF-8之间的关系是什么?(5分) + +A. Unicode是一种字符集,UTF-8是Unicode的一种编码方式 +B. Unicode和UTF-8都是编程语言 +C. UTF-8是一种字符集,Unicode是UTF-8的一种编码方式 +D. 没有任何关系 + +**正确答案:**A +**作答答案:**C + +> [!warning] 易错题 — Unicode vs UTF-8 +> - **Unicode**:字符集(Character Set),为每个字符分配唯一编号(码点 Code Point),如 `U+0041` → 'A' +> - **UTF-8**:编码方案(Encoding Scheme),将码点转换成字节序列存储 +> - 三者常见关系:Unicode(字符集)→ UTF-8 / UTF-16 / UTF-32(编码方式) + +**对比表:** + +| 概念 | 类比 | 作用 | +|------|------|------| +| Unicode | 电报码本 | 给每个字符编号 | +| UTF-8 | 传输协议 | 把编号变成可传输的字节 | + +--- + +#### 6. rune 类型用途 + +【单选题】在Go语言中,rune类型通常用于处理什么?(5分) + +A. 整型数据 +B. 浮点数数据 +C. Unicode字符 +D. 布尔型数据 + +**正确答案:**C +**作答答案:**C + +> [!info] rune 的本质 +> `rune` 就是 `int32` 的别名,专门用于表示 Unicode 码点(字符)。在 Go 中遍历字符串时,用 `range` 循环得到的就是 `rune` 类型的单个字符。 + +--- + +#### 7. defer 与命名返回值 + +【单选题】下面代码能编译通过吗?可以的话,输出什么?(5分) + +```go +func DeferTest2(i int) (r int) { + defer func() { + r += i + }() + return 2 +} + +func main() { + fmt.Println(DeferTest2(1)) +} +``` + +A. 1 +B. 2 +C. 3 +D. 以上都不是 + +**正确答案:**C +**作答答案:**C + +> [!important] defer + 命名返回值陷阱 +> `defer` 闭包捕获的是**命名返回值的引用**。执行流程: +> 1. `return 2` 先将 `r` 赋值为 `2` +> 2. `defer` 函数执行,`r += 1` → `r` 变为 `3` +> 3. 函数返回 `r = 3` +> +> **关键记忆:有命名返回值时,defer 修改它会影响最终返回值。** + +```mermaid +sequenceDiagram + participant Main as main + participant Func as DeferTest2(1) + participant Defer as defer closure + Main->>Func: 调用 DeferTest2(1) + Func->>Func: return 2 → r = 2 + Func->>Defer: 执行 defer: r += 1 + Defer-->>Func: r = 3 + Func-->>Main: 返回 3 +``` + +--- + +#### 8. fallthrough 语义 + +【单选题】下面代码输出什么?(5分) + +```go +func main() { + isMatch := func(i int) bool { + switch(i) { + case 1: + fallthrough + case 2: + return true + } + return false + } + fmt.Println(isMatch(1)) + fmt.Println(isMatch(2)) +} +``` + +A. `false true` +B. `true true` +C. `false false` +D. `true false` + +**正确答案:**B +**作答答案:**B + +> [!tip] fallthrough 的执行逻辑 +> - `case 1:` 后跟 `fallthrough` → 跳过 case 1 剩余代码,直接进入 case 2 并执行 +> - case 2 中有 `return true` → 所以传入 1 也返回 true +> - 传入 2 → 直接命中 case 2 → 返回 true +> - 因此两次输出均为 `true` + +```mermaid +flowchart TD + i[输入值 i] --> check1{i == 1?} + check1 -->|是| fall[falsthrough
进入 case 2] + check1 -->|否| check2{i == 2?} + check2 -->|是| retTrue[return true] + fall --> retTrue + check2 -->|否| retFalse[return false] + retTrue --> OUT1[输出: true] + retFalse --> OUT2[输出: false] +``` + +--- + +#### 9. 字符串不可变性 + +【单选题】下面代码输出什么?(5分) + +```go +func main() { + str := "hello" + str[0] = 'x' + fmt.Println(str) +} +``` + +A. hello +B. xello +C. compilation error +D. 其他 + +**正确答案:**C +**作答答案:**C + +> [!warning] Go 字符串是只读的! +> Go 中的 `string` 是不可变的(immutable),不能像切片那样通过索引赋值修改。如果需要修改字符串,应转为 `[]byte` 或 `[]rune`: +> +> ```go +> b := []byte("hello") +> b[0] = 'x' +> fmt.Println(string(b)) // 输出: xello +> ``` + +--- + +#### 10. Go 包管理工具 + +【单选题】Go语言包管理工具是哪个?(5分) + +A. dep +B. go mod +C. glide +D. gopkg + +**正确答案:**B +**作答答案:**B + +> [!info] Go 包管理演进 +> - **dep**:早期官方实验性工具(已废弃) +> - **glide**:社区流行但不再维护 +> - **go mod**:Go 1.11+ 引入,Go 1.16+ 成为默认标准 +> - **gopkg.in**:第三方包分发服务,非官方工具 + +--- + +#### 11. break 跳出循环 + +【单选题】在 Go 语言中,以下哪个关键字用于跳出当前的 for 循环?( )(5分) + +A. `break` +B. `continue` +C. `return` +D. `goto` + +**正确答案:**A +**作答答案:**A + +--- + +#### 12. 创建 Go Module + +【单选题】在Go语言中,如何创建一个新的module?(5分) + +A. 使用 `go create` 命令 +B. 使用 `go mod init` 命令 +C. 使用 `go new` 命令 +D. 直接创建一个新文件夹 + +**正确答案:**B +**作答答案:**B + +--- + +#### 13. 全局变量声明 + +【单选题】在Go中全局声明和初始化一个名为"counter"的整型变量正确的方式是?(5分) + +A. `var counter = int(0)` +B. `var counter int = 0` +C. `int counter = 0` +D. `counter := 0` + +**正确答案:**B +**作答答案:**B + +> [!info] var 声明 vs 短变量声明 +> - 选项 D `counter := 0` 是短变量声明,只能在**函数内部**使用,不能在包级别(全局)使用 +> - 选项 B `var counter int = 0` 在全局和局部均可使用 +> - 实际开发中常用简化写法:`var counter = 0`(省略类型,自动推断) + +--- + +#### 14. 短变量声明 + +【单选题】以下哪种是 Go 语言的短变量声明方式?(5分) + +A. `var x = 5` +B. `x := 5` +C. `x int = 5` +D. `let x = 5` + +**正确答案:**B +**作答答案:**B + +--- + +#### 15. init 函数参数 + +【单选题】Go语言中的init函数可以接受参数吗?(5分) + +A. 可以,可以接受任意类型的参数 +B. 可以,但只能接受同一个包内的类型为参数 +C. 不可以,init函数不能有参数,也不能定义返回值 +D. 可以,但只能接受基本类型的参数 + +**正确答案:**C +**作答答案:**C + +--- + +#### 16. 下划线 _ 的作用 + +【单选题】在 Go 语言中,以下关于下划线的作用描述正确的是?( )(5分) + +A. 作为变量名,用于存储无用的值 +B. 表示私有变量 +C. 用于注释 +D. 以上都不对 + +**正确答案:**A +**作答答案:**A + +> [!info] 下划线 _ 的多重用途 +> 1. **忽略值**:`_, err := os.Open("file")` — 不需要 err +> 2. **导入包副作用**:`import _ "image/png"` — 仅注册,不使用 +> 3. **空标识符**:用作占位符,避免编译器报错 + +--- + +#### 17. 转义符号 + +【单选题】在 Go 语言中,字符串转义使用的符号是?(5分) + +A. @ +B. `\` +C. `/` +D. # + +**正确答案:**B +**作答答案:**B + +--- + +#### 18. defer + recover 处理 panic + +【单选题】如何在Go中用`defer`和`recover`处理panic?(5分) + +A. 在defer函数中使用panic +B. 在defer函数中调用recover进行错误恢复 +C. 使用defer来抛出错误 +D. defer和recover无法一起使用 + +**正确答案:**B +**作答答案:**B + +> [!info] defer 与 recover 配合模式 +> ```go +> defer func() { +> if r := recover(); r != nil { +> fmt.Println("Recovered from:", r) +> } +> }() +> ``` +> - `recover()` 必须放在 `defer` 中才能捕获 panic +> - 若未发生 panic,`recover()` 返回 `nil` + +--- + +#### 19. 引发异常的关键字 + +【单选题】在Go程序中,使用哪个关键字可以引发一个异常?(5分) + +A. throw +B. error +C. exception +D. panic + +**正确答案:**D +**作答答案:**D + +--- + +#### 20. defer 执行顺序 + +【单选题】下面的Go代码块中,使用defer关键字的输出顺序是什么?(5分) + +```go +package main +import "fmt" +func main() { + defer fmt.Println("First") + defer fmt.Println("Second") + defer fmt.Println("Third") + fmt.Println("Done") +} +``` + +A. `First Second Third Done` +B. `Done Third Second First` +C. `Third Second First Done` +D. `Done First Second Third` + +**正确答案:**B +**作答答案:**B + +> [!important] defer 是 LIFO(后进先出)栈 +> defer 语句按照声明的**逆序**执行,类似栈的 push/pop: +> - 先 defer `First` → 最后执行 `First` +> - 再 defer `Second` → 倒数执行 `Second` +> - 最后 defer `Third` → 最先执行 `Third` +> - 正常 `fmt.Println("Done")` → 最先执行 + +```mermaid +stateDiagram-v2 + [*] --> MainCode + MainCode --> Deferred_Fist : defer Println("First") + Deferred_Fist --> Deferred_Second : defer Println("Second") + Deferred_Second --> Deferred_Third : defer Println("Third") + Deferred_Third --> Normal_Println : Println("Done") + Normal_Println --> Third_Exec : defer 开始执行 + Third_Exec --> Second_Exec : Println("Third") + Second_Exec --> First_Exec : Println("Second") + First_Exec --> Exit : Println("First") + Exit --> [*] + + note right of Normal_Println + 输出: Done Third Second First + defer 按声明逆序执行 + end note +``` + +--- + +### 二、多选题 + +#### 1. main() 函数特性 + +【多选题】关于 main() 函数,下面说法正确的是?(5分) + +A. 不能带参数; +B. 不能定义返回值; +C. 所在的包必须为 main 包; +D. 可以使用 flag 包来获取和解析命令行参数; + +**正确答案:**ABCD +**作答答案:**ABCD + +--- + +#### 2. switch 语句特性(一) + +【多选题】关于 switch 语句,下面说法正确的是?(5分) + +A. 单个 case 中,可以出现多个结果选项; +B. 需要使用 break 来明确退出一个 case; +C. 只有在 case 中明确添加 fallthrough 关键字,才会继续执行紧跟的下一个 case; +D. 条件表达式必须为常量或者整数; + +**正确答案:**AC +**作答答案:**AC + +> [!tip] Go switch 与 C/C++ switch 的区别 +> - Go 的 case 后**隐式自带 break**,不需要手动写 +> - 要穿透需显式使用 `fallthrough` +> - 条件表达式可以是任意类型(不限于整数) + +--- + +#### 3. defer 的实际应用 + +【多选题】Go语言defer的实际应用场景(5分) + +A. 在打开文件后立即使用defer来安排文件的关闭 +B. 在数据库连接成功后使用defer来安排断开连接 +C. 仅在程序的主函数中使用defer来进行错误处理 +D. 使用defer来执行重要的日志记录 + +**正确答案:**ABD +**作答答案:**ABD + +--- + +#### 4. bool 变量 if 判断规范 + +【多选题】flag 是 bool 型变量,下面 if 表达式符合编码规范的是?(5分) + +A. `if flag == 1` +B. `if flag` +C. `if flag == false` +D. `if !flag` + +**正确答案:**BCD +**作答答案:**C + +> [!warning] 易错题 +> - Go 不支持将整数隐式转为 bool,A 选项 `flag == 1` 语法上就不通(除非 flag 定义为 int) +> - Go Code Review Comments 推荐:`if condition` 而非 `if condition == true` +> - 作答选 C 可能是因为理解偏差——实际上 B 和 D 是最符合 Go 惯用法的选择 + +--- + +#### 5. 函数声明语法 + +【多选题】关于函数声明,下面语法正确的是?(5分) + +A. `func f(a, b int) (value int, err error)` +B. `func f(a int, b int) (value int, err error)` +C. `func f(a, b int) (value int, error)` +D. `func f(a int, b int) (int, int, error)` + +**正确答案:**ABD +**作答答案:**AB + +> [!info] 要点解析 +> - A ✅:合并相同类型的参数 `a, b int` 是合法的 +> - B ✅:逐个写类型也是合法的 +> - C ❌:混合了命名和无命名返回值,`error` 缺少类型名(应为 `e error`) +> - D ✅:匿名返回值是完全合法的写法 + +--- + +#### 6. 变量的自增和自减 + +【多选题】关于变量的自增和自减操作,下面语句正确的是?(5分) + +A. `i := 1; i++` +B. `i := 1; j = i++` +C. `i := 1; ++i` +D. `i := 1; i--` + +**正确答案:**AD +**作答答案:**ABD + +> [!warning] Go 的独特限制 +> - Go **只有** `++` 和 `--` 两个复合运算符,且它们是**语句**,不是表达式 +> - 不能出现在赋值右侧:❌ `j = i++`(选项 B 错误) +> - 不支持前置形式:❌ `++i`、`--i`(选项 C 错误) +> - 只能单独使用:✅ `i++`、`i--` + +--- + +#### 7. bool 变量赋值错误用法 + +【多选题】关于 bool 变量 b 的赋值,下面错误的用法是?(5分) + +A. `b = true` +B. `b = 1` +C. `b = bool(1)` +D. `b = (1 == 2)` + +**正确答案:**BC +**作答答案:**BC + +> [!info] Go 强类型约束 +> - Go 不允许整数到 bool 的隐式转换,也不允许 `bool(1)` 这种强制转换 +> - `1 == 2` 本身就是 bool 类型,赋值合法(选项 D 是正确的写法) + +--- + +#### 8. switch 语句特性(二) + +【多选题】关于switch语句,下面说法正确的有?(5分) + +A. 条件表达式必须为常量或者整数; +B. 单个case中,可以出现多个结果选项; +C. 需要用break来明确退出一个case; +D. 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case; + +**正确答案:**BD +**作答答案:**BD + +> [!note] 对比第 2 题 +> - 本题第 D 项和第 2 题第 C 项表述略有不同——注意区分"单个 case 中多个结果选项"(`case 1, 2, 3:`)和 fallthrough 穿透是两个不同机制 +> - Go switch 的 case 可以是任意类型,不限于整数 + +--- + +#### 9. 循环语句特性 + +【多选题】关于循环语句,下面说法正确的有(5分) + +A. 循环语句既支持 for 关键字,也支持 while 和 do-while; +B. 关键字 for 的基本使用方法与 C/C++ 中没有任何差异; +C. for 循环支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环; +D. for 循环不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量; + +**正确答案:**CD +**作答答案:**ACD + +> [!info] Go 只有 for 一种循环 +> ```go +> // 经典三要素 +> for i := 0; i < 10; i++ {} +> // while 风格 +> for condition {} +> // 无限循环 +> for {} +> ``` +> - Go **没有** while 和 do-while(选项 A 错) +> - Go 的 for 比 C/C++ 简化了许多场景(选项 B 错) +> - 多变量需平行赋值:`for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {}`(选项 D 对) + +--- + +#### 10. 全局字符串变量声明 + +【多选题】定义一个包内全局字符串变量,下面语法正确的是()(5分) + +A. `var str string` +B. `str:=""` +C. `str=""` +D. `var str=""` + +**正确答案:**AD +**作答答案:**AD + +> [!info] 全局 vs 局部 +> - A ✅:`var str string` — 包级别变量声明,零值为 `""` +> - B ❌:`:=` 只在函数体内可用 +> - C ❌:包级别不能用简单赋值 +> - D ✅:`var str = ""` — 包级别变量声明,带初始值 + +--- + +## 关联笔记 + +- [[hhs/EXAM/Week05]] +- [[hhs/EXAM/Week06]] +- [[Go/基础语法/变量与类型]] +- [[Go/基础语法/流程控制]] +- [[Go/基础语法/函数]] +- [[Go/进阶/defer与panic]] diff --git a/hhs/EXAM/Week02.md b/hhs/EXAM/Week02.md new file mode 100644 index 0000000..c5ec188 --- /dev/null +++ b/hhs/EXAM/Week02.md @@ -0,0 +1,539 @@ +--- +tags: [Go, 考试, Week02, 武科大服务端] +create time: 2026-05-14 12:00 +--- + +# 金山考试 - 武科大服务端-第二次课 + +## 概述 + +本次考试覆盖 Go 语言进阶核心知识点,包括切片(slice)、数组与切片的底层关系、Map 操作、结构体与方法、接口实现、标准库(JSON / 文件 / 时间)、指针与内存分配、并发编程(channel / select / sync)等。适合用于检验 Go 入门阶段学习成果。 + +**知识点地图:** + +```mermaid +graph LR + A[Go 进阶基础] --> B[切片 slice] + A --> C[映射 map] + A --> D[结构体 struct] + A --> E[方法 method] + A --> F[接口 interface] + A --> G[标准库 JSON/文件/时间] + A --> H[指针 new &] + A --> I[并发 channel select sync] + B --> B1[底层引用共享] + B --> B2[make vs new] + D --> D1[字段访问 .] + E --> E1[值接收者 vs 指针接收者] + I --> I1[无缓冲 Channel 同步语义] + I --> I2[select 多路复用] + I --> I3[WaitGroup / Mutex / RWMutex] +``` + +## 正文 + +### 一、单选题 + +#### 1. 错误的切片声明方式 + +【单选题】下列关于 Go 语言的切片声明,哪个是错误的?(5分) + +A. `var slice1 []int` +B. `slice2 := []int{}` +C. `slice3 := make([]int, 0)` +D. `slice4 := new([]int)` + +**正确答案:**D +**作答答案:**D + +> [!warning] new vs make +> - `make(T, args)`:仅用于 **slice、map、channel**,返回类型 T 本身并初始化 +> - `new(T)`:返回指向类型 T 的零值指针 `*T`,不初始化内部结构 +> - `new([]int)` 返回的是 `*[]int`,虽然能编译但不能直接当作切片使用 + +--- + +#### 2. 创建长度 0 容量 10 的切片 + +【单选题】要创建一个初始长度为 0,容量为 10 的整数切片,应该使用(5分) + +A. `new([]int)` +B. `make([]int, 0, 10)` +C. `new([10]int)` +D. `make([10]int)` + +**正确答案:**B +**作答答案:**B + +> [!info] make 的参数含义 +> `make([]int, len, cap)` — len 和 cap 均可选: +> - `make([]int, 0, 10)` → 长度 0、容量 10 +> - `make([]int, 10)` → 长度 10、容量 10(cap 省略时等于 len) +> - 数组用 `[N]T` 语法声明,不需要 make + +--- + +#### 3. 数组 vs 切片的长度特性 + +【单选题】以下关于数组和切片的说法,正确的是(5分) + +A. 数组的长度可以动态改变,切片不行 +B. 切片的长度可以动态改变,数组不行 +C. 数组和切片的长度都可以动态改变 +D. 数组和切片的长度都不能动态改变 + +**正确答案:**B +**作答答案:**B + +> [!tip] 核心区别一句话 +> 数组长度是**类型的一部分**(`[5]int` 和 `[10]int` 是不同的类型),不可变;切片是对数组的**轻量描述符**(ptr + len + cap),长度可以通过 append/slice 操作动态变化。 + +--- + +#### 4. 切片修改对底层数组的影响 + +【单选题】给定以下 Go 代码,运行后输出结果是什么?(5分) + +```go +package main + +import "fmt" + +func main() { + arr := [5]int{1, 2, 3, 4, 5} + slice := arr[1:4] // 引用 arr[1]=2, arr[2]=3, arr[3]=4 + slice[0] = 10 // 等价于 arr[1] = 10 + fmt.Println(arr) +} +``` + +A. `[1 2 3 4 5]` +B. `[1 10 3 4 5]` +C. `[10 2 3 4 5]` +D. 编译错误 + +**正确答案:**B +**作答答案:**B + +> [!important] 切片共享底层数组 +> 切片是对底层数组的**引用**。`arr[1:4]` 创建的切片其索引 0 对应 `arr[1]`,因此 `slice[0] = 10` 会直接修改 `arr[1]`。这是理解 Go 切片性能优势的关键——零拷贝传递数据。 + +```mermaid +sequenceDiagram + participant Arr as arr = [1,2,3,4,5] + participant Slc as slice = arr[1:4] + participant Set as slice[0] = 10 + Arr->>Slc: 创建引用(ptr→arr[1], len=3, cap=4) + Slc->>Set: 写 slice[0](即 arr[1]) + Set-->>Arr: arr[1] 被改为 10 + Arr-->>Arr: arr = [1,10,3,4,5] +``` + +--- + +#### 5. Map 的 delete 操作 + +【单选题】观察下面的 Go 代码,判断运行结果是什么?(5分) + +```go +package main + +import "fmt" + +func main() { + m := map[string]int{"a": 1, "b": 2, "c": 3} + delete(m, "b") + fmt.Println(m) +} +``` + +A. `map[a:1 b:2 c:3]` +B. `map[a:1 b:0 c:3]` +C. `map[a:1 c:3]` +D. `map[a:1]` + +**正确答案:**C +**作答答案:**C + +> [!info] delete 的语义 +> `delete(map, key)` 直接删除键值对,key 不存在时也是安全的(noop,不 panic)。打印时只保留剩余元素。 + +--- + +#### 6. map 的 json.Unmarshal + +【单选题】关于 map,下面说法正确的是?(5分) + +A. `json.Unmarshal()` 的入参必须为 map 的地址 +B. 在函数调用中传递 map,则子函数中对 map 元素的增加不会导致父函数中 map 的修改 +C. 不能使用内置函数 `delete()` 删除 map 的元素 +D. 以上都不对 + +**正确答案:**A +**作答答案:**A + +> [!info] 为什么必须是地址? +> `json.Unmarshal(data, &m)` 需要指针才能修改原 map。而 `json.Marshal(data, m)` 不需要地址(值即可读取)。 +> +> **补充:map 作为参数传递**——Go 中 map 是引用类型(内部是指针结构体),所以子函数通过同一底层数据结构增删元素,父函数可见。选项 B 说反了。 + +--- + +#### 7. 结构体字段访问 + +【单选题】在 Go 语言中,如果你有一个结构体实例 `myStruct`,你如何访问其字段 `field1` 的值?(5分) + +A. `myStruct[field1]` +B. `myStruct->field1` +C. `myStruct.field1` +D. `*myStruct.field1` + +**正确答案:**C +**作答答案:**C + +> [!note] Go 没有箭头运算符 +> Go 不使用 `->`(C/C++ 风格),也不使用 `[key]`(像 map 那样)。统一用点号 `.` 访问字段。当持有指针 `p` 时,也只需 `p.Field`,编译器自动解引用。 + +--- + +#### 8. 结构体方法的错误认知 + +【单选题】以下关于结构体方法的说法,错误的是(5分) + +A. 值接收者方法不能修改结构体的字段值 +B. 指针接收者方法可以修改结构体的字段值 +C. 值接收者方法在传递参数时效率更高 +D. 指针接收者方法在结构体较大时更节省内存 + +**正确答案:**C +**作答答案:**C + +> [!warning] 值接收者的陷阱 +> - A ✅:值接收者是副本,修改不影响原对象 +> - B ✅:指针接收者直接操作原对象 +> - C ❌:**这是本题答案**。值接收者在传参时需要复制整个结构体,当结构体较大时反而更慢 +> - D ✅:指针只传 8 字节(ptr),避免完整复制 + +--- + +#### 9. 接口实现的完整性 + +【单选题】Go 中如何正确实现一个接口?(5分) + +A. 只需要声明接口中的所有方法 +B. 只需要部分实现接口中的方法 +C. 需要声明并实现接口中的所有方法 +D. 接口不能被实现,只能被继承 + +**正确答案:**C +**作答答案:**C + +> [!info] Go 接口是隐式实现 +> Go 没有 `implements` 关键字。只要一个类型的**所有方法签名**与接口一致,就自动实现了该接口。缺少任一方法都会编译报错。 + +--- + +#### 10. JSON 处理的标准库 + +【单选题】在 Go 中处理 JSON 数据的推荐方法是?(5分) + +A. 使用 `encoding/json` 标准库 +B. 使用外部包如 `jsoniter` +C. 手动解析字符串 +D. 使用 `bufio` 读取器 + +**正确答案:**A +**作答答案:**A + +> [!info] encoding/json +> Go 标准库 `encoding/json` 提供 `Marshal` / `Unmarshal` 两个核心函数,通过结构体 tag(`` `json:"name"` ``)控制序列化行为,满足绝大多数场景需求。 + +--- + +#### 11. 格式化输出的函数选择 + +【单选题】在 Go 中,如何通过 fmt 包输出格式化的字符串?(5分) + +A. `fmt.Print()` +B. `fmt.Println()` +C. `fmt.Printf()` +D. `fmt.Format()` + +**正确答案:**C +**作答答案:**C + +> [!tip] fmt 包常用函数速记 +> - `Print`:无换行,无格式 +> - `Println`:自动追加换行 +> - `Printf`:**支持占位符格式化**,如 `%d`、`%s`、`%v` + +--- + +#### 12. 读取文件的便捷方式 + +【单选题】选择 Go 语言中正确的方式来读取文件。(5分) + +A. `os.ReadFile()` +B. `os.OpenFile()` +C. `ioutil.ReadAll()` +D. `file.Read()` + +**正确答案:**A +**作答答案:**A + +> [!note] ioutil 已废弃 +> `ioutil.ReadAll` 和 `ioutil.ReadFile` 在 Go 1.16+ 中被迁移到 `io` 和 `os` 包中。推荐写法:`os.ReadFile("path")` 一次性读取全部文件内容到 `[]byte`。 + +--- + +#### 13. 时间日期的标准库 + +【单选题】在 Go 中,用于处理时间和日期的标准库是哪一个?(5分) + +A. `time` +B. `date` +C. `timer` +D. `clock` + +**正确答案:**A +**作答答案:**A + +> [!info] time 包核心用法 +> ```go +> now := time.Now() // 当前时间 +> time.Sleep(1 * time.Second) // 休眠 +> t.Format("2006-01-02 15:04:05") // 格式化(记忆:参考时间 2006-01-02) +> ``` + +--- + +#### 14. new 的正确使用场景 + +【单选题】以下哪种情况应该使用 `new` 来分配内存?(5分) + +A. 创建一个切片 +B. 创建一个映射 +C. 创建一个结构体指针 +D. 扩展一个切片的容量 + +**正确答案:**C +**作答答案:**C + +> [!info] new vs make 对比 +> | 用途 | new(T) | make(T, ...)| +> |------|--------|-------------| +> | 返回类型 | `*T`(指针) | T 本身 | +> | 适用于 | 所有类型 | slice, map, channel | +> | 初始化 | 零值 | 完整初始化 | +> | 典型场景 | `new(Person)` | `make([]int, 10)` | + +--- + +#### 15. 无缓冲 channel 的同步语义 + +【单选题】无缓冲 channel 的特点是什么?(5分) + +A. 发送操作永远不会阻塞 +B. 接收操作永远不会阻塞 +C. 发送和接收操作必须同时发生 +D. 可以存储无限数量的值 + +**正确答案:**C +**作答答案:**C + +> [!important] 无缓冲 Channel = 同步通道 +> - 容量为 0,send 和 receive **必须同时就绪**才能完成 +> - 生产者 send 后阻塞直到有消费者 receive +> - 消费者 receive 后阻塞直到有生产者 send +> - 这是 goroutine 间最原子的同步机制 + +--- + +#### 16. Select 语句的作用 + +【单选题】Go 语言中的 `select` 语句用于什么目的?(5分) + +A. 声明变量 +B. 捕获异常 +C. 监听多个 Channel 操作 +D. 同步 Goroutine + +**正确答案:**C +**作答答案:**C + +> [!info] select 的行为规则 +> - 每个 case 必须是 channel 的 send 或 receive 操作 +> - 所有 case 求值后,随机选择一个可用的执行 +> - 若无可用 case 且有 `default`,立即执行 default +> - 若无可用 case 且无 default,阻塞直到某个 case 就绪 + +--- + +#### 17. Goroutine 同步的原语选择 + +【单选题】在 Go 语言中,哪个 sync 原语可以用于确保多个 Goroutine 的同步执行?(5分) + +A. `WaitGroup` +B. `Mutex` +C. `Atomic` +D. `Context` + +**正确答案:**A +**作答答案:**A + +> [!info] 各 sync 原语分工 +> - `sync.WaitGroup`:**等待一组 goroutine 完成**(Add / Done / Wait) +> - `sync.Mutex`:互斥锁,保护临界区 +> - `sync/atomic`:原子操作,无锁并发 +> - `context.Context`:取消信号与超时控制 + +--- + +#### 18. 取地址运算符 + +【单选题】在 Go 中获取变量的内存地址应使用什么运算符?(5分) + +A. `*` +B. `&` +C. `#` +D. `@` + +**正确答案:**B +**作答答案:**B + +> [!tip] 指针运算口诀 +> - `&x`:取 x 的地址 +> - `*p`:解除 p 指向,访问目标值 +> - 二者互为逆运算:`*&x == x`,`&*p == p` + +--- + +#### 19. RWMutex vs Mutex + +【单选题】在 Go 中,`sync.RWMutex` 与 `sync.Mutex` 的主要区别是?(5分) + +A. Mutex 只支持读操作 +B. RWMutex 允许多个读锁或一个写锁,Mutex 仅支持互斥访问 +C. 两者没有区别 +D. RWMutex 不能与 channel 同时使用 + +**正确答案:**B +**作答答案:**B + +> [!info] 读写锁的性能意义 +> 当读多写少时,`RWMutex` 显著优于 `Mutex`:多个 goroutine 可同时持有多把读锁 (`RLock`),只有写操作 (`Lock`) 时才互斥。这是典型的「读并发、写串行」优化手段。 + +--- + +#### 20. 使用指针接收者的原因 + +【单选题】以下结构体方法定义中,使用指针接收者的原因通常是(5分) + +A. 方法内部需要修改结构体的字段值 +B. 为了提高性能(避免大结构体拷贝) +C. 为了遵循代码规范 +D. 以上都是 + +**正确答案:**D +**作答答案:**D + +> [!info] 何时用指针接收者? +> 1. 需要**修改**原对象状态 +> 2. 结构体**较大**,减少拷贝开销 +> 3. 需要保证**一致性**(同一个对象的方法要么全用指针,要么全用值接收者) + +--- + +#### 21. 值接收者方法不会修改原对象 + +【单选题】对于以下结构体和方法定义,调用 ChangeName 方法后,原始的 Person 结构体实例的 Name 字段会改变吗?(5分) + +```language +type Person struct { + Name string + Age int +} + +func (p Person) ChangeName(newName string) { + p.Name = newName +} +``` + +A. 会 +B. 不会 +C. 不确定 +D. 以上都不对 + +**正确答案:**B +**作答答案:**B + +> [!warning] 经典面试题 +> `ChangeName` 使用**值接收者**,这意味着 `p` 是原对象的**副本**。方法内修改 `p.Name` 只会影响副本,原对象不受影响。若希望修改生效,应将接收者改为指针 `(p *Person)`。 + +--- + +### 二、多选题 + +#### 1. 指针访问成员变量的方式 + +【多选题】通过指针变量 p 访问其成员变量 name,有哪几种方式?(5分) + +A. `p.name` +B. `(&p).name` +C. `(*p).name` +D. `p->name` + +**正确答案:**AC +**作答答案:**AC + +> [!info] 指针访问字段两种方式 +> - `p.name` ✅:Go 语法糖,编译器自动解引用,推荐写法 +> - `(*p).name` ✅:先解引用再 `.name`,等价但稍显繁琐 +> - `(&p).name` ❌:`&p` 是指针的地址(双重指针 `**P`),不符合题意 +> - `p->name` ❌:Go 没有 `->` 运算符 + +--- + +#### 2. panic 和 recover 的使用 + +【多选题】关于 Go 语言中 panic 和 recover 的使用,以下正确的是?(5分) + +A. panic 可以在任何地方引发,但 recover 只有在 defer 调用的函数中有效 +B. recover 用于终止 panic 的异常传递过程 +C. panic 无法被捕获,会直接导致程序崩溃 +D. 使用 panic 和 recover 可以替代传统的错误处理方式 + +**正确答案:**AB +**作答答案:**ABC + +> [!warning] 易错辨析 +> - A ✅:recover 放在 defer 中能捕获当前 goroutine 的 panic,阻止程序崩溃 +> - B ✅:recover 被调用后,panic 链中断,程序恢复正常运行 +> - C ❌:panic **可以**被 recover 捕获,并非一定会导致程序崩溃 +> - D ❌:Go 设计哲学中,panic 用于**真正的异常情况**(如编程错误、初始化失败),业务错误应用 error 返回 + +--- + +#### 3. 数组和切片的综合理解 + +【多选题】在 Go 语言中,关于数组(array)和切片(slice),以下说法正确的是?(5分) + +A. 数组定义时必须指定长度,且长度是数组类型的一部分 +B. 切片定义时不需要指定长度,其长度可以在运行时动态变化 +C. 数组作为函数参数传递时,传递的是数组的副本,函数内对数组的修改不会影响原始数组 +D. 切片作为函数参数传递时,传递的是切片结构体的副本,函数内对切片的修改会反映到原始切片上 + +**正确答案:**ABCD +**作答答案:**ABCD + +> [!important] 四个选项全对的原因 +> - A ✅:`[3]int` 和 `[5]int` 是不同长度决定类型 +> - B ✅:slice 本质是「指针+长度+容量」描述符 +> - C ✅:数组传值是**深拷贝**整个数组内容 +> - D ✅:slice 内部含底层数组指针,虽然 slice 自身是副本(ptr/len/cap 三件套被拷贝),但对 `slice[i] = x` 等元素级修改通过 ptr 反映到底层数组,因此源数据可见 + +## 关联笔记 + +- [[hhs/EXAM/Week01]] +- [[hhs/EXAM/Week05]] +- [[hhs/EXAM/Week06]] diff --git a/hhs/EXAM/Week03.md b/hhs/EXAM/Week03.md new file mode 100644 index 0000000..e6d981b --- /dev/null +++ b/hhs/EXAM/Week03.md @@ -0,0 +1,811 @@ +--- +tags: [exam, gin, go, http, jwt, cors, middleware, session, cookie, web] +create time: 2026-05-14 18:30 +--- + +# 金山考试 - 武科大服务端 - 第三次课 + +## 概述 + +武科大服务端第三次课的考试错题整理与知识点拓展。共 30 道单选题,满分 150 分,涉及 HTTP 协议基础、Gin 框架中间件、Cookie/Session/JWT 认证、CORS 跨域、Nginx 反向代理等核心后端开发知识。 + +**得分:145/150(仅第 12 题出错)** + +--- + +## 我的错题 + +### 第 12 题 — Gin 全局注册中间件 + +**你的作答:** D · **正确答案:** A + +```go +// 错误答案 ❌ +router.Middleware(middleware) + +// 正确答案 ✔ +router.Use(middleware) +``` + +> [!tip] 考点解析 +> +> Gin 使用 `Use()` 方法注册**全局中间件**,参数接受一个或多个 `HandlerFunc`: +> +> ```go +> router := gin.Default() +> // 全局注册:所有路由都会经过此中间件 +> router.Use(GinLogger()) +> router.Use(GinRecovery()) +> +> // 组级注册:只对 /v1 组生效 +> v1 := router.Group("/v1") +> v1.Use(AuthMiddleware()) +> { +> v1.GET("/user", getUserHandler) +> } +> ``` +> +> **记忆要点:** `Use` 是链式的,中间件按注册顺序执行。 +> +> | 方法 | 作用范围 | 说明 | +> |------|---------|------| +> | `router.Use()` | 全局中间件 | 所有路由生效 | +> | `group.Use()` | 组中间件 | 仅该 Group 下路由生效 | + +--- + +## 正文 + +### 一、HTTP 协议基础 + +> [!abstract] 本节覆盖:第 1、5、29 题 +> +> HTTP 是 Web 的基石。理解它的无状态特性、方法语义和端口规范,是后续学习所有后端技术的前提。 + +#### 第 1 题 + +【单选题】HTTP请求中的 GET 和 POST 有什么主要区别?(5分) + +A. GET请求通常用于请求数据,POST请求只用于提交数据。 + +B. GET请求的数据会附加在URL之后,POST请求的数据放在请求体内。 + +C. GET请求可以获取新的资源,POST请求无法获取资源。 + +D. GET请求不安全,POST请求比GET请求安全。 + +**正确答案:** B + +**作答答案:** B + +> [!tip] 深度理解 +> +> - **URL vs Body**:GET 的参数拼在 URL 后(如 `/api/user?id=1`),受长度限制且会出现在浏览器历史记录中;POST 的数据放在 Request Body 中,没有长度限制。 +> - **幂等性**:GET 应该是幂等的(多次请求结果一致),POST 不是。 +> - **安全性误区**:POST != 安全!两者都是明文传输(除非配合 HTTPS)。敏感数据应该用 HTTPS,而非依赖 POST。 + +#### 第 5 题 + +【单选题】下列哪一项是 HTTP 的默认端口号?(5分) + +A. 21 + +B. 80 + +C. 443 + +D. 8080 + +**正确答案:** B + +**作答答案:** B + +> [!note] 常见端口速查 +> +> | 端口 | 协议 | 用途 | +> |------|------|------| +> | 80 | HTTP | 标准 Web 服务 | +> | 443 | HTTPS | 加密 Web 服务 | +> | 21 | FTP | 文件传输 | +> | 3306 | MySQL | 数据库 | +> | 8080 | HTTP | 常用开发/替代端口 | + +#### 第 29 题 + +【单选题】HTTP协议被称为"无状态 (Stateless)"协议,这到底意味着什么?(5分) + +A. 服务器无法一次性处理多个状态的请求 + +B. 每次请求都是独立的,服务器默认不会记住之前的请求信息 + +C. 数据在传输过程中没有加密状态 + +D. 客户端不需要等待服务器的响应即可发送下一个请求 + +**正确答案:** B + +**作答答案:** B + +> [!example] 为什么需要 Cookie/Session/JWT? +> +> 因为 HTTP 是无状态的——每次请求都是一次"失忆"的新对话。为了维持用户登录态,我们需要额外的机制来"记住"用户是谁: +> +> ```mermaid +> sequenceDiagram +> participant Browser as 浏览器 +> participant Server as 服务端 +> Browser->>Server: 1. POST /login (账号密码) +> Server->>Browser: 2. Set-Cookie: sessionId=abc123 +> Browser->>Server: 3. GET /profile (携带 Cookie) +> Server->>Server: 4. 根据 sessionId 查 Session 数据 +> Server->>Browser: 5. 返回用户个人信息 +> ``` + +--- + +### 二、缓存控制 + +> [!abstract] 本节覆盖:第 2、3、6 题 +> +> 缓存是 Web 性能优化的核心手段。理解 Cache-Control 和 304 状态码,能让你的应用快得多。 + +#### 第 2 题 + +【单选题】以下哪个 HTTP 头字段可以用于管理缓存的过期时间?(5分) + +A. Content-Type + +B. Cache-Control + +C. Authorization + +D. Location + +**正确答案:** B + +**作答答案:** B + +#### 第 3 题 + +【单选题】哪种 HTTP 状态码代表"未修改"?(5分) + +A. 200 OK + +B. 304 Not Modified + +C. 404 Not Found + +D. 500 Internal Server Error + +**正确答案:** B + +**作答答案:** B + +#### 第 6 题 + +【单选题】HTTP协议中哪个状态码表示请求的资源未被修改?(5分) + +A. 200 + +B. 403 + +C. 304 + +D. 500 + +**正确答案:** C + +**作答答案:** C + +> [!tip] Cache-Control + 304 组合拳 +> +> 两种缓存策略各有分工: +> +> | 策略 | 原理 | 关键头 | +> |------|------|--------| +> | **强缓存** | 不发起请求,直接读本地缓存 | `Cache-Control: max-age=3600` | +> | **协商缓存** | 发请求问服务端"我缓存过期的没" | `ETag` + `If-None-Match` | +> +> **完整流程:** +> +> ```mermaid +> sequenceDiagram +> participant 浏览器 +> participant 服务端 +> 浏览器->>服务端: GET /data (带 If-None-Match: "abc123") +> 服务端->>服务端: 检查资源是否变化 +> alt 资源未变化 +> 服务端-->>浏览器: 304 Not Modified +> 浏览器->>浏览器: 使用本地缓存 +> else 资源已变化 +> 服务端-->>浏览器: 200 + 新内容 + 新 ETag +> end +> ``` +> +> **两个关键头**: +> - **ETag**:服务器生成的资源指纹(哈希值),如 `"abc123"` +> - **If-None-Match**:浏览器下次请求时把上次的 ETag 传回来让服务端比对 + +--- + +### 三、Gin 框架核心 + +> [!abstract] 本节覆盖:第 8、13~18、20、27 题 +> +> Gin 是 Go 生态最流行的 Web 框架。掌握 Context、路由、绑定和中间件,你就掌握了 Gin 的命脉。 + +#### 第 8 题 + +【单选题】在Go language HTTP server中,如何从HTTP请求中解析查询参数?(5分) + +A. 使用req.URL.Query()方法 + +B. 调用req.ParseForm()然后使用req.Form + +C. 使用req.GetHeader('Query')方法 + +D. 手动解析req.Body流中的内容 + +**正确答案:** A + +**作答答案:** A + +> [!code Go] 查询参数解析对比 +> +> ```go +> // 方式1:net/http 原生 +> values := req.URL.Query() // map[string][]string +> id := values.Get("id") // string +> +> // 方式2:Gin 封装(更简洁) +> id := c.Query("id") // string(单值) +> ids := c.QueryArray("id") // []string(多值) +> fid := c.DefaultQuery("id", "1") // 带默认值 +> ``` + +#### 第 13 题 + +【单选题】编写一个Gin中间件(比如计算请求耗时)时,哪个方法用于暂停当前中间件的执行,先去执行后续的业务逻辑(Handler),等业务执行完再回来继续执行中间件剩余的代码?(5分) + +A. c.Next() + +B. c.Abort() + +C. c.Stop() + +D. c.Return() + +**正确答案:** A + +**作答答案:** A + +> [!important] Next() vs Abort() — 中间件的双向控制 +> +> ```go +> func Logger() gin.HandlerFunc { +> start := time.Now() // 记录开始时间 +> c.Next() // ▶ 放行给后续中间件/handler +> // ◀ 这里会继续执行(像洋葱剥开又合上) +> log.Printf("%s %s %v", c.Writer.Status(), c.Request.URL.Path, time.Since(start)) +> } +> +> func Auth() gin.HandlerFunc { +> token := c.GetHeader("Authorization") +> if token == "" { +> c.Abort() // 中断链条,不再执行后续中间件 +> c.JSON(401, gin.H{"error": "缺少认证"}) +> return +> } +> c.Next() // 验证通过,放行 +> } +> ``` + +#### 第 14 题 + +【单选题】当前端发送了一段JSON数据,后端想将其解析并映射到Go的一个结构体(Struct)上,推荐使用哪个方法?(5分) + +A. c.Query() + +B. c.PostForm() + +C. c.ShouldBindJSON() + +D. c.GetString() + +**正确答案:** C + +**作答答案:** C + +> [!code Go] Gin 常用绑定方法 +> +> ```go +> type User struct { +> Name string `json:"name" binding:"required"` +> Age int `json:"age" binding:"gte=0,lte=150"` +> } +> +> func handler(c *gin.Context) { +> var u User +> _ = c.ShouldBindJSON(&u) // 绑定 JSON body +> // 其他绑定方式: +> c.ShouldBindQuery(&u) // 绑定 ?name=x&age=18 +> c.ShouldBindUri(&u) // 绑定 /users/123 +> c.ShouldBind(&u) // 自动识别 content-type +> } +> ``` + +#### 第 15 题 + +【单选题】在Gin中,你想设计一个路由来获取特定ID的用户信息(例如从 /user/123 中提取出 123),正确的动态路由定义方式是什么?(5分) + +A. router.GET("/user/?id", handler) + +B. router.GET("/user/:id", handler) + +C. router.GET("/user/*id", handler) + +D. router.GET("/user/{id}", handler) + +**正确答案:** B + +**作答答案:** B + +> [!note] Gin 路由语法速记 +> +> | 模式 | 含义 | 示例 | +> |------|------|------| +> | `/user/:id` | 单变量 | `/user/123` → id="123" | +> | `/user/*path` | 通配符 | `/user/a/b/c` → path="/a/b/c" | +> | `/user/search` | 固定路径 | 精确匹配 | + +#### 第 16 题 + +【单选题】在Gin框架中,中间件的用途是什么?(5分) + +A. 在处理请求和发送响应之间执行额外的逻辑。 + +B. 生成项目的文档。 + +C. 处理数据库连接。 + +D. 定义项目配置。 + +**正确答案:** A + +**作答答案:** A + +#### 第 17 题 + +【单选题】在Gin框架中,若需要在中间件之间共享数据,应使用哪种方式?(5分) + +A. 利用c.Set存储数据,并在另一个中间件中通过c.Get获取。 + +B. 直接将数据作为参数传递至下一个中间件。 + +C. 使用全局变量来存储和共享数据。 + +D. 创建一个新的结构体来在中间件之间传递数据。 + +**正确答案:** A + +**作答答案:** A + +> [!code Go] c.Set / c.Get 在中间间共享数据 +> +> ```go +> func UserInfoExtractor() gin.HandlerFunc { +> user := fetchUserFromToken(c.GetHeader("Authorization")) +> c.Set("userID", user.ID) // 存入上下文 +> c.Set("username", user.Name) +> c.Next() +> } +> +> func GetProfile(c *gin.Context) { +> // 直接从上下文中取,无需层层传参 +> id, _ := c.Get("userID") +> name, _ := c.Get("username") +> c.JSON(200, gin.H{"id": id, "name": name}) +> } +> ``` + +#### 第 18 题 + +【单选题】在Gin框架中,如何使用JWT验证中间件?(5分) + +A. 通过router组添加中间件进行验证 + +B. 使用静态文件服务 + +C. 直接在handler函数中编写JWT验证代码 + +D. 使用外部服务进行验证 + +**正确答案:** A + +**作答答案:** A + +> [!code Go] JWT 中间件实战模板 +> +> ```go +> func JWTAuth() gin.HandlerFunc { +> return func(c *gin.Context) { +> tokenStr := c.GetHeader("Authorization") +> claims, err := parseJWT(tokenStr) +> if err != nil { +> c.AbortWithStatusJSON(401, gin.H{"error": "无效令牌"}) +> return +> } +> c.Set("userID", claims.UserID) // 存入上下文供后续使用 +> c.Next() +> } +> } +> +> // 使用:只对需要认证的路由组生效 +> admin := r.Group("/admin") +> admin.Use(JWTAuth()) +> { +> admin.GET("/users", listUsers) +> } +> ``` + +#### 第 20 题 + +【单选题】在Gin框架中,gin.Context 是出现频率最高的参数。它的核心作用是什么?(5分) + +A. 仅仅用于连接数据库的上下文 + +B. 专门用来渲染HTML模板的对象 + +C. 封装了HTTP的请求(Request)和响应(ResponseWriter),并在中间件之间传递数据 + +D. 用于启动Gin的底层HTTP服务器 + +**正确答案:** C + +**作答答案:** C + +> [!note] gin.Context 一图看懂 +> +> ```mermaid +> graph TD +> A["gin.Context"] --> B["Request 读取
Query/Header/Body"] +> A --> C["Response 写入
JSON/String/File"] +> A --> D["中间件通信
c.Set / c.Get / c.Next"] +> A --> E["生命周期管理
Abort / Deadline"] +> ``` +> +> Context 贯穿整个请求生命周期——从接收到响应,它是最核心的桥梁。 + +#### 第 27 题 + +【单选题】后端处理完逻辑后,想给前端返回一个标准的JSON格式响应(包含状态码200),以下哪句代码是正确的?(5分) + +A. c.String(200, "{\"status\": \"success\"}") + +B. c.JSON(http.StatusOK, gin.H{"message": "success", "data": myData}) + +C. c.XML(200, myData) + +D. c.HTML(http.StatusOK, "index.html", nil) + +**正确答案:** B + +**作答答案:** B + +> [!code Go] Gin 响应方法一览 +> +> | 方法 | 适用场景 | Content-Type | +> |------|---------|-------------| +> | `c.JSON()` | JSON 响应 | application/json | +> | `c.String()` | 纯文本 | text/plain | +> | `c.XML()` | XML 响应 | application/xml | +> | `c.Data()` | 自定义类型 | 可指定任意类型 | +> | `c.File()` | 下载文件 | 自动推断 | + +--- + +### 四、Cookie、Session 与 JWT 认证 + +> [!abstract] 本节覆盖:第 7、9、10、11、21 题 +> +> 这三种方案解决了同一个问题——在**无状态**的 HTTP 上建立**有状态**的会话。各有优劣,适用于不同场景。 + +#### 第 7 题 + +【单选题】在使用 Gin 设置 Cookie 时,为了防止前端恶意脚本通过 JavaScript 读取到包含用户登录凭证的敏感 Cookie,你应该将哪个参数设置为什么值?(5分) + +httpOnly 是防御 XSS 攻击的利器。当它设为 true 时,浏览器会把这个 Cookie 藏好,只有在向后端发送 HTTP 请求时才会默默带上,前端的任何 JS 代码都无权读取。顺便提一下,secure 也很重要,它表示该 Cookie 只能在 HTTPS 加密连接下传输,用来防止网络中间人窃听。 + +A. maxAge 设置为 0 + +B. secure 设置为 true + +C. httpOnly 设置为 true + +D. domain 设置为 "localhost" + +**正确答案:** C + +**作答答案:** C + +> [!tip] Cookie 安全属性对照表 +> +> | 属性 | 作用 | 推荐生产环境值 | +> |------|------|---------------| +> | `HttpOnly` | JS 无法读取,防 XSS | `true` | +> | `Secure` | 仅 HTTPS 传输,防抓包 | `true` | +> | `SameSite` | 防 CSRF,限制第三方携带 | `Strict` 或 `Lax` | +> | `Max-Age` | 有效期(秒),0=删除 | 按需设置 | + +#### 第 9 题 + +【单选题】关于 Gin 框架中 Session 的运用,以下说法正确的是?(5分) + +Session 的本质是服务器在后端存储了用户状态,只把一个无意义的、随机的 SessionID 通过 Cookie 发给前端保存。Gin 核心库为了保持轻量,并没有内置 Session,推荐使用第三方中间件。 + +A. Gin 官方核心库自带了完善的 Session 管理器,无需引入第三方包。 + +B. Session 数据默认以明文形式保存在客户端的浏览器中。 + +C. Session 的本质是服务器在后端存储用户状态,只把 SessionID 发给前端。 + +D. 只要用户关闭浏览器页面,服务器端的 Session 数据就会被自动删除。 + +**正确答案:** C + +**作答答案:** C + +#### 第 10 题 + +【单选题】JWT 解决了传统 Session 架构的什么核心痛点?(5分) + +JWT 是"无状态"的,服务端不需要存储会话记录就能验证其有效性,从而解决了传统 Session 在服务器集群环境下的共享与同步问题。 + +A. JWT 对用户密码进行了极强的加密,即便被黑客拦截也绝对安全。 + +B. 传统 Session 强依赖于服务端内存或集中存储,JWT 是"无状态"的。 + +C. JWT 生成的字符串非常短,比传统的 SessionID 更省网络带宽。 + +D. JWT 只能在 Go 语言和前端之间解析,跨语言安全性极高。 + +**正确答案:** B + +**作答答案:** B + +#### 第 11 题 + +【单选题】当中间件成功解析并验证了前端请求头中的 JWT 令牌后,最佳的代码实践是?(5分) + +中间件是查票员,查验完 JWT 后,通过 c.Set() 把乘客信息存储在当前 HTTP 请求的生命周期上下文中,后续函数可以安全、隔离地拿到用户信息。 + +A. 将解析出的用户 ID 保存到全局变量中。 + +B. 使用 c.Redirect 将请求重定向,并在 URL 参数里带上用户 ID。 + +C. 使用 c.Set("userID", id) 将用户信息存储在上下文中。 + +D. 将用户信息打包成 JSON 写回到响应体中,结束请求。 + +**正确答案:** C + +**作答答案:** C + +#### 第 21 题 + +【单选题】当用户首次访问网站时,服务器通常会在HTTP响应中使用哪个字段来向客户端存储session ID?(5分) + +A. Cache-Control + +B. Set-Cookie + +C. Content-Length + +D. User-Agent + +**正确答案:** B + +**作答答案:** B + +> [!summary] 认证方案横向对比 +> +> | 维度 | Session (Cookie) | JWT | +> |------|-----------------|-----| +> | 存储位置 | 服务端(内存/Redis) | 客户端(自行保管) | +> | 服务端压力 | 需维护会话,集群需共享 | 无状态,天然分布式友好 | +> | 主动注销 | 可从服务端删除 ✅ | 需黑名单/缩短有效期 ⚠️ | +> | token 大小 | Cookie 仅传 SessionID(很小) | JWT 本身较长(含 payload) | +> | 适用场景 | 后台管理系统、需要频繁踢人 | 移动端 API、微服务、SSO | + +--- + +### 五、CORS 跨域 + +> [!abstract] 本节覆盖:第 22~26 题 +> +> 跨域是现代全栈开发的"必修课"。理解同源策略、预检请求和各类解决方案,才能游刃有余地应对生产环境。 + +#### 第 22 题 + +【单选题】当你的项目正式上线:前端部署在 www.myweb.com,Go 后端 API 部署在 api.myweb.com,此时前端又碰到了跨域问题。生产环境下,业界最标准、最推荐的解决方式是什么?(5分) + +A. 在 Nginx 配置中开启反向代理,让前端和后端走同一个域名,彻底消灭跨域。或者在 Go 后端通过代码严格配置 CORS 中间件,允许指定域名的访问。 + +B. 要求所有用户在浏览器中安装一个允许跨域的插件。 + +C. 把 Vue/React 前端代码编译进 Go 语言的二进制文件里,打包成一个单体服务,从而避免跨域。 + +D. 在前端代码中使用 JSONP 的方式来绕过浏览器的同源策略。 + +**正确答案:** A + +**作答答案:** A + +#### 第 23 题 + +【单选题】当我们的全栈项目要部署到生产环境时,通常会在最外层挡一个 Nginx,并将外部请求"反向代理"给内部的 Go 应用程序。为什么要多此一举,而不是直接让 Go 监听 80/443 端口对外提供服务?(5分) + +A. 因为 Go 语言标准库的 net/http 性能太差,无法承受高并发,必须靠 Nginx 提速。 + +B. 因为 Go 程序无法处理 HTTPS 证书的加密解密。 + +C. Nginx 专精于静态文件的高效分发,并且能够作为统一入口隐藏后端真实 IP,还能轻松实现请求路由分发、负载均衡和限流,极大提升了整体架构的健壮性和安全性。 + +D. 这是操作系统的强制要求,所有的 Web 流量必须先经过 Nginx 才能到达用户态程序。 + +**正确答案:** C + +**作答答案:** C + +#### 第 24 题 + +【单选题】当你在前端用 axios 发送一个携带了自定义请求头的 POST 请求时,浏览器实际上发了两次请求。这个 OPTIONS 请求是做什么的?(5分) + +A. 它是用来试探后端服务器是否死机的心跳检测。 + +B. 这是浏览器的 CORS"预检请求 (Preflight)"。因为带有自定义头或非普通类型的请求属于"复杂请求",浏览器必须先发个 OPTIONS 问问后端:"我带了特殊的头,你可以接受吗?" + +C. 这是 axios 库本身的 bug 导致的重复请求。 + +D. 它是用来提前把数据进行加密的握手请求。 + +**正确答案:** B + +**作答答案:** B + +#### 第 25 题 + +【单选题】在前端开发阶段,我们通常会在 vite.config.js 或 vue.config.js 中配置 proxy 代理来解决跨域问题。这个 Proxy 底层的运作逻辑是什么?(5分) + +A. 它修改了浏览器的安全设置,让浏览器临时关闭了同源策略。 + +B. 它修改了前端发出的 HTTP 请求的 Origin,伪装成和后端同源。 + +C. 前端代码实际上把请求发给了 Vite 自带的本地 Node.js 开发服务器,开发服务器再把请求转发给真正的 Go 后端,最后把结果原样返回给前端。 + +D. 它自动为后端的 Go 代码注入了处理跨域的中间件。 + +**正确答案:** C + +**作答答案:** C + +#### 第 26 题 + +【单选题】前后端分离开发时,前端调用后端经常会遇到可怕的 CORS 报错。导致跨域报错的根本原因是什么?它为了解决什么痛点而存在?(5分) + +A. 因为 HTTP 协议不允许不同端口之间通信,这是底层网络协议的限制。 + +B. 因为后端的 Go 代码默认开启了防火墙,拦截了来自其他端口的请求。 + +C. 这是浏览器的"同源策略 (Same-Origin Policy)"导致的,为了防止恶意网站窃取用户的敏感数据(如 Cookie),浏览器强制拦截了跨域的响应。 + +D. 因为前端框架(如 Vue/React)的安全机制,阻止了发起非同源的请求。 + +**正确答案:** C + +**作答答案:** C + +> [!tip] 跨域三大解决方案全景图 +> +> ```mermaid +> graph LR +> A["跨域问题"] --> B["开发环境"] +> A --> C["生产环境"] +> B --> B1["Vite Proxy
Node 中转"] +> C --> C1["Nginx 反向代理
同域名免跨域"] +> C --> C2["CORS 中间件
服务端放白名单"] +> ``` +> +> **Go 后端 CORS 中间件示例:** +> +> ```go +> import "github.com/gin-contrib/cors" +> +> r := gin.New() +> r.Use(cors.New(cors.Config{ +> AllowOrigins: []string{"https://www.myweb.com"}, +> AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, +> AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, +> AllowCredentials: true, +> MaxAge: 12 * time.Hour, +> })) +> ``` + +--- + +### 六、响应与状态码 + +> [!abstract] 本节覆盖:第 28、29 题(续) +> +> 正确使用 HTTP 状态码是专业后端的基本素养。 + +#### 第 28 题 + +【单选题】前端向你的Go后端发送了一个请求,但忘了带上认证Token。作为标准化的后端,你应该返回哪个状态码来明确告知他"你没有权限/未认证"?(5分) + +A. 400 Bad Request + +B. 401 Unauthorized + +C. 404 Not Found + +D. 500 Internal Server Error + +**正确答案:** B + +**作答答案:** B + +> [!note] 常用 HTTP 状态码速查 +> +> | 状态码 | 含义 | 何时使用 | +> |--------|------|---------| +> | **200** | OK | 请求成功 | +> | **201** | Created | 资源创建成功 | +> | **301** | Moved Permanently | 永久重定向 | +> | **304** | Not Modified | 资源未变化(缓存) | +> | **400** | Bad Request | 请求参数错误 | +> | **401** | Unauthorized | 未认证/Token失效 | +> | **403** | Forbidden | 已认证但无权限 | +> | **404** | Not Found | 资源不存在 | +> | **429** | Too Many Requests | 触发限流 | +> | **500** | Internal Server Error | 服务端内部错误 | + +--- + +### 七、Git 版本控制 + +#### 第 30 题 + +【单选题】在Git中,如何临时保存当前工作进度以便切换到其他分支进行工作?(5分) + +A. 使用 git stash apply 命令 + +B. 使用 git commit 命令 + +C. 使用 git stash 命令 + +D. 使用 git merge 命令 + +**正确答案:** C + +**作答答案:** C + +> [!code Bash] git stash 常用命令 +> +> ```bash +> git stash save "WIP: 修复bug" # 保存当前改动 +> git stash list # 查看保存列表 +> git stash pop stash@{0} # 恢复并删除栈顶记录 +> git stash apply stash@{0} # 恢复但不删除(保留记录) +> git stash drop stash@{0} # 删除指定记录 +> git stash drop # 删除最近一条(快捷写法) +> ``` + +--- + +## 关联笔记 + +- [[hhs/EXAM/]] +- [[Go/Gin框架/中间件]] +- [[Web/CORS跨域]] +- [[Web/JWT认证]] diff --git a/hhs/EXAM/Week04.md b/hhs/EXAM/Week04.md new file mode 100644 index 0000000..123a92f --- /dev/null +++ b/hhs/EXAM/Week04.md @@ -0,0 +1,804 @@ +--- +tags: [] +create time: 2026-05-14 10:30 +--- + +# 金山考试 - 武科大 - 第四次课(React 核心) + +## 概述 + +本次考试围绕 **React 核心概念与性能优化**,涵盖以下知识点: + +- useState / useEffect / useMemo / useCallback 等 Hooks 的使用与陷阱 +- React.memo 与组件渲染行为控制 +- 单向数据流与组件通信模式(父子/兄弟/跨级) +- Context API 的作用域与注意事项 +- React Router 路由机制( BrowserRouter、Route、动态参数) +- Redux 状态管理(Action、Reducer、中间件) +- Axios 拦截器封装最佳实践 +- 大列表渲染与 Vite 打包性能优化 +- 前端监控埋点方案对比(GIF vs AJAX) + +## 我的错题 + +> [!FAIL] ❌ 错题记录 +> 以下题目作答错误,标注解题思路与关键误区。 + +### Q14 — 兄弟组件通信 + +| 项目 | 内容 | +|------|------| +| 官方答案 | C(Context) | +| 你的答案 | D(Redux) | + +> [!NOTE] 💡 为什么选 Context? +> Redux 属于**全局状态管理库**,适用于跨多个不相关组件共享复杂状态的**大型项目**。而本题限定为「**兄弟组件之间的简单通信**」,使用 Context 更轻量、更符合场景定位——通过一个公共父组件创建 Context,两个兄弟组件分别消费即可,无需引入额外依赖。 + +### Q29 — React.memo + 对象 prop + +| 项目 | 内容 | +|------|------| +| 官方答案 | D(父组件每次创建新对象引用) | +| 你的答案 | (正确 ✅) | + +> [!NOTE] ℹ️ 标记已纠正 +> 此题已掌握,保留备忘。React.memo 基于**浅比较**(shallow equal),只要 prop 引用地址变了就会重新渲染。**每次渲染都 new 一个对象**会导致引用始终不同,`useMemo` 可以缓存引用解决这个问题。 + +--- + +## 武科大 - 第四次课 + +### 一、单选题 + +#### 1. useState 闭包陷阱 + +【单选题】点击按钮后,以下 React 代码,console 的输出和按钮中的展示分别是什么?(2分) + +```js +function Counter() { + const [count, setCount] useState(0); + + const handleClick = () => { + setCount(count + 1); + setCount(count + 1); + setCount(count + 1); + console.log(count); + }; + + return ; +} +``` + +A. 3, 3 +B. 0, 1 +C. 0, 3 +D. 3, 1 + +**正确答案:** B +**作答答案:** C + +> [!TIP] 🔑 核心考点 +> `setCount(count + 1)` 三次调用中,`count` 是**同一闭包快照值 0**,所以实际等价于 `setCount(1)` 执行三次。但 React 会**批量合并**同一次事件中的状态更新,最终只渲染一次 → count = 1。 +> +> ⚠️ 若用函数式更新 `setCount(prev => prev + 1)` 三次,则结果为 3。 + +--- + +#### 2. React.memo + 对象 prop + +【单选题】考虑一个使用 `React.memo` 包装的 `Child` 组件。如果父组件传递一个对象 `prop`,以下哪种方法可以确保 `Child` 组件在父组件重新渲染时不会不必要地重新渲染? + +A. 将 `Child` 组件从 `React.memo` 中移除 +B. 在 `Child` 组件内部使用 `useEffect` 来比较 `prop` +C. 将对象 `prop` 传递给 `useMemo`,并将其作为 `Child` 的 `prop` +D. 将对象 `prop` 的所有属性分别作为独立的 `prop` 传递给 `Child` + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 3. 父组件状态更新 → 子组件渲染 + +【单选题】在 React 中,当父组件状态更新时子组件的渲染行为是怎样的?(2分) + +A. 子组件不会重新渲染,除非子组件自身的状态或属性变化 +B. 子组件会随父组件的重新渲染而无条件重新渲染 +C. 子组件仅在其接收的 props 发生变化时重新渲染 +D. 父组件的渲染不影响子组件 + +**正确答案:** B +**作答答案:** B ✅ + +> [!NOTE] ℹ️ 深入理解 +> 父组件 re-render 会**默认触发所有子组件** re-render,无论 props 是否改变。如需跳过,可使用 `React.memo`、`useMemo`、`useCallback` 等手段进行优化。 + +--- + +#### 4. useMemo vs useCallback + +【单选题】React 中 useMemo 和 useCallback 的主要区别是什么?(2分) + +A. useMemo 返回一个 memoized 值,useCallback 返回一个 memoized 函数 +B. useMemo 和 useCallback 效果完全一样 +C. useCallback 可以依赖多个变量,而 useMemo 只能依赖一个变量 +D. 没有区别,两者在 React 中可以互换使用 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 5. useCallback 的主要用途 + +【单选题】在 React 中 useCallback 的主要用途是什么?(2分) + +A. 用于在组件之间传递状态 +B. 用于避免组件中的函数在每次渲染时都重新创建 +C. 用于直接修改组件的 state +D. 用于数据的结构化管理 + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 6. React 性能优化手段 + +【单选题】下面哪些是 React 性能优化的手段?(2分) + +```js +// 方案1 — useMemo 缓存计算结果 +const memoizedValue = useMemo(() => computeValue(a), [a]); + +// 方案2 — useCallback 缓存函数引用 +const handleClick = () => { /*...*/ }; +const memoizedHandle = useCallback(handleClick, []); +``` + +A. 仅方案1 +B. 仅方案2 +C. 两者都需要 +D. 都不需要 + +**正确答案:** C +**作答答案:** C ✅ + +> [!TIP] 🔑 何时使用? +> - `useMemo`:适合**昂贵计算**的结果缓存 +> - `useCallback`:适合**传给子组件作为 prop 的函数**,配合 `React.memo` 防止子组件无谓重渲染 + +--- + +#### 7. 单项数据流 + +【单选题】在 React 中,以下关于单项数据流的描述,哪个是正确的?(3分) + +A. 数据可以在组件间随意双向流动 +B. 数据只能从父组件流向子组件,子组件不能直接修改父组件的数据 +C. 数据从子组件流向父组件,父组件接收数据后再分发给其他子组件 +D. 数据在同级组件间可以自由流动 + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 8. React Router 核心原理 + +【单选题】React Router 的核心原理是基于以下哪种机制来实现路由匹配和渲染的?(2分) + +A. 事件监听 +B. 虚拟 DOM 比对 +C. URL 变化监听和组件映射 +D. 状态管理 + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 9. 路由根组件 + +【单选题】在 React 中使用路由时,用于包裹整个应用路由组件的根组件是?(3分) + +A. Route +B. BrowserRouter +C. Switch +D. Link + +**正确答案:** B +**作答答案:** B ✅ + +> [!NOTE] ℹ️ 注意版本差异 +> React Router v5 用 `BrowserRouter` + `Switch`;v6 改用 `BrowserRouter` + `` + ``。Switch 在 v6 已被废弃。 + +--- + +#### 10. React Context 错误说法 + +【单选题】关于 React Context 的说法,错误的是?(3分) + +A. 适合跨层级传递 props(避免 props drilling) +B. 每次 Context 值变化时,所有消费组件都会重渲染 +C. 可以通过 useContext Hook 或 Consumer 组件使用 +D. 不能与类组件一起使用 + +**正确答案:** D +**作答答案:** D ✅ + +> [!TIP] 🔑 Context 常见误区 +> Context 本身**不优化性能**——值变化时所有 consumer 都会重渲染。应结合 `useMemo` 分割 context 或将消费逻辑抽取到自定义 Hook 中以缩小更新范围。 + +--- + +#### 11. Fragment 主要作用 + +【单选题】在 React 中,Fragment 的主要作用是?(3分) + +A. 为组件添加额外的 DOM 节点 +B. 替代 div 减少冗余 DOM 层级 +C. 优化组件的渲染性能 +D. 包裹多个子元素且不添加额外节点 + +**正确答案:** D +**作答答案:** D ✅ + +> [!NOTE] ℹ️ 两种写法 +> - 完整语法:`` 或 `` +> - 简写语法:`<>...` (不支持 key 属性) + +--- + +#### 12. 按需加载 + +【单选题】React Router 中如何实现按需加载?(3分) + +A. 使用 React.lazy 和 React.Suspense 实现 +B. 使用 webpack 的 Code Splitting 实现 +C. 使用 React Router 自带的按需加载功能实现 +D. 使用 ES6 的 import() 函数实现 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 13. 父向子传数据 + +【单选题】在 React 中,如何实现父组件向子组件传递数据?(3分) + +A. 使用 state 属性 +B. 使用 props 属性 +C. 使用 context 属性 +D. 使用 ref 属性 + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 14. 兄弟组件通信 + +【单选题】在 React 中,实现兄弟组件之间的通信,相对来说下面哪个比较合适?(3分) + +A. 使用 props +B. 使用 state +C. 使用 context +D. 使用 Redux + +**正确答案:** C +**作答答案:** D + +--- + +#### 15. 子传父 + +【单选题】子传父在 React 中通常通过什么实现?(3分) + +A. 回调函数 +B. 事件 +C. 状态提升 +D. 上升至共同的父组件 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 16. React.createElement + +【单选题】React.createElement 用于做什么?(3分) + +A. 创建类组件实例 +B. 创建函数组件实例 +C. 创建 DOM 元素 +D. 创建 React 元素 + +**正确答案:** D +**作答答案:** D ✅ + +--- + +#### 17. 获取 DOM 引用 + +【单选题】如果需要在 React 组件中保持对 DOM 元素的引用,应该使用哪个钩子?(3分) + +A. useState +B. useEffect +C. useRef +D. useReducer + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 18. useState 返回值 + +【单选题】在 React 中,useState 钩子的返回值包含什么?(3分) + +A. 一个包含状态和更新状态的函数 +B. 一个包含副作用和清理副作用的函数 +C. 一个包含 DOM 引用和更新 DOM 的方法 +D. 一个包含生命周期方法的对象 + +**正确答案:** A +**作答答案:** A ✅ + +> [!NOTE] ℹ️ 解构方式 +> ```js +> const [count, setCount] = useState(0); +> // ↑ 更新函数(非 setter) +> // ↑ 当前状态值 +> ``` + +--- + +#### 19. useEffect 清除副作用 + +【单选题】在 React 中,如何清除 useEffect 钩子中设置的副作用?(3分) + +A. 返回一个函数 +B. 使用 useRef +C. 使用 useState +D. 使用 useMemo + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 20. useEffect 主要用途 + +【单选题】在 React 中,useEffect 钩子的主要用途是什么?(3分) + +A. 声明性地处理组件的副作用 +B. 声明性地处理组件的样式 +C. 声明性地处理组件的事件 +D. 声明性地处理组件的渲染 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 21. 动态参数写法 + +【单选题】在路由路径中定义动态参数(如用户 ID)的正确写法是?(3分) + +A. `/user?id=123` +B. `/user/:id` +C. `/user/{id}` +D. `/user[id]` + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 22. 路由匹配组件 + +【单选题】以下哪个组件用于定义路由匹配规则?(3分) + +A. Link +B. NavLink +C. Route +D. Switch + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 23. 全局状态管理适用场景 + +【单选题】以下哪种场景最适合使用全局状态管理库(如 Redux)?(3分) + +A. 多个不相关组件共享复杂状态 +B. 单一组件内部状态管理 +C. 兄弟组件简单数据传递 +D. 跨页面数据持久化 + +**正确答案:** A +**作答答案:** A ✅ + +> [!TIP] 🔑 选型决策树 +> +> ```mermaid +> graph LR +> A[需要共享状态?] -->|否| B[用 useState/useRef] +> A -->|是| C{是否在同一个组件树中?} +> C -->|是| D[父→子 props / 状态提升] +> C -->|否| E{涉及组件数量?} +> E -->|少且相邻| F[Context] +> E -->|多且不相关| G[Redux/Zustand] +> ``` + +--- + +#### 24. Redux reducer + +【单选题】在 React-Redux 中,什么是 reducer?(3分) + +以下哪个描述最准确地解释了 React-Redux 中的 reducer? + +A. reducer 是一个纯函数,它接收一个先前的状态和一个 action,返回一个新的状态 +B. reducer 是一个对象,它定义了应用程序的状态和行为 +C. reducer 是一个数组,它包含了应用程序的所有状态 +D. reducer 是一个类,它封装了应用程序的所有逻辑 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 25. Redux action + +【单选题】在 React-Redux 中,什么是 action?(3分) + +以下哪个描述最准确地解释了 React-Redux 中的 action? + +A. action 是一个对象,它描述了发生了什么事件,它包含了 type 和 payload 属性 +B. action 是一个函数,它定义了应用程序的行为 +C. action 是一个数组,它包含了应用程序的所有状态 +D. action 是一个类,它封装了应用程序的所有逻辑 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 26. Action 与 Reducer 关系 + +【单选题】Redux 中的 action 和 reducer 有什么关系?(3分) + +在 Redux 中,action 和 reducer 是组成状态管理的两个重要概念。请问,action 和 reducer 之间的关系是什么? + +A. action 用于描述应用状态的结构,reducer 用于修改状态 +B. action 用于存储应用状态的数据,reducer 用于获取状态 +C. action 用于触发状态修改,reducer 用于响应状态修改 +D. action 用于监听状态变化,reducer 用于修改状态 + +**正确答案:** C +**作答答案:** C ✅ + +> [!NOTE] ℹ️ Redux 数据流全景 +> +> ```mermaid +> flowchart LR +> UI["组件发起 dispatch"] -->|Action| STORE["Store"] +> STORE -->|派发| REDUCER["Reducer 纯函数"] +> REDUCER -->|new State| STORE +> STORE -->|订阅更新| UI +> ``` + +--- + +#### 27. Redux 异步操作 + +【单选题】在 Redux 中,有时需要进行异步操作,比如发送网络请求。请问,以下哪个中间件可以用来实现异步操作?(3分) + +A. redux-thunk +B. redux-saga +C. redux-logger +D. redux-observable + +**正确答案:** A +**作答答案:** A ✅ + +> [!TIP] 🔑 常用中间件对比 +> +> | 中间件 | 特点 | +> |--------|------| +> | redux-thunk | 轻量,返回函数的 action creator,适合简单异步 | +> | redux-saga | 基于 Generator,适合复杂异步流程控制 | +> | redux-logger | 日志打印,不参与业务逻辑 | +> | redux-observable | 基于 RxJS,响应式编程风格 | + +--- + +#### 28. Axios 拦截器问题 + +【单选题】axios 封装请求拦截器时,以下代码有什么问题?(3分) + +```js +import axios from 'axios'; + +const instance = axios.create({ + baseURL: 'https://api.example.com', + timeout: 5000 +}); + +// 请求拦截器 +instance.interceptors.request.use( + config => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + error => Promise.reject(error) +); + +// 响应拦截器 +instance.interceptors.response.use( + response => response.data, + error => { + if (error.response?.status === 401) { + // token过期,跳转登录 + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default instance; +``` + +A. 没有问题,这是标准的 axios 封装 +B. token 过期时应该先尝试刷新 token +C. 直接操作 window.location 会导致 SPA 路由失效 +D. B 和 C 都对 + +**正确答案:** D +**作答答案:** D ✅ + +> [!TIP] 🔑 最佳实践 +> - **刷新 Token**:使用 refresh_token 流程重试原请求,而非直接跳登录 +> - **SPA 导航**:使用 `router.push('/login')` 而非 `window.location.href`,避免整页刷新丢失状态 + +--- + +#### 29. React.memo + 新对象引用 + +【单选题】在使用 `React.memo` 时,如果父组件传递一个对象作为 `prop`,以下哪种情况可能导致子组件仍然不必要的重新渲染?(3分) + +A. 父组件传递的是一个原始类型的值,例如数字 +B. 父组件传递的是一个使用 `useMemo` 缓存的对象 +C. 子组件内部的状态发生了变化 +D. 父组件每次渲染时都创建了一个新的对象 + +**正确答案:** D +**作答答案:** D ✅ + +--- + +### 二、多选题 + +#### 1. 大数据渲染优化 + +【多选题】项目中遇到大量数据渲染导致页面卡顿,以下哪些优化方案有效?(4分) + +```js +// 方案A:虚拟滚动 + + +// 方案B:分页加载 +const pageSize = 20; +const currentPage = ref(1); +const displayList = computed(() => { + return list.slice(0, currentPage.value * pageSize); +}); + +// 方案C:时间分片 +function renderWithTimeSlice(list) { + let current = 0; + const pageSize = 50; + function render() { + const fragment = document.createDocumentFragment(); + const end = Math.min(current + pageSize, list.length); + for (let i = current; i < end; i++) { + const item = createListItem(list[i]); + fragment.appendChild(item); + } + container.appendChild(fragment); + current = end; + if (current < list.length) { + requestAnimationFrame(render); + } + } + render(); +} + +// 方案D:Web Worker 处理数据 +const worker = new Worker('process-data.js'); +worker.postMessage(rawData); +worker.onmessage = (e) => { + renderList(e.data); +}; +``` + +A. 只渲染可视区域 +B. 按需加载 +C. 避免长时间阻塞 +D. 数据处理放在 Worker + +**正确答案:** ABCD +**作答答案:** ABCD ✅ + +> [!NOTE] ℹ️ 各方案定位 +> - **虚拟滚动**:DOM 节点数固定,滚动时复用节点 +> - **分页加载**:减少一次性渲染量,配合无限滚动体验更佳 +> - **时间分片**:将任务拆入微任务队列,避免主线程长时间占用 +> - **Web Worker**:计算密集型任务放后台线程,主线程只管渲染 + +--- + +#### 2. Vite 打包优化 + +【多选题】Vite 项目打包优化时,以下哪些配置是有效的?(4分) + +```js +// vite.config.js +export default { + build: { + rollupOptions: { + output: { + // 配置A:手动分包 + manualChunks: { + 'vendor': ['vue', 'vue-router', 'pinia'], + 'ui': ['element-plus'] + }, + // 配置B:第三方库按包名分组 + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0]; + } + } + } + }, + // 配置C:启用压缩 + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true + } + }, + // 配置D:调整 chunk 大小警告阈值 + chunkSizeWarningLimit: 2000 + } +}; +``` + +A. 配置 A 可以有效减少首屏加载时间 +B. 配置 B 比 A 更灵活,能自动分离第三方库 +C. 配置 C 能减小包体积,但会增加构建时间 +D. 配置 D 只是隐藏警告,不能真正优化性能 + +**正确答案:** ABCD +**作答答案:** ABC + +> [!FAIL] 💔 漏选分析 +> **配置 D 虽不直接优化性能,但它影响开发体验**——合理设置可避免频繁干扰,让真正的警告突出显示。这是一项工程化最佳实践。建议全选。 + +--- + +#### 3. 大列表渲染最佳实践 + +【多选题】以下性能优化的最佳实践是什么?(4分) + +```js +// 场景:大列表渲染 +const List = ({ items }) => { + return ( +
+ {items.map((item, index) => ( +
handleClick(item)}> + {item.name} +
+ ))} +
+ ); +}; +``` + +A. 使用 item.id 作为 key 而非 index +B. 使用虚拟滚动 +C. 使用 useMemo 包裹 items +D. onClick 使用 useCallback + +**正确答案:** ABD +**作答答案:** ABD ✅ + +> [!TIP] 🔑 为什么不选 C? +> `useMemo` 包裹 items 并不能直接优化列表渲染性能——items 来自 props,其变化由父组件决定。关键在于 **key 稳定性**(A)、**渲染策略**(B)和**事件处理器引用稳定**(D)。 + +--- + +#### 4. useEffect + 键盘事件监听 + +【多选题】关于下面代码说法正确的是?(4分) + +```js +useEffect(() => { + const onKeyDown = (e) => { + const userAgent = navigator.userAgent; + const isMac = /Macintosh|MacIntel|MacPPC|Mac68K/.test(userAgent); + if ((isMac && e.metaKey && e.key.toLowerCase() === 'k') || + (!isMac && e.ctrlKey && e.key.toLowerCase() === 'k')) { + e.preventDefault(); + inputRef.current?.focus(); + } + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); +}, []); +``` + +A. 在 Mac 设备上按下 Command+K 键,或在非 Mac 设备上按下 Ctrl+K 键时,让 inputRef 对应的输入框获取焦点 +B. 该 useEffect 仅在组件挂载时执行一次,因为其依赖数组为空 +C. 清理函数会在组件卸载时执行,用于移除添加的 keydown 事件监听,避免内存泄漏 +D. 清理函数的主要作用是阻止键盘事件的默认行为 + +**正确答案:** ABC +**作答答案:** ABCD + +> [!FAIL] 💔 易错点 +> **D 是错误的**:`e.preventDefault()` 阻止的是浏览器默认行为(如 Cmd+K 的搜索框聚焦),而清理函数 `removeEventListener` 的作用是**移除事件监听器本身**,二者职责完全不同。 + +--- + +#### 5. GIF 埋点 vs AJAX + +【多选题】相比较 AJAX,有些公司前端监控都喜欢用 GIF 做埋点上报,以下哪些是 GIF 的优势?(4分) + +```js +// GIF 方案 +const report = (data) => { + const img = new Image(); + img.src = `https://log.example.com/track.gif?data=${encodeURIComponent(JSON.stringify(data))}`; +}; + +// Ajax 方案 +const report = (data) => { + fetch('https://log.example.com/track', { + method: 'POST', + body: JSON.stringify(data) + }); +}; +``` + +A. GIF 请求不需要等待响应,发送即走 +B. GIF 可以跨域,不受 CORS 限制 +C. 页面卸载时 GIF 请求不会被取消 +D. GIF 体积最小(1x1 像素) + +**正确答案:** ABD +**作答答案:** ABD ✅ + +> [!NOTE] ℹ️ 为什么不用 C? +> 页面卸载(unload)时,`navigator.sendBeacon()` 可以保证请求发送,但普通 `` 请求**同样会被取消**,因为浏览器不再维护连接。GIF 的优势在于**零 CORS 限制**(资源加载不走 XHR 同源策略)和**极简体积**。 + +--- + +## 关联笔记 + +- [[hhs/EXAM/Week03]](推测:前三次课相关考题) +- [[hhs/EXAM/README]](考试文档索引) diff --git a/hhs/EXAM/Week05.md b/hhs/EXAM/Week05.md new file mode 100644 index 0000000..d84bb8f --- /dev/null +++ b/hhs/EXAM/Week05.md @@ -0,0 +1,1228 @@ +--- +tags: [MySQL, Docker, Gin, JWT, Protobuf, Nginx, CORS, Exam Review] +create time: 2026-05-14 10:30 +--- + +# 金山考试 - 武汉科技大学第5次考试复习笔记_26/04/16 + +## 概述 + +本文档是 **武汉科技大学第5次考试** 的完整题目回顾与知识点解析。本次考试涵盖 8 大技术领域:**Docker、MySQL、Gin 框架、Go 并发编程、JWT & Session、Protobuf、Nginx & CDN、CORS**。以下内容将原始 40 道单选题和多选题按知识模块重新组织,每道题保留原题和答案,并补充教学讲解、代码示例和图表。 + +> [!NOTE] 使用建议 +> - 先快速浏览所有错题标记(作答答案 ≠ 正确答案),确定薄弱模块 +> - 重点阅读每个模块的 **核心概念图** 和 **易错点提示** +> - 代码块均经过精简注释,核心逻辑一目了然 + +--- + +## 一、Docker 基础命令 + +### 核心概念图 + +```mermaid +graph LR + A[Dockerfile] -->|docker build| B[镜像 Image] + B -->|docker run| C[容器 Container] + C -->|docker stop| D[已停止] + C -->|docker pause| E[已暂停] + B -->|docker push| F[仓库 Registry] +``` + +> [!TIP] Docker 核心工作流 +> **Dockerfile → 构建镜像 → 运行容器 → 推送仓库**,这是最完整的开发到生产流程。 + +### 原题回顾 + +#### 第1题:停止容器 + +> [!EXAMPLE] 易错提醒 +> `docker pause` 是"暂停"而非"停止"——它通过 cgroup 将容器进程挂起,内存中数据仍在;`docker stop` 则是发送 SIGTERM 信号后优雅终止。 + +1. + +【单选题】以下哪个命令用于在Docker中停止一个正在运行的容器?(5分) + +A. `docker stop` + +B. `docker pause` + +C. `docker end` + +D. `docker halt` + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第2题:构建镜像 + +> [!EXAMPLE] 关键字记忆 +> `build` 对应构建镜像,类似 `"builder"` → `"built image"`。记住 `Dockerfile` + `docker build` 的组合。 + +2. + +【单选题】哪个命令是用于在Docker中构建镜像的?(5分) + +A. `docker compose` + +B. `docker build` + +C. `docker make` + +D. `docker assemble` + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 第3题:容器与镜像的关系 + +> [!INFO] 类比理解 +> **镜像 = 类(Class)**,**容器 = 实例(Instance)**。就像你可以从同一个类创建多个对象实例一样,你也可以从同一个镜像启动多个容器。 + +3. + +【单选题】关于Docker容器和镜像的关系正确的是?(5分) + +A. 容器是镜像的一个实例 + +B. 镜像可以包含多个容器 + +C. 容器和镜像是两个没有联系的独立组件 + +D. 容器比镜像更早出现于Docker技术中 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第4题:查看运行容器 + +4. + +【单选题】在Docker中,通过哪条命令可以查看所有当前运行的容器?(5分) + +A. `docker ps` + +B. `docker view` + +C. `docker list` + +D. `docker show` + +**正确答案:** A +**作答答案:** A ✅ + +> [!NOTE] docker ps vs docker ps -a +> - `docker ps`:仅显示正在运行的容器 +> - `docker ps -a`:显示所有容器(包括已停止的) + +--- + +#### 第5题:创建并启动容器 + +5. + +【单选题】Docker 中创建并启动一个容器的正确命令是什么?(5分) + +A. `docker build -t myimage .` + +B. `docker create --name mycontainer myimage` + +C. `docker run --name mycontainer myimage` + +D. `docker start mycontainer` + +**正确答案:** C +**作答答案:** C ✅ + +> [!WARNING] create vs run +> `docker create` 仅创建容器但不启动;`docker run` 等价于 `create + start` 的组合。考试常考这个细微差别。 + +--- + +#### 第1题(多选):Docker 与虚拟机对比 + +> [!INFO] Docker vs 虚拟机核心对比 +> | 维度 | Docker 容器 | 虚拟机 (VM) | +> |------|-------------|-------------| +> | 启动速度 | 秒级 | 分钟级 | +> | 资源占用 | 共享宿主内核,轻量 | 独立 Guest OS,臃肿 | +> | 隔离性 | 进程级隔离 | 硬件级完全隔离 | +> | 镜像大小 | MB 级别 | GB 级别 | +> | 性能损耗 | 几乎无 | 虚拟化层开销 | + +1. + +【多选题】Docker与虚拟机的优劣势对比中,下列哪些是正确的?(5分) + +A. Docker容器启动比虚拟机更迅速 + +B. 虚拟机提供了更高的隔离性和安全性 + +C. Docker比虚拟机需要更多的硬件资源 + +D. Docker比虚拟机具有更快的性能提升潜力 + +**正确答案:** ABD +**作答答案:** ABD ✅ + +--- + +## 二、MySQL 索引与查询优化 + +### 覆盖索引流程图 + +```mermaid +flowchart TD + Q[SQL 查询] --> C{SELECT 字段是否都在索引中?} + C -->|是| Cover[✅ 覆盖索引 — 只需扫描索引树] + C -->|否| NoCover[❌ 需要回表 — 先查索引再查主表] + Cover --> Fast[🚀 性能高 — 减少 IO] + NoCover --> Slow[⏱️ 性能低 — 额外 IO 操作] +``` + +### 函数导致索引失效 + +```mermaid +flowchart LR + A["WHERE YEAR(create_time) = 2023"] -->|对字段做运算| B["💥 索引失效 — 全表扫描"] + C["WHERE create_time >= '2023-01-01'"] -->|范围比较,字段无修改| D["✅ 索引命中 — 范围扫描"] +``` + +> [!CRITICAL] ⚠️ 索引失效的核心规则 +> **对索引字段做任何运算(函数、表达式)都会导致索引失效**。因为索引存储的是原始值,无法匹配计算后的结果。 + +### 深分页对比 + +```mermaid +flowchart LR + A["LIMIT 100000, 10"] -->|传统写法| B["MySQL: 扫描 100010 行 → 丢弃前 100000 行"] + C["WHERE id > last_id LIMIT 10"] -->|游标翻页| D["MySQL: 直接定位到目标位置 → 只读 10 行"] +``` + +### 原题回顾 + +#### 第8题:索引失效的场景 + +> [!CAUTION] 高频考点 +> 这类题出现在几乎所有后端面试中。**记住口诀:字段不做函数运算,不等于号慎用,LIKE 不以 % 开头才可走索引。** + +8. + +【单选题】下列哪个WHERE子句中的条件最有可能导致create_time字段上的索引失效?(5分) + +A. `WHERE YEAR(create_time) = 2023` + +B. `WHERE create_time >= '2023-01-01' AND create_time < '2023-02-01'` + +C. `WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'` + +D. `WHERE create_time = '2023-06-01'` + +**正确答案:** A +**作答答案:** B ❌ + +> [!THINK] 💡 为什么选 A? +> `YEAR()` 是对 `create_time` 字段的函数运算,MySQL 在匹配时无法直接使用 B+ 树索引来加速查找,必须逐行计算判断。而 B、C、D 都是直接用原始值进行比较,可以正常走索引。 + +--- + +#### 第9题:深分页优化写法 + +9. + +【单选题】在大数据表进行LIMIT深分页时,下列哪种SQL写法具有优化作用?(5分) + +A. `SELECT * FROM table WHERE id > (SELECT id FROM table ORDER BY id LIMIT 100000, 1) LIMIT 10` + +B. `SELECT * FROM table LIMIT 100000, 10` + +C. `SELECT * FROM table WHERE name LIKE '%abc%' LIMIT 10` + +D. `SELECT * FROM table ORDER BY RAND() LIMIT 10` + +**正确答案:** A +**作答答案:** A ✅ + +> [!NOTE] A 选项的本质 +> 先用子查询找到第 100001 行的 id 值,然后用 `WHERE id > xxx LIMIT 10` 直接定位到目标页,避免扫描大量无用数据。注意:这只适用于有序且无删除空洞的场景。 + +--- + +#### 第10题:深分页优化技术 + +10. + +【单选题】在优化MySQL的深分页查询(如 LIMIT 100000, 10)时,推荐使用哪种技术以显著减少扫描的行数?(5分) + +A. 基于索引的子查询配合主键筛选 + +B. 添加更多字段到SELECT查询列表 + +C. 在ORDER BY后对LIMIT增加OFFSET参数 + +D. 增加连接数以提升并发执行 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第11题:覆盖索引的实际应用 + +> [!INFO] 什么是覆盖索引? +> 当 SQL 查询的所有字段都能从索引中直接获取,而不需要再去主表中查数据(即"回表"),就是使用了覆盖索引。 + +11. + +【单选题】如果有如下联合索引KEY(idx_col1_col2) (col1, col2),执行 `SELECT col1 FROM table WHERE col1=5` 时,下列哪项说法正确?(5分) + +A. 该查询可以使用覆盖索引,并且只扫描索引 + +B. 该查询不能使用索引 + +C. 该查询必须回表查询主表数据 + +D. 该查询会进行全表扫描 + +**正确答案:** A +**作答答案:** A ✅ + +> [!THINK] 💡 为什么能用覆盖索引? +> 联合索引 `(col1, col2)` 的 B+ 树叶子节点上存的就是 `(col1, col2)` 的值。既然只需要 `col1`,那直接在索引树上找到即可,无需回表。但如果查询的是 `SELECT col2 WHERE col1=5`,同样也能覆盖索引——因为联合索引包含了两个字段。 + +--- + +#### 第12题:覆盖索引的概念 + +> [!CAUTION] 错题回顾 +> 这道题你做错了,说明对覆盖索引的核心价值还不够清晰——它的本质是 **"只访问索引,不访问表"**。 + +12. + +【单选题】以下关于MySQL覆盖索引的说法,哪项是正确的?(5分) + +A. 覆盖索引可以减少IO,因为查询只需访问索引 + +B. 覆盖索引只能用于InnoDB引擎 + +C. 覆盖索引不能用于联合索引 + +D. 覆盖索引使用时必须回表查询原始数据 + +**正确答案:** A +**作答答案:** D ❌ + +--- + +#### 第13题:使用覆盖索引的条件 + +13. + +【单选题】在MySQL中,什么情况下可以使用覆盖索引来优化查询?(5分) + +A. 查询的字段全部被所用的索引覆盖,无需回表查询主表数据 + +B. 查询字段必须是聚合函数的字段 + +C. 只有当表中没有主键时才能使用覆盖索引 + +D. 只有当表有唯一索引时才能使用覆盖索引 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +## 三、MySQL 慢查询综合优化 + +### 优化策略金字塔 + +```mermaid +quadrantChart + title "MySQL 慢查询优化策略优先级" + x-axis "低实施难度" --> "高实施难度" + y-axis "高效益" --> "低效益" + "SQL改写": [0.15, 0.9] + "加索引": [0.3, 0.8] + "EXPLAIN分析": [0.1, 0.85] + "缓存层(Redis)": [0.5, 0.7] + "硬件升级": [0.9, 0.3] + "分库分表": [0.95, 0.6] +``` + +### 原题回顾 + +#### 第3题(多选):慢查询措施 + +3. + +【多选题】在优化MySQL数据库时,如果遇到慢查询,应该考虑采取哪些措施?(5分) + +A. 分析执行计划 + +B. 调整数据库系统的硬件配置 + +C. 优化程序的业务逻辑 + +D. 使用存储过程简化复杂查询 + +**正确答案:** ABCD +**作答答案:** ACD ❌ + +> [!THINK] 💡 为什么不排除 B 和 D? +> - **B 是合理的**:如果 SQL 已经无可优化空间,硬件升级(如换 SSD、加内存)确实能改善性能 +> - **D 在某些场景有效**:存储过程可以减少网络往返次数,适合批量处理场景 +> - 这是一个**多选题**,题目问的是"哪些应该考虑",而非"最佳方案是什么" + +--- + +#### 第4题(多选):提高查询性能的措施 + +4. + +【多选题】在进行MySQL性能优化时,可以采取哪些措施来提高查询性能?(5分) + +A. 使用索引来加速查询 + +B. 禁用所有外键以减少约束检查 + +C. 选择合适的存储引擎 + +D. 定期进行表碎片整理 + +**正确答案:** ACD +**作答答案:** ABCD ❌ + +> [!WARNING] 为什么排除 B? +> `禁用所有外键` 是错误的优化思路。外键保证数据完整性,盲目禁用会导致脏数据。如果性能瓶颈确实在外键检查,应该考虑架构层面的解耦(如用应用层校验替代数据库外键约束),而不是粗暴地"禁用所有"。 + +--- + +#### 第5题(多选):慢查询优化方式 + +5. + +【多选题】在Mysql中,针对慢查询优化有哪些有效方式?(5分) + +A. 可以通过为查询字段建立合适的索引以减少全表扫描 + +B. 查询时尽量避免使用 SELECT *,只查需要的字段有助于提升性能 + +C. 增加表的数据量可以显著提高查询效率 + +D. 可以通过分析慢查询日志定位并优化低效的SQL语句 + +**正确答案:** ABD +**作答答案:** ABD ✅ + +--- + +#### 第7题(多选):优化查询语句的方法 + +7. + +【多选题】优化MySQL查询语句的方法(5分) + +A. 使用正确的索引 + +B. 避免 SELECT *,只获取必需的列 + +C. 使用 GORM 可以提升查询性能 + +D. 使用 EXPLAIN 查看查询计划,然后针对性优化 + +**正确答案:** ABD +**作答答案:** ABCD ❌ + +> [!CAUTION] 为什么排除 C? +> GORM 是一个 ORM 框架,它能简化开发但**不会自动提升查询性能**——如果写出的 SQL 本身有问题(比如 N+1 查询),GORM 反而会生成更差的 SQL。性能优化取决于你写的查询语句本身。 + +--- + +#### 第8题(多选):索引优化的考虑因素 + +8. + +【多选题】关于MySQL数据库索引优化的考虑因素有哪些?(5分) + +A. 考虑为那些经常作为查询条件的列添加索引 + +B. 为经常要排序、分组和联合操作的字段建立索引 + +C. 为经常更新的字段添加索引 + +D. 避免为表中的每个列都建立索引 + +**正确答案:** ABD +**作答答案:** ABCD ❌ + +> [!CAUTION] 为什么排除 C? +> **索引不是免费的!** 每次 INSERT/UPDATE 都需要更新索引树,频繁更新的字段加索引会显著拖慢写入性能。索引的原则是"多读少写的字段优先建索引"。 + +--- + +## 四、MySQL 多选型错题精讲 + +#### 第6题(多选):Docker 数据持久化 + +> [!CAUTION] 错题回顾 +> 你对 Docker Volume 和 Bind Mount 的区别还需要理清。 + +6. + +【多选题】在使用Docker时,如何实现数据的持久化存储?(5分) + +A. 通过配置 tmpfs 挂载 + +B. 使用 Docker Volume + +C. 创建 Bind Mounts + +D. 在容器内部使用外部存储设备 + +**正确答案:** BC +**作答答案:** BD ❌ + +> [!THINK] 💡 tmpfs vs Volume vs Bind Mount +> - **tmpfs 挂载**:存在宿主机内存中,容器重启后数据丢失 → ❌ 不是持久化 +> - **Docker Volume**:由 Docker 管理,存储在 `/var/lib/docker/volumes/`,容器删除后数据还在 → ✅ +> - **Bind Mounts**:绑定宿主机目录到容器,生命周期一致 → ✅ +> - 选 B 和 C + +--- + +#### 第9题(多选):Protobuf 优势 + +> [!INFO] Protobuf 核心特性速查表 +> | 特性 | JSON | Protobuf | +> |------|------|----------| +> | 大小 | 较大(文本格式) | 小(二进制编码) | +> | 解析速度 | 中等 | 快 | +> | 人类可读 | ✅ | ❌(需 .proto 文件) | +> | 向后兼容 | ✅ | ✅(靠 field number) | +> | 跨语言 | ✅ | ✅ | + +9. + +【多选题】当需要在不同的应用程序之间传输数据时,使用 Protobuf 有哪些优势?(5分) + +A. 数据压缩率高,节省存储空间 + +B. 直接可读,无需反序列化 + +C. 支持多种编程语言 + +D. 序列化过程速度快,对性能影响小 + +**正确答案:** ACD +**作答答案:** AD ❌ + +> [!CAUTION] 为什么选 A 不选 B? +> - **A 是对的**:Protobuf 二进制编码比 JSON 文本紧凑很多,尤其是数值类型用 varint 编码后更小 +> - **B 是错的**:Protobuf 是二进制格式,对人不可读,必须先反序列化才能得到结构化数据 + +--- + +#### 第10题(多选):Protobuf 序列化基础 + +> [!INFO] Protobuf 版本兼容性机制 +> Protobuf 通过 field number(字段编号)而非字段名来识别数据。添加新字段不影响旧客户端解析(向前兼容),删除旧字段不影响新客户端读取已有字段(向后兼容)。 + +10. + +【多选题】在使用 Protobuf 进行序列化和反序列化时,以下哪些说法是正确的?(5分) + +A. Protobuf 是一种轻量级的数据交换格式 + +B. Protobuf 只能用于 C++ 语言 + +C. Protobuf 版本支持向后兼容性和向前兼容性 + +D. Protobuf 的序列化格式比 JSON 小 + +**正确答案:** ACD +**作答答案:** ACD ✅ + +--- + +#### 第20题:Protobuf varint 编码 + +> [!INFO] Varint 编码原理 +> Protobuf 使用可变长度整数编码(varint),每个字节只用 7 位存储数据,最高位表示"是否还有后续字节"。小整数只需 1 个字节,大整数可能需要 2-5 个字节。相比固定长度的 int32/int64,在大多数场景下更省空间。 + +20. + +【单选题】在解析 Protobuf 的数据时,为什么需要使用 varint 编码的整数?(5分) + +A. 因为 varint 编码更节省空间,可以表示任意大小的整数。 + +B. 因为 varint 编码使整数的编码更简单。 + +C. 因为 varint 编码使数据解析更快。 + +D. 因为 varint 编码适合网络传输的不定长整数编码。 + +**正确答案:** A +**作答答案:** D ❌ + +--- + +## 五、Go 并发编程 + +### Goroutine 调度模型 + +```mermaid +flowchart TB + G[goroutines] --> S[Scheduler 调度器] + S --> M[M threads] + M --> W1["OS Thread 1 → g1, g3"] + M --> W2["OS Thread 2 → g2, g4"] + S -->|"协程切换开销 ~0.2μs"| P["⚡ 远轻于线程"] +``` + +> [!INFO] goroutine vs 线程 +> goroutine 初始栈仅 2KB(可动态伸缩),而系统线程通常是几 MB。Go 的 M:N 调度器在少量 OS 线程上复用成千上万 goroutine,因此并发成本极低。 + +### 原题回顾 + +#### 第7题:goroutine 输出顺序 + +7. + +【单选题】下列代码的输出结果是什么?(5分) + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + var wg sync.WaitGroup + for i := 0; i < 3; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + fmt.Println(i) + }(i) // 注意:传参确保每个 goroutine 拿到独立的 i + } + wg.Wait() +} +``` + +A. 打印 0 1 2,顺序不定 + +B. 打印三次 2 + +C. 编译错误 + +D. 打印顺序一定是 0 1 2 + +**正确答案:** A +**作答答案:** A ✅ + +> [!THINK] 💡 关键细节 +> 虽然传了 `(i)` 参数避免了闭包共享变量问题(所以不会全打印 2),但三个 goroutine 是并发执行的,谁先抢到 CPU 时间片不确定,所以打印顺序不定。`wg.Wait()` 只保证等所有 goroutine 执行完,不规定顺序。 + +--- + +## 六、Gin 框架 + +### Gin 上下文流转 + +```mermaid +flowchart LR + Client[客户端请求] --> Handler[Handler 处理] + Handler -->|"c.Set(key, val)"| Context["Gin Context\n(c.Get / c.ShouldBind)"] + Context --> Next[中间件 / 下一个 Handler] + Next --> Response[返回响应] +``` + +### 原题回顾 + +#### 第6题:获取路由参数 + +> [!INFO] Gin 参数获取速查 +> | 方法 | 用途 | 示例 URL | +> |------|------|----------| +> | `c.Param("id")` | 路由参数 | `/users/:id` | +> | `c.Query("page")` | Query 参数 | `/users?page=1` | +> | `c.DefaultQuery("page", "1")` | Query + 默认值 | `/users` | +> | `c.PostForm("name")` | Form 表单 | POST body | + +6. + +【单选题】在Gin框架中如何获取路由参数?(5分) + +A. `c.Param("param_name")` + +B. `c.GetQuery("param_name")` + +C. `c.Params("param_name")` + +D. `c.Query("param_name")` + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第18题:设置 JSON 响应 + +18. + +【单选题】在Gin中如何设置响应格式为JSON?(5分) + +A. `c.JSON(200, gin.H{"message": "ok"})` + +B. `c.SetJSON(200, gin.H{"message": "ok"})` + +C. `c.Response(200, "application/json", gin.H{"message": "ok"})` + +D. `c.SendJSON(200, gin.H{"message": "ok"})` + +**正确答案:** A +**作答答案:** A ✅ + +> [!NOTE] Gin.H 的本质 +> `gin.H` 就是 `map[string]interface{}` 的类型别名,简洁写法。底层调用 `c.Status(code).Render(200, h)` 自动设置 `Content-Type: application/json`。 + +--- + +## 七、Docker Compose & 工程实践 + +### Dockerfile 层缓存优化 + +```mermaid +flowchart LR + L1["COPY go.mod go.sum"] -->|"命中缓存 🟢"| L2["go mod download"] + L2 -->|"源码变了 ✏️"| L3["COPY . ."] + L3 -->|"编译 🔨"| L4["go build"] + L1 -.|-| L2b["未命中 🟡\n依赖文件变了,重建缓存"] + L2b --> L3b["COPY . ."] +``` + +> [!TIP] Dockerfile 分层缓存的黄金法则 +> **变化越少的层放前面,变化多的放后面**。这样大部分构建步骤可以直接复用缓存层,大幅提升构建速度。 + +### 多阶段构建对比 + +```mermaid +flowchart LR + subgraph Before["单阶段构建 ❌"] + B1["Go SDK + src + deps"] --> B2["Binary"] + B_end["最终镜像 ≈ 800MB"] + end + subgraph After["多阶段构建 ✅"] + A1["Builder Stage: Go SDK + src + deps"] --> A2["Binary"] + A_end["Final Stage: Binary only\n≈ 20MB"] + end +``` + +### 原题回顾 + +#### 第14题:Dockerfile 指令顺序 + +14. + +【单选题】在Go服务的Dockerfile中,如何安排COPY指令和依赖安装指令顺序以最大程度利用缓存?(5分) + +A. 先 COPY go.mod 和 go.sum,再执行依赖安装,然后再 COPY 源码 + +B. 全部文件一次性 COPY 到镜像后,统一安装依赖 + +C. 先安装依赖,再 COPY go.mod 和 go.sum + +D. 先 COPY 源码,再 COPY go.mod 和 go.sum,然后安装依赖 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第15题:多阶段构建适用场景 + +15. + +【单选题】在下列哪种场景下最推荐在 Dockerfile 中使用多阶段构建?(5分) + +A. 需要从源代码构建应用,并且只需最终运行文件进镜像 + +B. 仅需将静态资源服务器部署到镜像 + +C. 部署单一配置文件到镜像,且无需编译 + +D. 基础镜像极小,且无需进一步处理 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第16题:多阶段构建的好处 + +16. + +【单选题】在编写 Dockerfile 时,使用多阶段构建最主要的好处是什么?(5分) + +A. 可以显著减小最终镜像的体积,避免无关构建文件被包含 + +B. 可以自动部署更新到生产环境 + +C. 能够让 Dockerfile 支持热重载代码 + +D. 可以让 Docker 镜像自动支持跨平台运行 + +**正确答案:** A +**作答答案:** A ✅ + +--- + +#### 第21题:Docker Compose 容器通信 + +> [!INFO] Docker Compose 内置 DNS +> Compose 会自动为同一网络中的服务创建一个内部 DNS 服务器。服务名就是主机名,内部端口就是连接端口。 + +21. + +【单选题】在同一个 docker-compose.yml 文件中定义了一个前端 nginx 服务和一个后端 go-api 服务(均未特别配置外部网络)。如果前端容器内的服务想要调用后端接口,最正确、最符合容器化工程实践的通信地址应该怎么写?(5分) + +A. 必须通过宿主机的内网 IP 加上被映射出来的端口进行调用。 + +B. 必须在前端服务中使用 links: - go-api 指令后才能通信。 + +C. 直接使用后端服务在 YAML 中定义的服务名(即 go-api)作为域名,加上其容器内部端口号即可调用。 + +D. 前后端容器相互隔离,必须通过配置 Nginx 将后端暴露到外网后才能互相访问。 + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 第22题:depends_on 指令 + +> [!WARNING] depends_on 的限制 +> `depends_on` 只保证**启动顺序**,不保证**就绪状态**。Redis 容器启动了 ≠ Redis 服务已准备好接受连接。如果需要等待就绪,应结合 `wait-for-it.sh` 或 Healthcheck 机制。 + +22. + +【单选题】在一个包含 Go Web 服务和 Redis 缓存的微服务架构中,为了保证 Redis 容器在 Go 服务之前启动,应该在 Go 服务的配置中使用哪个指令?(5分) + +A. `network_mode: redis` + +B. `wait_for: redis` + +C. `depends_on: - redis` + +D. `links: - redis` + +**正确答案:** C +**作答答案:** B ❌ + +--- + +#### 第23题:Docker Compose 的核心作用 + +23. + +【单选题】关于 Docker Compose 的主要作用和核心机制,以下说法最准确的是?(5分) + +A. 它是一个用于打包单一 Docker 镜像的构建工具,类似于更高级的 Dockerfile。 + +B. 它是一个容器编排工具,主要用于定义和运行多容器的 Docker 应用程序,默认配置文件名为 docker-compose.yml。 + +C. 它是 Docker 官方提供的跨主机集群管理工具,专门用于生产环境的负载均衡。 + +D. 它只能用于管理无状态服务,无法处理需要挂载数据卷的数据库容器。 + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 第2题(多选):加快镜像构建 + +> [!TIP] Docker 镜像构建提速清单 +> 1. ✅ 使用 `.dockerignore` 排除不必要的文件 +> 2. ✅ 合并 RUN 指令减少层级 +> 3. ✅ 利用缓存:将不变的依赖放在前面 +> 4. ✅ 多阶段构建减小最终体积 +> 5. ✅ 使用 `--mount=type=cache` 挂载 Go/npm 缓存目录 + +2. + +【多选题】为了加快Docker镜像的构建速度,以下哪些操作是推荐的?(5分) + +A. 使用多阶段构建来优化镜像 + +B. 在构建过程中,频繁变换基础镜像 + +C. 最大限度地利用已有层的缓存 + +D. 将不变的依赖放在Dockerfile的前面 + +**正确答案:** ACD +**作答答案:** AD ❌ + +> [!THINK] 💡 为什么漏选 A? +> 多阶段构建虽然主要目的是减小镜像体积,但也间接提升了构建效率——因为最终的产物更精简,构建步骤也更清晰。B 明显错误(频繁换基础镜像完全违背了缓存原则)。 + +--- + +## 八、Nginx & CDN + +### Nginx 反向代理角色 + +```mermaid +sequenceDiagram + participant User as 用户浏览器 + participant Nginx as Nginx 反向代理 + participant Go as Go 后端 + participant CDN as CDN 边缘节点 + User->>Nginx: HTTPS 请求 :80/443 + alt 静态资源 + Nginx->>CDN: 转发到最近边缘 + CDN-->>User: 静态内容 + else API 请求 + Nginx->>Go: 反向代理到内部端口 + Go-->>Nginx: JSON 响应 + Nginx-->>User: 返回响应 + end +``` + +> [!INFO] 为什么前端要用 Nginx 挡一层? +> - **统一入口**:SSL 终止、请求过滤、路由分发在一处完成 +> - **静态服务**:Nginx 处理静态文件的性能远超业务框架 +> - **安全**:隐藏后端真实 IP 和端口,便于集中做限流和黑白名单 + +### 原题回顾 + +#### 第17题:CDN 的工作原理 + +17. + +【单选题】内容分发网络(CDN)如何帮助提高网站的加载速度?(5分) + +A. 通过将网站流量全部重定向到一个特定的服务器节点 + +B. 通过在地理上分布的网站服务器上缓存内容来降低延迟 + +C. 通过增加网站的图像和视频质量 + +D. 通过限制用户访问内容的次数 + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 第24题:Nginx 反向代理的意义 + +24. + +【单选题】当我们的全栈项目要部署到生产环境时,通常会在最外层挡一个 Nginx,并将外部请求"反向代理"给内部的 Go 应用程序。为什么要多此一举,而不是直接让 Go 监听 80/443 端口对外提供服务?(5分) + +A. 因为 Go 语言标准库的 net/http 性能太差,无法承受高并发,必须靠 Nginx 提速。 + +B. 因为 Go 程序无法处理 HTTPS 证书的加密解密。 + +C. Nginx 专精于静态文件的高效分发,并且能够作为统一入口隐藏后端真实 IP,还能轻松实现请求路由分发、负载均衡和限流,极大提升了整体架构的健壮性和安全性。 + +D. 这是操作系统的强制要求,所有的 Web 流量必须先经过 Nginx 才能到达用户态程序。 + +**正确答案:** C +**作答答案:** C ✅ + +> [!NOTE] Go vs Nginx 的性能对比 +> Go 的 `net/http` 性能非常强(Go 1.21 压测可达百万级 QPS),不需要 Nginx 来"提速"。Nginx 在这里的价值在于**架构层面**的多路复用、静态处理和安全管理,而非纯吞吐量。 + +--- + +## 九、HTTP 协议 & CORS + +### CORS 工作流程 + +```mermaid +flowchart TD + A[前端页面 http://app.com:3000] -->|"普通请求 → 带 Origin 头"| B[后端 API http://api.com:8080] + B -->|"同源 ✅ | 跨域 → 预检请求"| C{CORS 检查} + C -->|"Access-Control-Allow-Origin 匹配"| D["✅ 允许,返回数据"] + C -->|"不匹配 / 缺失"| E["❌ 浏览器拦截\nConsole 报 CORS error"] +``` + +> [!CRITICAL] CORS 的本质 +> CORS 是**浏览器的安全机制**,不是服务器端的限制。服务器完全可以收到跨域请求并返回数据——只是浏览器看到缺少正确的 CORS 头就会拒绝把响应交给 JS。 + +### 原题回顾 + +#### 第19题:HTTP 状态码 + +> [!INFO] 常用 HTTP 状态码速记 +> | 状态码 | 含义 | 场景 | +> |--------|------|------| +> | 200 | OK | 请求成功 | +> | 304 | Not Modified | 缓存命中,无需重新下载 | +> | 301 / 302 | Moved / Found | 永久/临时重定向 | +> | 401 | Unauthorized | 未认证 | +> | 403 | Forbidden | 已认证但无权 | +> | 404 | Not Found | 资源不存在 | +> | 500 | Internal Server Error | 服务端错误 | + +19. + +【单选题】哪种HTTP状态码代表"未修改"?(5分) + +A. 200 OK + +B. 304 Not Modified + +C. 404 Not Found + +D. 500 Internal Server Error + +**正确答案:** B +**作答答案:** B ✅ + +--- + +#### 第25题:Proxy 代理原理 + +25. + +【单选题】在前端开发阶段,我们通常会在 vite.config.js 或 vue.config.js 中配置 proxy 代理来解决跨域问题。这个 Proxy 底层的运作逻辑是什么?(5分) + +A. 它修改了浏览器的安全设置,让浏览器临时关闭了同源策略。 + +B. 它修改了前端发出的 HTTP 请求的 Origin,伪装成和后端同源。 + +C. 前端代码实际上把请求发给了 Vite 自带的本地 Node.js 开发服务器,开发服务器再把请求转发给真正的 Go 后端,最后把结果原样返回给前端。 + +D. 它自动为后端的 Go 代码注入了处理跨域的中间件。 + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 第26题:CORS 报错的根本原因 + +> [!CAUTION] 高频面试考点 +> CORS 是前后端分离开发中最常见的痛点之一,也是面试必考题。记住关键词:"**同源策略 → 浏览器安全机制 → 阻止跨域响应读取**"。 + +26. + +【单选题】前后端分离开发时,前端调用后端经常会遇到可怕的 CORS 报错。导致跨域报错的根本原因是什么?它为了解决什么痛点而存在?(5分) + +A. 因为 HTTP 协议不允许不同端口之间通信,这是底层网络协议的限制。 + +B. 因为后端的 Go 代码默认开启了防火墙,拦截了来自其他端口的请求。 + +C. 这是浏览器的"同源策略 (Same-Origin Policy)"导致的,为了防止恶意网站窃取用户的敏感数据(如 Cookie),浏览器强制拦截了跨域的响应。 + +D. 因为前端框架(如 Vue/React)的安全机制,阻止了发起非同源的请求。 + +**正确答案:** C +**作答答案:** C ✅ + +> [!THINK] 💡 如何在后端解决 CORS? +> ```go +> import "github.com/gin-contrib/cors" +> +> r.Use(cors.New(cors.Config{ +> AllowOrigins: []string{"http://localhost:3000"}, // 允许的前端地址 +> AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, +> AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, +> AllowCredentials: true, // 携带 Cookie 时必须设为 true +> MaxAge: 12 * time.Hour, // 预检请求缓存时间 +> })) +> ``` + +--- + +## 十、JWT & Session + +### Session vs JWT 对比 + +```mermaid +flowchart LR + subgraph SessionAuth["Session 认证"] + S1["客户端: 保存 SessionID (Cookie)"] + S2["服务端: 存储用户数据
Redis / 内存 / DB"] + S3["⚠️ 集群环境下需要 Session 共享"] + end + subgraph JWTAuth["JWT 认证"] + J1["客户端: 保存 JWT Token"] + J2["服务端: 用私钥验证签名
无需存储会话"] + J3["✅ 天然无状态,水平扩展友好"] + end +``` + +### Gin 中 JWT 信息传递流程 + +```mermaid +flowchart TB + Middleware["JWT 中间件\nParseString(bearer token)"] --> Valid{"Token 有效?"} + Valid -->|否| Reject["返回 401 Unauthorized"] + Valid -->|是| Set["c.Set claims.UserID / c.Set claims.Role"] + Set --> Handler["业务 Handler\nc.Get claims.UserID"] +``` + +> [!TIP] Gin Context 的正确用法 +> `c.Set` / `c.Get` 是基于 `context.Context` 的键值存储,生命周期限定在当前请求范围内,线程安全,是 Gin 中跨中间件和 Handler 传递信息的标准做法。 + +### 原题回顾 + +#### 第27题:JWT 解析后的最佳实践 + +> [!CAUTION] 错题回顾 +> 不要用 `c.Redirect` 来传递用户信息!Redirect 会中断当前请求流程,把用户带到另一个页面,这与"把用户信息传给后续 Handler"的需求完全不符。 + +27. + +【单选题】当中间件成功解析并验证了前端请求头中的 JWT 令牌后,最佳的代码实践是?(5分) + +中间件是查票员,查验完 JWT 后,通过 `c.Set()` 把乘客信息存储在当前 HTTP 请求的生命周期上下文中,后续函数可以安全、隔离地拿到用户信息。 + +A. 将解析出的用户 ID 保存到全局变量中。 + +B. 使用 `c.Redirect` 将请求重定向,并在 URL 参数里带上用户 ID。 + +C. 使用 `c.Set("userID", id)` 将用户信息存储在上下文中。 + +D. 将用户信息打包成 JSON 写回到响应体中,结束请求。 + +**正确答案:** C +**作答答案:** B ❌ + +--- + +#### 第28题:JWT 解决的痛点 + +28. + +【单选题】JWT 解决了传统 Session 架构的什么核心痛点?(5分) + +JWT 是"无状态"的,服务端不需要存储会话记录就能验证其有效性,从而解决了传统 Session 在服务器集群环境下的共享与同步问题。 + +A. JWT 对用户密码进行了极强的加密,即便被黑客拦截也绝对安全。 + +B. 传统 Session 强依赖于服务端内存或集中存储,JWT 是"无状态"的。 + +C. JWT 生成的字符串非常短,比传统的 SessionID 更省网络带宽。 + +D. JWT 只能在 Go 语言和前端之间解析,跨语言安全性极高。 + +**正确答案:** B +**作答答案:** B ✅ + +> [!NOTE] JWT ≠ 加密 +> JWT 默认是 Base64 编码(可读的),不包含加密。即使加了签名防止篡改,Token 内容本身仍然是明文的。因此**不要在 JWT 中存放敏感信息**(如密码、身份证号)。 + +--- + +#### 第29题:HttpOnly Cookie + +> [!TIP] Cookie 安全防护体系 +> | 属性 | 作用 | 防御目标 | +> |------|------|----------| +> | `HttpOnly` | JavaScript 无法读取 | XSS 攻击 | +> | `Secure` | 仅 HTTPS 传输 | 中间人窃听 | +> | `SameSite` | 控制跨站发送 | CSRF 攻击 | +> | `maxAge` | 过期时间 | 长期驻留风险 | + +29. + +【单选题】在使用 Gin 设置 Cookie 时,为了防止前端恶意脚本通过 JavaScript 读取到包含用户登录凭证的敏感 Cookie,你应该将哪个参数设置为什么值?(5分) + +httpOnly 是防御 XSS 攻击的利器。当它设为 true 时,浏览器会把这个 Cookie 藏好,只有在向后端发送 HTTP 请求时才会默默带上,前端的任何 JS 代码都无权读取。顺便提一下,secure 也很重要,它表示该 Cookie 只能在 HTTPS 加密连接下传输,用来防止网络中间人窃听。 + +A. maxAge 设置为 0 + +B. secure 设置为 true + +C. httpOnly 设置为 true + +D. domain 设置为 "localhost" + +**正确答案:** C +**作答答案:** C ✅ + +--- + +#### 第30题:Gin 中 Session 的使用 + +30. + +【单选题】关于 Gin 框架中 Session 的运用,以下说法正确的是?(5分) + +Session 的本质是服务器在后端存储了用户状态,只把一个无意义的、随机的 SessionID 通过 Cookie 发给前端保存。Gin 核心库为了保持轻量,并没有内置 Session,推荐使用第三方中间件。 + +A. Gin 官方核心库自带了完善的 Session 管理器,无需引入第三方包。 + +B. Session 数据默认以明文形式保存在客户端的浏览器中。 + +C. Session 的本质是服务器在后端存储用户状态,只把 SessionID 发给前端。 + +D. 只要用户关闭浏览器页面,服务器端的 Session 数据就会被自动删除。 + +**正确答案:** C +**作答答案:** C ✅ + +> [!INFO] Gin + Session 常见组合 +> ```go +> // 常用第三方 Session 中间件 +> import "github.com/gin-contrib/sessions" +> import "github.com/gin-contrib/sessions/redis" +> +> store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret")) +> r.Use(sessions.Sessions("session_name", store)) +> // 后续可通过 session.Get("user_id") 读取 +> ``` + +--- + +## 十一、多选题错题汇总 + +将所有做错的多选题汇总在此,方便集中复习: + +| 题号 | 知识点 | 你的答案 | 正确答案 | 失分原因 | +|------|--------|---------|---------|---------| +| 单选-8 | 索引失效 | B | A | 未能识别函数对索引的影响 | +| 多选-2 | Docker 构建加速 | AD | ACD | 漏选多阶段构建 | +| 多选-3 | 慢查询措施 | ACD | ABCD | 排除了合理选项(硬件/存储过程) | +| 多选-4 | 查询性能优化 | ABCD | ACD | 未排除"禁用外键"的错误选项 | +| 多选-6 | Docker 持久化 | BD | BC | 混淆了 Volume/Bind Mount/tmfs | +| 多选-7 | 查询优化方法 | ABCD | ABD | 误认为 ORM 能自动提升性能 | +| 多选-8 | 索引优化因素 | ABCD | ABD | 忽略了索引对写入性能的影响 | +| 多选-9 | Protobuf 优势 | AD | ACD | 未选"支持多种语言" | +| 单选-22 | depends_on | B | C | 拼写了不存在的指令 | +| 单选-27 | JWT 中间件 | B | C | 混淆了重定向与上下文传递 | +| 单选-20 | Protobuf varint | D | A | 理解不够精准 | + +> [!SUCCESS] 💪 复习建议 +> 从上面的表格可以看出,你的薄弱环节集中在: +> 1. **MySQL 索引原理**(函数导致索引失效、索引对写入的影响) +> 2. **Docker 概念辨析**(Volume vs tmpfs、depends_on 的作用) +> 3. **Protobuf 特性**(多语言支持) +> 下次考试前重点回顾这些模块的核心概念图。 + +--- + +## 关联笔记 + +- [[Docker 容器与镜像]] +- [[MySQL 索引优化]] +- [[Gin 框架使用]] +- [[JWT & Session 认证]] +- [[Protobuf 入门]] +- [[Nginx 反向代理]] +- [[CORS 跨域解决方案]] diff --git a/hhs/Redis/一文吃透/README.md b/hhs/Redis/一文吃透/README.md new file mode 100644 index 0000000..896de9c --- /dev/null +++ b/hhs/Redis/一文吃透/README.md @@ -0,0 +1,601 @@ +在分布式系统中,为了解决单点问题,通常会把redis服务部署到多个服务器上,满足故障恢复和[负载均衡](https://cloud.tencent.com/product/clb?from_column=20065&from=20065)等请求。 + +所谓的单点问题,就是某个服务器程序,只有一个节点(只有一个[物理服务器](https://cloud.tencent.com/product/cbm?from_column=20065&from=20065),来部署这个服务器程序)。 + +存在的问题: + +- 可用性问题:如果这个机器挂了,那么部署的redis服务就挂了,服务就中断了。 +- 性能/支持的并发量有限:毕竟一台主机的硬件资源(比如CPU,硬盘,网络带宽等等)是有限的,一个请求就要消耗一定量的硬件资源。一旦并发量太高,如果请求超过主机所能提供这些资源,那么可能就会出现异常,甚至主机直接宕机了。 + +在分布式系统中,往往希望有多个服务器来部署redis服务,从而构成一个redis集群,此时就可以让这个集群给分布式系统中的其他服务,提供更稳定/更高效的数据存储功能 。 + +主要存在以下几种部署方式: + +- 主从模式 +- 主从+哨兵模式 +- 集群模式 + +### 一,主从复制 + +##### 1,配置主从复制 + +**主从模式,即在若干个redis节点中,有的是主节点,有的是从节点 。** + +**redis主从模式中,从节点上的数据,不允许修改,只允许读取数据。要想修改数据,只能访问主节点。** + +假设有三个物理服务器(三个节点),都部署了redis服务,此时就可以把其中一个看作是主节点,其余两个看作是从节点。从节点上的数据要跟随主节点的数据而变化,从节点的数据要和主节点保持一致。 + +本来,在主节点上保存了一堆数据,现在引入从节点,就是要把主节点上的数据复制出来,放到从节点中。后续,主节点这里对数据有任何修改,都会把这样的修改同步给从节点。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/23bb7b483aa4bd35387d69ff325bb064.png) + +总结:主从模式,主要是针对读操作进行可用性和并发量的提高,而对于写操作来说,无论是可用性还是并发量,都很依赖与主节点,但是主节点又不能搞多个。 + +--- + +如何在一个云服务器上部署多个redis服务,按照主从模式,实现类似于分布系统的效果?(ubutun20.04) + +我们可以在一个云服务器主机上,运行多个redis-server进程,我们只需保证这几个服务的端口号不同即可。本来redis-server的默认端口号是6379,此时新启动的redis-server端口号就不能是6379了。 + +在这里我们创建两个从节点,端口号是6380和6381,而主节点就是原先的redis-server,端口号为默认的6379。 + +可以通过修改配置文件来改变: + +1,首先需要将配置文件拷贝两份 + +`cp /etc/redis/redis.conf slave1.conf` + +`cp /etc/redis/redis.conf slave2.conf` + +2,然后修改拷贝后的两个配置文件中的选项 + +修改端口号——`port 6380 port6381` + +将该服务设置为后台进程——`daemonize yes` + +只需修改这两个选项即可。 + +3,启动这两个redis-server(从节点) + +`redis-server slave1.conf` + +`redis-server slave2.conf` + +4,现在我们只是有了多个redis-server,还没有构成主从结构。 + +要想配置成主从结构,就需要使用slaveof。有如下三个方法: + +- 在配置文件中加入`slaveof{masterHost} {masterPort}`,之后随redis启动生效。 +- 在redis-server启动命令中加入--slaveof{masterHost}{masterPort}生效。 +- 直接使用redis命令:slaveof{masterHost}{masterPort}生效。 + +其中{masterHost}和{masterPort}分别指主节点的ip和端口。 + +5,在这里我们选择通过修改配置文件的方式来构成主从结构 + +在slave1.conf和slave2.conf配置文件中加上slaveof 127.0.0.1 6379即可,以6379的redis-server作为主节点。配置文件修改完毕之后,需要我们重新启动redis-server,配置文件才会生效,所以此时需要重启端口号为6380和6381的redis-server。 + +由于我们启动redis-server服务时,是使用`redis-server port`这样的方式启动的,所以在重启的时候,需要使用`kill -9`来杀掉该服务进程,然后再通过redis-server port重启即可。 + +而如果我们是使用`service redis-server start`来启动服务器,就需要使用`service redis-server stop`来终止程序。此时如果使用kill -9来终止服务进程,kill掉之后,这个redis-server进程能自动启动 。相当于有一个进程来专门监控指定服务器的运行状态,如果服务器挂了,会立即重新启动。 + +因此怎么启动的redis-server,就必须搭配对应的方式进行终止。 + +6,再次启动redis-server即可实现主从模式 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/7d0fdc53718347bf8c63695dd5f4375b.png) + +![](https://developer.qcloudimg.com/http-save/yehe-100000/f1ff89af0fc6999c1ec6ca1c34dd57d4.png) + +##### 2,断开主从复制 + +使用命令slaveof no one可以断开主从关系,这只是暂时的,当redis-server重启之后,主从关系就会恢复,所以要想一直断开主从关系,还是需要修改配置文件。 + +##### 3,拓扑 + +我们的redis-server服务器可能有很多个节点,如何组织这些节点? + +###### 一主一从结构 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/413ee0bc4aefee9b2cf0ee9db65d9abc.png) + +如果其他客户端想要读取数据,从主节点A或 从节点B读取都可以,两个服务器上的数据是一致的。 + +如果是写数据请求,那么只能由主节点A来完成。 + +如果此时写请求太多,也会给主节点造成 一定的压力。我们可以通过关闭主节点的AOF,毕竟写内存比写磁盘快,只打开从节点的AOF。但是这种设定,有一个严重缺陷,主节点一旦挂了,不能立即重启,因为主节点这边没有使用AOF保存数据,如果重启了,那么数据就会丢失,进一步的主从同步,也就使从节点上数据也丢失了。改进办法是,当主节点挂了,先从从节点那里获取AOF文件,再启动,这样就可以保证数据没有问题了。 + +###### 一主多从结构 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/44e367a0d68b1d47f05aeb3801c4ac0d.png) + +同理,如果是读请求,可以访问主节点,也可以访问从节点。 + +如果是写请求 ,是能访问主节点,主节点上的数据发生变化,就会把改变的数据同步给其他从节点。 + +这个结构存在的问题:同步操作是需要消耗网络带宽的,如果子节点太多,就会大大消耗主节点主机的硬件资源。 + +###### 树形结构 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/7bb71b3e886e429ea7f3d98caba3101a.png) + +主节点将数据同步给从节点B和从节点C,再由从节点B将数据同步给从节点D和从节点E。 + +此时主节点A就不需要太高的网络带宽,但此时数据同步的延时会更长。 + +##### 4,原理 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/6da6398227e8959c1bf54e8d65b1cd89.png) + +同步数据的命令:**`PSYNC replicationid offset`**,这个命令是从节点执行的。 + +其中replicationid是复制id,是主节点生成的,主节点在启动的时候就会生成。 + +当从节点和主节点建立了复制关系,就会从主节点上 获取到这个id,这个id就表明了当前从节点是从哪个主节点上获取的数据。 + +在主节点和从节点上一般都有两个replicationid:replicationid和replicationid2。其中replicationid2其备份的作用,比如下面的例子。 + +> 现在有一个主节点A和一个从节点B,A在启动的时候会生成一个replicationid,之后B获取到A的replicationid。如果A和B通信过程中出现了一些网络抖动,B可能会认为A挂了,此时从节点B就会晋升为主节点,并给自己生成一个replicationid,此时B的replicationid2就保存了之前获取到的A的replicationid。等到网络稳定,B还可以根据这个replicationid2再次与节点A建立主从关系。(但是主从复制这种方法,当主节点挂了,一般从节点是不会晋升为主节点的) 这个再次建立主从关系的过程,一般是需要手动完成的。当然,在下面的哨兵机制部分,可以自动完成这个过程。 + +offset表示偏移量 + +主节点和从节点都会维护这个偏移量(一个整数)。 + +主节点的偏移量:主节点可能会收到很多的修改操作的命令,每个命令都选哟占据几个字节。主节点会把这些命令的字节数进行累加,这个数字就是主节点的偏移量。 + +从节点的偏移量:描述了当前数据同步的进度,从节点会每秒上报自身的偏移量给主节点。 + +如果从节点和主节点的偏移量一致,就表述数据 同步完成了。 + +**综上,replicationid描述了从哪个主节点同步数据,offset表示同步数据的进度。如果两个机器的replicationid和 offset一样,就表示这两个机器上的数据一致。** + +**replicationid和offset就共同描述了一个"数据集合"。** + +###### PSYNC工作流程 + +PSYNC可以从主节点获取全量数据,也可以获取部分数据。 + +主要看offset的值,如果设为-1,表示全量获取,如果设置为具体的整数,则表示从当前整数位置来进行获取。 + +当然,从节点想要全量的获取数据,还是增量的获取数据,同时也取决于主节点,主节点会自行判定,看当前是否方便给部分数据,如果不方便,会直接给 全量数据。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/6fd30d0b139ae458ee2b1cff761ac2ce.png) + +###### 全量复制的流程 + +在一个从节点与主节点第一次建立主从关系的时候,就会涉及到数据的同步,而此时一般就是进行全量复制。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/f2396822b24165e86addb30fed4aece7.png) + +在进行全量复制的时候,主节点要进行生成rdb文件的操作,然后再把该文件通过网络传输发送给从节点,从节点也是先将该rdb文件保存,然后读取该文件来获取数据。 + +而redis也支持"无硬盘模式"(diskless):主节点生成的rdb的二进制数据,不直接保存到文件中,而是直接通过网络传输发送给从节点 ,省去了读写硬盘的操作,而从节点现在也可以省去这个操作了,直接将收到数据加载到内存中...... + +但是,即使引入了"无硬盘模式",对全量复制的效率提升不是很大,因为全量复制整个操作是比较重量的,数据规模较大。相比于网络传输,读写硬盘操作算是快的了。所以减少了对硬盘的操作,但网络传输无法省去,意味着对整个操作的效率提升不大。 + +###### 部分复制流程 + +在主节点与从节点连接的过程中,可能由于网络抖动等原因,主从节点的连接断开,此时重新建立连接并进行数据同步的时候,使用的就是部分复制。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/83577a7663c06a47a5b627bee9024a40.png) + +###### 实时复制 + +全量复制:主要适用于从节点刚连上主节点进行数据初始化的工作。 + +部分复制:是全量复制的一种特殊情况,属于全量复制的一种优化。 + +实时复制:在从节点与主节点建立好连接,并且完成数据同步之后,此时主节点可能会受到源源不断的请求,其中就会包含修改数据的请求,主节点的数据就会发生变化,此时就要把数据也同步给从节点。从节点和主节点之间建立有Tcp连接,当主节点收到修改数据的请求,就会通过该Tcp连接,将这个请求发送给从节点,从节点再根据这些请求修改数据即可。 + +所以在实时复制的时候,我们需要保证主节点与从节点的Tcp连接处于可用状态。在这里,使用**心跳包机制**来实现。 + +> **心跳包机制:** 主节点:默认每隔10s,向从节点发送一个ping命令,从节点收到后会返回一个"pong"。 从节点:默认每隔1s,向主节点发送一个特定的请求,告诉主节点当前的同步进度(offset),之后主节点也会给出一个响应。 如果,达到某个阈值,还没有收到响应,那么就认为这个主节点/从节点存在问题,判断下线了。 + +##### 5,总结 + +主从复制解决的问题:单点问题 + +单点问题:单个redis节点,可用性不高,性能有限。 + +主从复制的特点: + +1,主节点可以用来读写,从节点只能用来读,可以减少主节点的访问压力。 + +2,主从复制存在多种拓扑结构:可以在适当的场景使用适当的拓扑结构,比如一主多从的结构,同步操作快,但是消耗的网络资源多,因为主节点要通过网络和所有从节点实现同步。而树形结构,主节点消耗的网络资源减少了,但是如果树的层级太高,会造成数据同步的延迟增长加。 + +3,复制分为全量复制,部分复制和实时复制。 + +4,通过心跳机制保证主节点和从节点的正常通信和数据一致。 + +主从复制的缺点: + +1,从节点多了,数据复制的延时就会非常明显。 + +2,如果主节点挂了,从节点不会晋升为主节点,需要通过人工干预的方式恢复。 + +### 二,哨兵(Sentinel) + +主从复制最大的问题,还是在主机点上,如果主节点挂了,从节点不会晋升为主节点,需要通过人工干预的方式恢复。 + +因此Redis哨兵机制,就是为了解决上述问题,自动的对挂了的主节点进行替换,也就是将上述手动的过程改成自动。 + +哨兵机制是通过启动一个不同的进程来体现的,它和redis-servver服务器进程不是 同一个进程 。 + +##### 1,相关名词解释 + +|名词|逻辑结构|物理结构| +|---|---|---| +|主节点|Redis服务|一个独立的redis-server进程| +|从节点|Redis服务|一个独立的redis-server进程| +|Redis数据节点|主从节点|主节点和从节点的进程能| +|哨兵节点|监控Redis数据节点的节点|一个独立的redis-sentinel进程| +|哨兵节点集合|若干哨兵节点构成的整体|若干redis-sentinel进程| +|Redis哨兵(Sentinel)|Redis提供的高可用方案|哨兵节点集合和Redis主从节点| +|应用方|一个或多个客户端|一个或多个连接Redis的进程| + +##### 2,哨兵自动恢复主节点故障 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/aa54d94afbba42a8bbcfa3a814b82010.png) + +如果一个哨兵节点发现主节点挂了,为了防止预判,还需多个哨兵节点共同认为这件事情。 + +如果主节点确实挂了,这些哨兵节点会选出一个作为leader,由这个哨兵节点负责从剩下的从节点中选一个出来,作为主节点。选出新的主节点之后,哨兵节点就会控制该节点执行slaveof no one,并且通知其他从节点,修改slaveof到新的主节点之上。哨兵节点会自动通知客户端程序,告知新的主节点是谁,并且此后客户端再进行写操作时,就会访问新的主节点了。 + +##### 3,哨兵机制的特点 + +监控:Sentinel节点会定期检查redis数据节点,使用心跳包机制。 + +故障转移:实现从节点晋升为主节点,并维护好正确的主从关系。 + +通知:Sentinel会将故障转移的结果通知给应用方。 + +##### 4,主从切换的具体流程 + +_**重点,面试题:**_ + +1,主观下线:哨兵节点通过心跳包进制,判读redis主节点服务器是否正常工作,如果没有沙鸥到响应,该哨兵节点就会认为该主节点下线了。 + +2,客观下线:当多个哨兵节点都认为主节点挂了之后,此时这个主节点就是主观下线了。 + +3,再从多个哨兵节点中,选举出一个leader节点,由这个leader节点负责从剩下的节点中选出一个作为主节点。 + +4,leader挑选完毕后,此时需要从剩下的从节点中选一个当作新的主节点。 + +- 首先会看这些从节点的优先级,每个 redis数据节点,都会有自己的配置文件,配置文件中就有一个优先级的设置。leader会选择优先级高的作为新的主节点。 +- 如果数据节点的优先级都一样,那么就比较这些数据节点的offset,offset表示从节点与原来主节点进行数据同步的进度,offset越大,说明这个节点与原来主节点的数据相似度最高,此时就会选择这个节点作为新的主节点。 +- 如果前面两个条件都一样,此时其实意味着从这些节点中任选一个都行。那么再看这些节点的runid(一串数字),每个redis数据节点启动时都会生成一个随机的runid,此时比较runid的大小。让runid更小的作为新的主节点。 +- 指定好新的主节点之后,此时leader就会控制这个主节点执行slaveof no one,让这个节点成为主节点(master)。再控制其他从节点,执行slaveof,让这些从节点,以新的mater作为主节点。 + +##### 5,总结 + +哨兵节点不能只有一个,因为哨兵节点挂了也会影响 系统的运作。 + +哨兵节点最好是奇数个,方便选举leader,得票数更容易超过一半。 + +哨兵+主从复制解决的问题是"提高可用性",极端情况下写操作的数据丢失无法解决。 + +哨兵+主从复制不能提高数据的存储容量 ,当数据接近或者几乎超过机器的物理内存时,这样的结构就难以胜任了,**而接下来的redis集群,就是解决存储容量问题的有效方案。** + +### 三,集群 + +**广义上的集群,是指多个机器,构成的分布式系统,就可以称为一个集群,所以前面的主从复制和哨兵模式也可以看作是一个集群。** + +**而侠义上的集群,是redis提供的集群模式。这个集群模式之下,主要是解决存储空间不足的问题。** + +在redis哨兵模式中,本质还是redis数据节点存储数据,其中就要求主节点/从节点来存储数据的全集。为了提高 数据存储的容量,这时就引入多台机器,每台机器只存储一部分数据。 + +> 假设有1TB的数据需要存储: 拿两台机器来存储 ,每台机器需要存储512GB。 拿三台机器来存储,每台机器需要存储300多GB。 拿四台机器来存储,每台机器需要存储 256GB...... + +但是还存在一个问题,如果使用三台机器来存储1TB的数据,如果某个机器挂了怎么办,所以我们还需要为每个机器 再分配几个从节点。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/bc649c778f118c0182738fe6ce9dd965.png) + +这三组机器的数据都是不同的,每个 slave都是mster的备份,当master挂了,slave就会晋升为master。 + +如上图所示,每个虚线框就可以看作是一个分片(Sharding)。 + +_**重点面试题:**_ + +三种主流的分片算法:哈希求余,一致性哈希算法,哈希槽分区算法。 + +##### 1,哈希求余 + +借助hash函数,把一个key映射成为 一个数字,在对数组长度(这里就是分片的个数)进行求余,就可以得到这个key是在哪个分片中。 + +比如现在有3个分片,编号为0,1,2 + +此时就可以针对要查询的数据(或插入的数据)计算hash值(比如可以使用md5算法),再把这个值余上分片的个数,此时就会得到一个数字,这个数字就表示这个数据在哪个分片中。 + +但是当总体的数据增长时,就需要扩容,引入更多的分片,此时分片的个数就变了。 + +如果发现某个数据在扩容之后,不该待在当前的分片中,那么就需要重新分配数据(数据搬运)。这里涉及到的数据搬运不仅仅是主节点进行,从节点也需要进行。 + +这种方式开销极大,往往不能再生产环境上操作的,搬运成本比较大。 + +**数据搬运成本大的原因:这种哈希求余的方式,导致数据是交替出现的,比如100出现在0号分片,101出现在1号分片,102出现在2号分片,103又是0号分片 。这就导致在扩容之后,分片个数增长,就会有 大量的数据需要进行搬运。** + +##### 2,一致性哈希算法 + +这种方式可以降低上述的"搬运开销"。 + +key映射到分片序号的过程不再是简单的求余了,而是改成以下过程: + +第一步:把0~2^32-1这个数据空间,映射到一个圆环上,按照顺时针方向增长。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/642b9996e5a797d824c04e5fb62cae97.png) + +第二步:假设分成3个分片,将分片放到对应的位置上 + +每个分片就会对应一个值,比如0号分片对应的值就是0 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/ef99ed9f6536cea7bc4d0c10e0cc483d.png) + +第三步:假定有一个key,计算得到的hash值为H,如何计算这个key是在哪个分区?此时H会在圆环上的某个位置,从这个位置开始,顺时针向下找,找到的第一个分片,就是这个key所属 的分片。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/f6f7d13dc8b18e8555d3d3b8e7c94a7b.png) + +这就相当于,N个分片,把整个圆环分成N个区域,key的hash值落在哪个区域,它就属于哪个分片。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/659bf9d0c355369184acba47b8673539.png) + +因此在 一致性哈希这样的设定下,把数据交替出现,改进成了连续出现。 + +在这种情况下,如何进行扩容,假设新增一个分片。 + +如下图所示,在圆环上找一个位置,设为3号分片的位置。该部分本来是0号分片上的,这样一来,只需将0号分片上的这段数据搬运到3号分片上即可,其他分片上的数据不需要搬运。 + +这种搬运的成本是有的,但是比之前哈希求余的方式低了不少。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/4aaf8c07c37d689dc7fc0aa33ee929f9.png) + +这种方式,虽然搬运的成本降低了,但是也导致了各个分片上的数据量不均匀,称作数据倾斜。 + +##### 3,哈希槽分区算法 + +这是Redis真正采用的分片算法。 + +哈希槽计算公式:hsah_slot=hash(key)%16384,一共有16384个槽。 + +假设现在有3个分片,一种分配方式如下: + +- 0号分片:[0,5461],共5462个槽位。 +- 1号分片:[5462,10923],共5462个槽位。 +- 2号分片:[5463,16384],共5460个槽位。 + +这里只是分片的一种,分片可以很灵活。每个分片持有的槽位号:可以是连续的,也可以是不连续的。 + +此处 ,每个分片都会使用一个位图结构,来表示该分片有多少槽位号,16384个bit位,用每一位的0/1表示是否持有这个槽位。 + +现在假设要新增一个分片,那么此时可以从0号分片,1号分片,2号分片上分别截取一部分出来,放到新的分片上,这样就可以解决数据倾斜的问题。 + +##### 4,故障处理 + +如果某个主节点挂了,此时就会把该主节点旗下的某一个从节点提拔为主节点,保证我们的redis能够正常工作。 + +###### 故障判定 + +识别某个节点是否挂了。 + +1. 节点A给节点B发送ping包,B就会给A返回一个pong包。ping和pong除了携带message type属性之外,其他部分都是一样的。还会包含集群的配置信息(该节点的id,该节点属于哪个分片,该节点是主节点还是从节点,从属于哪个主节点,持有哪些slot槽位)。 +2. 每个节点,每秒钟都会给一些 随机的节点发送ping包,而不是给所有节点都发送。这样设定设为了在节点很多的时候,心跳包也会非常多。 +3. 当节点A向节点B发送ping包后,B不能如期回应的时候,此时A就会尝试重置和B的TCP连接,看能否连接成功,如果连接失败,就会认为B节点下线了,A就会把B设为PFAIL状态(相当于主观下线)。 +4. A判定B为PFAIL后,会通过redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态。(每个 节点都会维护一个自己的下线列表,由于视角不同,每个节点的下线列表也就不同)。 +5. 此时A发现很多节点也认为B为PFAIL,并且数目超过集群节点个数的一半,那么A就会把B标记为PFAIL(相当于客观下线),并且把这个消息同步给其他节点(其他节点收到后,也会把B标记为PFAI)。 + +以下三种情况会出现集群宕机: + +- 某个分片,所有的主节点和从节点都挂了。 +- 某个分片,主节点挂了,没有从节点。 +- 整个集群超过一半的主节点挂了。 + +###### 故障迁移 + +还是上述的例子。 + +如果B是从节点挂了,那么就不需要进行故障迁移,毕竟从节点挂了,还可以通过访问同一个 分片内的主节点或者其他从节点来获取数据。 + +如果B是主节点,就会由B的从节点(比如C和D)发生故障迁移。重新挑选一个主节点,代替之前主节点的位置。 + +具体过程如下: + +1. 从节点需要判断自己是否具有参选资格,如果主节点和从节点太久没有进行通信(此时认为主节点和从节点的数据差异太大了),就失去竞选资格。 +2. 具有资格的节点,比如C和D,就会先休眠一段时间,休眠时间=500ms基础时间+【0,500ms】随机事件+排名*1000ms。offset值越大,排名就越靠前(越小)。offset表示主节点和从节点数据同步的进度。 +3. 比如C的休眠时间到了,C就会给集群中其他所有节点,进行拉票操作。但是只有主节点才有投票资格。 +4. 主节点就会把自己的票投给C节点(每个主节点只有一票),当C收到的票数超过主节点数目的一半,C就会晋升为主节点(C会自己执行slaveof no one,并且让D执行slaveof D)。 +5. 同时,C还会把自己称为主节点的消息,同步给集群中的其他节点,大家也都会更新自己保存的集群信息结构。 + +总之,哪个节点会成为主节点,就看哪个节点先被唤醒,哪个节点的休眠时间短,大概率就是新的主节点。 + +如果两个节点被唤醒的时间是差不多的,那么此时就各凭本事了,取决于网络延迟,线程调度等等因素。 + +**上述选举的过程,称为Raft算法。** + +### 四,Redis典型应用——缓存 + +Redis最主要的三个用途: + +1. 存储数据(内存数据库) +2. 缓存 +3. 消息队列 + +##### 1,使用redis作为数据库的缓存 + +在一个网站中,通常会使用[关系型数据库](https://cloud.tencent.com/product/tencentdb-catalog?from_column=20065&from=20065)(如MySQL)来存储数据,关系型数据库虽然强大,但是有一个很大的缺陷,就是性能不高。(换言之 ,进行一次 查询操作消耗的系统资源较多)。 + +> 为什么说关系型数据库的性能不高? + +1. 数据库是把数据存储在硬盘上的,硬盘的IO速度并不快,尤其是随机访问。 +2. 如果查询不能命中索引,就需要进行表的遍历,这就会大大增加 IO的次数。 +3. 关系型数据库对SQL的操作会进行一系列的解析,校验,优化工作。 +4. 如果是一些复杂查询,比如联合查询,需要进行笛卡尔积的操作,效率更是降低很多。 + +因为MySQL等数据库 ,效率比较低,所以承担的并发量就有限了,一旦请求量多了,数据库的压力就很大,甚至很容易就宕机了。对于服务器的每一个请求,都要消耗一定的硬件资源(CPU,内存,硬盘,网络带宽等等),任意一种资源的消耗超出了机器能提供的上限,机器就很容易出故障。 + +如何提高MySQL能承担的并发量? + +1. 开源:引入更多的机器,构成数据集群。 +2. 节流:引入缓存,将一些热点数据保存到缓存中。后续在查询数据的时候,如果数据库中已经存在了,就不再访问MySQL了。 + +##### 2,如何知道redis中应该存储哪些数据? + +也就是怎么获得热点数据。 + +这涉及到缓存的两种更新策略:1,定期生成 2,实时生成 + +1,定期生成 + +将访问的数据 ,以日志的 形式记录下来。接下来就可以针对这些日志进行统计了,统计这一天/一周/一个月,数据出现的频率,然后再按照降序排序,取出前20%的数据数据,这些数据 就是热点数据。 + +优点:上述过程,实际上实现起来比较简单,过程更可控,缓存中的数据是比较扶额和预期的,方便排查问题。 + +缺点:实时性不够。如果出现一些突发事件,有些本来不是热词的内容成了热词,这就可能会给后面的数据库带来较大的压力。 + +2,实时生成 + +- 如果在redis中查到了数据,就直接返回。 +- 如果没有查到,就从数据库查,同时把查到的数据写入redis中。 + +这里就会有一个问题,如果不停的向redis中写入数据,就会使redis的内存占用越来越高,逐渐达到内存上限。 + +> 此时如果继续向redis中写入数据,就会出现问题,为了解决这个问题,redis就引入了"内存淘汰策略"。 _**经典面试题:**_ + +- FIFO(First In First Out)先进先出:把缓存中存在时间最久的(也就是先来的数据)淘汰掉。 +- LRU(Least Recently Used)淘汰最近未使用的:记录每个key的最近访问时间,把最近访问时间最老的key淘汰掉 +- LFU(Least Frequently Used)淘汰访问次数最少的:记录每个key最近一段时间的访问次数,把访问次数最少的淘汰掉。 +- Random 随机淘汰:从所有的key中随机抽取一个淘汰掉。 + +##### 3,缓存预热,缓存穿透,缓存雪崩和缓存击穿 + +**缓存预热(Cache preheating)** + +缓存中的数据,有两种更新策略:**1,定期生成 2,实时生成** + +1. 如果使定期生成,就不涉及到预热。 +2. 如果是实时生成,在redis服务首次接入之后,服务器里是没有数据的,此时客户端的所有请求就都会打给MySQL,如果请求量太多,可能就会导致MySQL服务挂了。随着时间的推移,reids中的数据越来越多,MySQl承担的压力也就越来越小了。 + +缓存预热,就是为了解决上述问题。把定期生成和实时生成相结合,先通过离线的方式,通过一些统计的途径,先找到一批热点数据,导入到redis中。此时导入的这批热点数据就能帮MySQL分担一些压力了。随着时间的推移,使用新的热点数据来淘汰旧的热点数据。 + +在刚开始架构演进的时候,没有缓存,此时要加入缓存,就要进行缓存预热。还有当服务器进行重启的时候,我们要保证重启之后缓存中是否有数据以及 这里的数据 是否是热点数据,这也涉及到缓存预热。 + +--- + +**缓存穿透(Cache penetration)** + +在一次查询的过程中,如果要查询的某个key,在redis中没有,在MySQL中也没有。也就意味着此时这个key是不会被放到redis中,那么下次访问依然会访问数据库,这就会导致数据库承担的请求太多,压力很大。这种情况称为缓存穿透。 + +出现这种情况可能的原因: + +- 业务设计不合理:比如缺少必要的参数检验环节,导致非法到的key也被进行查询了。 +- 开发/运维误操作:不小心把部分数据从数据库中删除。 +- 黑客恶意攻击。 + +解决方案: + +1. 如果发现这个key,在redis和MySQL中都不存在在,仍然写入redis,将value设成一个非法值(比如"")。再应用层程序可以检查出这是一个非法的key。 +2. 还可以引入布隆过滤器,每次查询redis/MySQL之前,都先判断一下key是否在布隆过滤器上存在。布隆过滤器本质是结合了hash+bitmap,以较小的空间开销,以较快的访问速度,实现针对key是否存在的判定。 + +--- + +**缓存雪崩(Cache avalanche)** + +由于在短时间内,redis上大规模的key失效,导致缓存命中率陡然下降,并且MySQL压力迅速上升,甚至导致MySQL直接宕机。 + +可能的原因: + +- redis直接挂了,redis宕机/redis集群模式下很多节点宕机。(这是最主要的) +- redis正常工作,但是可能之前短时间内设置了很多key给redis,并且设置的过期时间是相同的。在给redis里设置key作为缓存的时候,有的时候为了考虑时效性,就会设置过期时间(和redis的内存淘汰机制是配合使用的)。 + +解决方法: + +- 加强监控报警,加强redis集群可用性的保证。 +- 不给key设置过期时间,或者在设置过期时间的时候,添加随机因子(避免同一时刻过期 )。 + +**缓存击穿(Cache breakdown)** + +相当于缓存雪崩的特殊情况,针对**热点key**,突然过期了,导致大量的请求访问到数据库上,导致数据库宕机了。 + +解决方案: + +1. 基于统计的方式发现热点key,并设置为永不过期。这种方案往往需要服务器做出较大的调整。比如把当前访问哪些key的日志记录下来,接到一个消息队列中,再通过一些计算,将结果再返回给我们的服务器。 +2. 进行必要的降级服务。例如访问数据库的时候,使用分布式锁,限制同时请求数据库的并发数。 + +### 五,分布式锁 + +在一个分布式系统中,会涉及到多个节点访问同一个公共资源的问题,此时就需要通过 锁 来做互斥控制,避免出现类似于 线程安全的问题。而C++中的std::mutex,这样的锁只能在当前进程中生效。 + +而在分布式系统中,是有很多进程的(每个服务器,都是独立的进程)。因此,之前的锁就难以对现在分布式系统中的多个进程之前产生制约。分布式系统中,多个进程之间的执行顺序也是不确定的。 + +此时就需要引入"分布式锁",来解决上述 问题。 + +**所谓的分布式锁,也是一个/一组单独的服务器程序,给其他的服务器提供"加锁"这样的服务。redis是一种典型的可以是实现分布式锁的方案,但不是唯一的一种。** + +![](https://developer.qcloudimg.com/http-save/yehe-100000/a8e89bbb5a581a4cdb426262e46ed632.png) + +买票服务器在进行买票的过程中,就需要先加锁,就是往redis上尝试设置一个特殊的key-value,完成买票后,就会把这个key-value删掉。其他服务器在买票的过程中,也会去尝试设置这个key-value,如果发现key-value已经存在,就认为加锁失败(是放弃还是阻塞,就看具体的实现策略了)。 + +**这个加锁过程其实就对标redis中的一个命令setnx key val,这个命令如果key不存在才会设置,如果key存在就会执行出错,同时解锁过程也对标redis中的del key命令。** + +##### 1,引入过期时间 + +问题1:某个服务器加锁成功了(setnx成功),如果该服务器执行后续逻辑的过程中,程序崩溃了,此时还没有执行到解锁操作。这种情况就会导致redis上的key无人删除,也就导致其他服务器无法获取到锁了。 + +解决办法:在加锁过程中,给这个key设置一个过期时间,set ex nx这样的命令来完成设置,时间到了,redis服务器会自动删除这个key,这是其他服务器就可以获取到锁了。 + +注意:在设置过期时间的时候,智能使用set nx ex这样的方式设置,不能使用set nx ,exprie这两个命令来设置。因为redis上多个命令之间,是无法保证原子性的,此时就可能出现,这两个命令,一个执行成功,一个执行失败。相比之下,使用一条命令设置,是更加稳妥的。 + +##### 2,引入校验id + +问题2:所谓的加锁,就是给redis上设置一个key-val,所谓的解锁,就是给redis上的key-val删除掉。锁,就可以认为是redis上的一个普通键值对。可能会出现服务器1执行了加锁,而服务器2误执行了解锁。因此就可能给我们的系统带来严重的问题。(比如票数超卖) + +为了解决这个问题,就引入了校验机制。 + +1. 给服务器编号,每个服务器都有自己的身份标识。 +2. 进行加锁的时候,设置key-val。**key对应着服务器要访问的资源,val表示服务器的编号。** +3. 在解锁的时候,先查询这个锁对应的服务器编号,然后判定这个编号和执行解锁的服务器的编号是否一致,如果是,才能真正执行del;否则,执行失败。 + +##### 3,引入lua脚本 + +对于问题2,我们引入了校验id,但是还存在问题。就是在解锁的时候,需要两步操作,先获取到key对应的val,在执行del,此处是两步操作(不是原子的),就可能会出现问题。 + +一个服务器内部,也可能是多线程的,此时,就可能服务器A的两个线程都在执行解锁操作,首先进行id校验,都通过了,然后开始执行del命令,del就会被重复执行。 + +这看起来没有什么问题,但是如果此时一个线程执行完了del,又有一个服务器B来进行加锁(set nx ex),加锁成功,之后服务器A的另一个线程执行del,就会把服务器B的锁给解掉。 + +归根节点,是因为get 和 del这两个命令不是原子的,此时可以引入事务,将这两个操作打包成一个事务,使在执行get 和 del之间不会执行其他操作(避免插队)。 + +**使用事务,能解决上述问题,但是在实践中,往往推荐使用更好的方案——lua脚本。** + +**redis执行lua脚本的过程 ,也是原子的,相当于执行一条命令一样。** + +**在redis官方文档中,也明确说明了,lua就属于事务的替代方案。** + +##### 4,引入看门狗(Watch Dog) + +在前面提到过,服务器在进行加锁的时候,要给key设置一个过期时间。 + +- 这个过期时间,如果设置的太短,就可能在服务器的业务逻辑还未执行完,锁就释放了。 +- 如果设置的太长,也会导致"锁释放不及时"的问题。 + +**这里更好的方式是"动态续约"。** + +**初始情况下,设置一个过期时间(比如设置1s),就提前在还剩300ms的时候(不一定是300ms,数值可以灵活调整),如果当前任务还未执行完,就把过期时间再续上1s。等到时间又快到了,任务还未执行完,就再续。** + +这样设置也有一个好处:如果服务器中途崩溃了,也就没人续约了,此时,锁就可以再较短的时间内被释放。 + +服务器进行"动态续约"往往是需要有一个专门的线程来完成这个事情,这个线程就叫做**"看门狗"。** + +##### 5,redlock算法 + +使用redis作为分布式锁,redis本身是有可能挂了的。 + +要想保证redis的高可用,可以使用主从复制,哨兵,集群模式等方案。这里使用哨兵机制最合适。 + +进行加锁操作,就是把key设置到设置到主节点上,如果主节点挂了,有哨兵节点会把从节点升级为主节点,进一步保证刚才的锁可用。 + +**但是主节点和从节点的数据同步是有延迟的,可能主节点收到了加锁的请求(set nx ex),还没来得及推送给从节点,主节点就挂了。即使从节点升级成为了主节点,但是刚才加锁的对应的数据是不存在的。** + +**此时就需要使用 redlock算法。(redis作者给出的一种方案)核心思想:冗余,少数服从多数。** + +此时加锁,就是按照一定的顺序,针对这些redis都进行加锁操作。如果某个主节点挂了(加不上锁),没关系,继续给下一个主节点加锁。如果加锁成功的主节点个数超过总结点总数的一半,就视为加锁成功。同理,进行解锁的时候,每个主节点都会进行一遍解锁。 + +![](https://developer.qcloudimg.com/http-save/yehe-100000/22a60af03db7b1c2cc410117f99171b4.png) \ No newline at end of file diff --git a/hhs/finalhomework/hhs执行计划.md b/hhs/finalhomework/hhs执行计划.md deleted file mode 100644 index 18dc464..0000000 --- a/hhs/finalhomework/hhs执行计划.md +++ /dev/null @@ -1,592 +0,0 @@ ---- -tags: [全栈开发, 大作业, 执行计划, gRPC, Gin, OSS, Eino, React] -create time: 2026-05-09 14:30 ---- - -# 大作业·执行计划 — 双端 gRPC HR 招聘系统 - -## 概述 - -本文档基于《大作业项目要求》《思维导图》《工程结构设计》三份参考资料,梳理出分阶段、可落地的开发执行计划。整体采用 **六阶段推进** 策略:环境准备 → Proto 先行 → 后端双服务 → **前端一日冲刺** → 联调测试 → 视频录制与提交,每个阶段明确任务清单、前置依赖和验收标准,确保按期完成高质量交付。 - -> [!tip] 如何使用本计划 -> 建议按阶段的顺序推进,不要跳跃。Proto 定义是前后端的"契约",必须先于业务代码完成。每个阶段完成后对照验收清单自检,避免返工。 - -## 一、整体时间规划与打勾进度 - -将整个开发周期拆分为 6 个阶段,各阶段之间存在明确的先后依赖关系。下方是总进度看板,每完成一个阶段就打勾: - -```mermaid -gantt - title 大作业开发总时间线 - dateFormat YYYY-MM-DD - axisFormat %m-%d - todayMarker stroke-width:3px,stroke:#fb8c00,opacity:0.6 - - section 阶段一 · 环境就绪 (M0) - Go / Node / MySQL / OSS :done, p1, 2026-05-10, 1d - .gitignore + .env.example :done, p1b, after p1, 30m - - section 阶段二 · Proto 先行 (M1) - 7 个接口定义文件 :done, p2, 2026-05-11, 1d - protoc 生成 Go 代码 :done, p2b, after p2, 30m - - section 阶段三 · 后端双服务 (M2) - 3A · MySQL 建表 : p3a, 2026-05-12, 1d - 3B · Logic 核心服务 (12项) :milestone, m3b, 2026-05-14, 0d - 3C · Web 网关服务 : p3c, after p3a, 2d - - section 阶段四 · 前端一日冲刺 (M3) - Foundation 共享基建 : p4a, 2026-05-15, 3h - 候选人用户端 (6页) : p4u, after p4a, 4h - HR 管理端 (7页) : p4h, after p4a, 5h - Build 验证 + Commit :milestone,m4z, 2026-05-15, 0d - - section 阶段五 · 联调自测 (M4) - 全链路跑通 : p5a, 2026-05-16, 1d - 专项测试 + Bug 修复 : p5b, 2026-05-17, 1d - - section 阶段六 · 录制提交 (M5) - 文档完善 : p6a, 2026-05-18, 2d - 视频录制 + KDoc 提交 :milestone,m6end, 2026-05-18, 0d - - %% 里程碑锚点 - milestone M0_环境就绪 :milestone, mk1, 2026-05-10, 0d - milestone M1_Protobuf冻结 :milestone, mk2, 2026-05-11, 0d - milestone M2_后端可运行 :milestone, mk3, 2026-05-14, 0d - milestone M3_双端完成 :milestone, mk4, 2026-05-15, 0d - milestone M4_联调通过 :milestone, mk5, 2026-05-17, 0d - milestone M5_正式提交 :milestone, mk6, 2026-05-18, 0d -``` - - - -| 阶段 | 日期 | 核心交付物 | 里程碑 | -| --------------- | ------------- | ------------------------------ | -------- | -| ~~阶段一:环境与基础建设~~ | ~~05-10~~ | Go / Node / MySQL / OSS 就绪 | ✅ **M0** | -| 阶段二:Proto 接口定义 | ~~05-11~~ | 7 个 `.proto` 文件 + 生成代码 | ⬜ **M1** | -| 阶段三:后端双服务 | 05-12 ~ 05-14 | Logic gRPC + Web Gin 均可独立运行 | ⬜ **M2** | -| 阶段四:前端一日冲刺 | 05-15 | hr-frontend + user-frontend 启动 | ⬜ **M3** | -| 阶段五:前后端联调 | 05-16 ~ 05-17 | 全链路测试通过 | ⬜ **M4** | -| 阶段六:视频录制与提交 | 05-18 | 视频 + 文档全部提交 | ⬜ **M5** | - -> [!tip] 使用说明 -> - 🟢 **绿色条** = 已完成阶段 · 🟡 **黄色条** = 当前活跃期 · 🔴 **菱形标记** = 里程碑检查点 -> - 橙色竖线为今日日期标注,直观对比当前所处阶段 -> - 甘特图下方表格同步映射 M0~M5 里程碑编号,与第九节里程碑总表一一对应 - -> [!question] 如果某个阶段超时了怎么办? -> 优先级原则:核心功能(gRPC 分层调用、OSS 签名上传、Eino 对话)> 次要功能(分页搜索、数据可视化)。宁可缩减 UI 装饰细节,也要保住验收红线。 - -## 二、阶段一:环境与基础建设 - -> [!note] 进度追踪 -> - [x] T1.1 Go 1.21+ 安装并验证 -> - [x] T1.2 Node.js 18+ / npm 安装并验证 -> - [x] T1.3 MySQL 8.0+ 安装 + 数据库实例创建 -> - [x] T1.4 OSS 平台注册 + 私有 Bucket 创建 -> - [x] T1.5 Git 仓库初始化 + 目录结构建立 -> - [ ] T1.6 `.gitignore` + `.env.example` 初始化 - -
-📋 展开查看详细任务表 - -### 2.1 详细任务清单 - -| 编号 | 任务 | 耗时估计 | 说明 | -|------|------|----------|------| -| T1.1 | 安装并验证 Go 1.21+ | 30min | `go version`,配置 GOPATH 和 GOMODCACHE | -| T1.2 | 安装并验证 Node.js 18+ / npm | 30min | 后续用于两个前端项目 | -| T1.3 | 安装并验证 MySQL 8.0+ | 30min | 创建数据库实例,记录连接信息 | -| T1.4 | 注册 OSS 平台,创建私有 Bucket | 30min | 关闭匿名访问,关闭公开读权限 | -| T1.5 | 创建 Git 仓库,建立最终目录结构 | 30min | 参照 `final_homework/` 规范 | -| T1.6 | 初始化 .gitignore 和 .env.example | 30min | 敏感文件排除:.env、go.sum(可选)、node_modules | - -
- -### 2.2 前置条件 - -- 无,这是第一个阶段。 - -### 2.3 验收标准 - -- [ ] `go version`、`node -v`、`mysql --version` 均正常输出 -- [ ] OSS Bucket 已创建且为私有(匿名访问已关闭) -- [ ] 本地目录结构已按 `工程结构.md` 的顶层树创建出来 -- [ ] `.gitignore` 正确排除了敏感配置文件 - -```bash -# 预期目录骨架 -final_homework/ -├── hr-frontend/ -├── user-frontend/ -├── web-gin-service/ -├── logic-grpc-service/ -└── api/ -``` - -> [!warning] 常见陷阱 -> - OSS 密钥务必写入 .env 或独立配置文件,**不要硬编码到业务代码中**。真实业务场景中 key 是绝对禁止提交到仓库的。 -> - 新建 Git 仓库后立刻添加 .gitignore,避免误提交 .env 文件。 - -## 三、阶段二:Proto 接口定义(契约先行) - -> [!note] 进度追踪 -> - [ ] T2.1 `common.proto` — 通用响应码、分页参数 -> - [ ] T2.2 `auth.proto` — Login/RPC、Register/RPC、VerifyToken/RPC -> - [ ] T2.3 `job.proto` — CreateJob/RPC、UpdateJob/RPC、ListJobs/RPC、DeleteJob/RPC -> - [ ] T2.4 `application.proto` — SubmitApplication/RPC、ListApplications/RPC -> - [ ] T2.5 `profile.proto` — GetProfile/RPC、UpdateProfile/RPC -> - [ ] T2.6 `resume.proto` — GenOssSign/RPC、SaveResume/RPC -> - [ ] T2.7 `chat.proto` — SendChat/RPC、GetHistory/RPC -> - [ ] T2.8 protoc 生成 Go 代码 - -
-📋 展开查看详细任务表 + 前置条件 - -### 3.1 详细任务清单 - -| 编号 | 任务 | 前置依赖 | 耗时估计 | -|------|------|----------|----------| -| T2.1 | 定义 `common.proto`(通用响应码、分页参数) | — | 30min | -| T2.2 | 定义 `auth.proto`(Login/RPC、Register/RPC、VerifyToken/RPC) | T2.1 | 30min | -| T2.3 | 定义 `job.proto`(CreateJob/RPC、UpdateJob/RPC、ListJobs/RPC、DeleteJob/RPC) | T2.1 | 30min | -| T2.4 | 定义 `application.proto`(SubmitApplication/RPC、ListApplications/RPC) | T2.1 | 30min | -| T2.5 | 定义 `profile.proto`(GetProfile/RPC、UpdateProfile/RPC) | T2.1 | 30min | -| T2.6 | 定义 `resume.proto`(GenOssSign/RPC、SaveResume/RPC) | T2.1 | 30min | -| T2.7 | 定义 `chat.proto`(SendChat/RPC、GetHistory/RPC) | T2.1 | 30min | -| T2.8 | protoc 生成 Go 代码 | T2.1 ~ T2.7 | 30min | - -
- -### 3.2 前置条件 - -- 阶段一已完成(T1.1 ~ T1.6) -- protoc 编译器及 go plugin 插件已安装 - -### 3.3 验收标准 - -- [ ] `protoc --go_out=... --go-grpc_out=...` 在 `api/proto/v1/` 下编译通过 -- [ ] 生成的 Go 代码存在于两个服务的 `proto/gen/v1/` 目录 -- [ ] 所有 RPC 方法签名覆盖项目需求文档中的全部接口 - -> [!tip] Proto 设计最佳实践 -> - 每个 Service 对应一个 proto 文件,职责单一 -> - Request 和 Response 消息统一命名规范:`XxxRequest`、`XxxResponse` -> - `common.proto` 中的 `CommonResponse{code, msg, data}` 作为所有响应的包装壳 -> - 字段使用 tag 编号,从 1 开始递增,预留扩展空间 - -## 四、阶段三:后端双服务开发 - -### 3.1 子阶段 3A:MySQL 数据库建表 - -> [!note] 进度追踪 -> - [ ] T3A.1 根据 ER 图设计 SQL DDL -> - [ ] T3A.2 执行建表 + 插入测试种子数据 - -建表清单(对照 `db.md` 细化): - -| 表名 | 用途 | 关键字段 | -|------|------|----------| -| `users` | 用户表 | id, username, password_hash, role, created_at | -| `jobs` | 岗位表 | id, creator_id, title, description, status, created_at | -| `applications` | 投递记录表 | id, job_id, candidate_id, applied_at | -| `profiles` | 候选人档案表 | id, user_id, name, phone, education, school, experience, skills | -| `resumes` | 简历信息表 | id, profile_id, file_key, oss_url, created_at | -| `chat_records` | AI对话历史表 | id, hr_id, question, ai_reply, created_at | - -> [!note] 思考题 -> 为什么 `creator_id` 放在 jobs 表中而不是单独的 job_ownership 关联表?因为本系统架构轻量化,HR 仅管理本人发布的岗位,单字段外键足以表达这种一对一的归属关系。 - -### 3.2 子阶段 3B:Logic 核心业务服务 - -> [!note] 进度追踪 -> - [ ] T3B.1 gRPC Server 框架 + config 模块 -> - [ ] T3B.2 AuthService(注册/登录/JWT签发) -> - [ ] T3B.3 JobService(岗位 CRUD + 创建者权限校验) -> - [ ] T3B.4 ApplicationService(投递逻辑 + 校验拦截) -> - [ ] T3B.5 ProfileService(候选人档案增改查) -> - [ ] T3B.6 ResumeService + OSS signer(签名 URL 生成 + 文件头校验) -> - [ ] T3B.7 ChatService(意图解析 → SQL查询 → Prompt拼接 → Eino推送 → 持久化) -> - [ ] T3B.8 repository 层数据访问封装(6张表) -> - [ ] T3B.9 converter 层 proto ↔ model 转换 -> - [ ] T3B.10 JWT 工具模块(签发 + 验证 + 角色提取) -> - [ ] T3B.11 AI 模块封装(Eino Chat Engine + prompt 模板 + 意图解析器) -> - [ ] T3B.12 Logic 服务独立启动测试 - -
-📋 展开查看 Logic 详细任务表 - -| 编号 | 任务 | 前置依赖 | 耗时估计 | -|------|------|----------|----------| -| T3B.1 | 搭建 gRPC Server 框架 + config 模块 | T3A.2 | 1h | -| T3B.2 | 实现 AuthService(注册/登录/JWT签发) | T3B.1 | 2h | -| T3B.3 | 实现 JobService(岗位 CRUD + 创建者权限校验) | T3B.1 | 2h | -| T3B.4 | 实现 ApplicationService(投递逻辑 + 校验拦截) | T3B.1, T3B.3 | 2h | -| T3B.5 | 实现 ProfileService(候选人档案增改查) | T3B.1 | 1.5h | -| T3B.6 | 实现 ResumeService + OSS signer(签名 URL 生成 + 文件头校验) | T3B.1 | 2h | -| T3B.7 | 实现 ChatService(意图解析 → SQL查询 → Prompt拼接 → Eino推送 → 持久化) | T3B.1, T3A.2 | 2.5h | -| T3B.8 | repository 层数据访问封装(6张表) | T3B.1 | 2h | -| T3B.9 | converter 层 proto ↔ model 转换 | T2.8, T3B.8 | 1h | -| T3B.10 | JWT 工具模块(签发 + 验证 + 角色提取) | T3B.1 | 1h | -| T3B.11 | AI 模块封装(Eino Chat Engine + prompt 模板 + 意图解析器) | T3B.1 | 2h | -| T3B.12 | Logic 服务独立启动测试 | T3B.2 ~ T3B.11 | 1h | - -
- -#### Logic 核心流程自查 - -- [ ] 用户注册时密码 bcrypt 加密存储 -- [ ] 登录成功签发 JWT(包含 userId + role) -- [ ] 岗位编辑/下架时校验当前操作用户 == creator_id -- [ ] 候选人投递时拦截:未完善资料或无简历 → 返回错误码 -- [ ] OSS 签名 URL 严格后缀白名单 (.pdf/.doc/.docx) + 文件头 Magic Number 校验 -- [ ] 简历不经过服务端缓存,客户端直传 OSS -- [ ] AI 对话每条问答自动写入 chat_records 表 - -> [!danger] 验收红线 -> Logic 服务内所有业务逻辑只能通过 gRPC Server 暴露,Web 服务不得通过同工程内部函数直连调用。如果同一个 Go module 里直接 import service 包 = 架构违规。 - -### 3.3 子阶段 3C:Web 网关服务 - -> [!note] 进度追踪 -> - [ ] T3C.1 Gin 项目框架 + router 注册 -> - [ ] T3C.2 CORS 中间件(全局跨域处理) -> - [ ] T3C.3 JWT 鉴权中间件(读取 Token + 角色校验) -> - [ ] T3C.4 参数合法性校验中间件 -> - [ ] T3C.5 gRPC Client 连接池 + codec JSON 转换 -> - [ ] T3C.6 Auth Handler -> - [ ] T3C.7 Job Handler -> - [ ] T3C.8 Application Handler -> - [ ] T3C.9 Profile Handler -> - [ ] T3C.10 Resume Handler -> - [ ] T3C.11 Chat Handler -> - [ ] T3C.12 Web 服务独立启动测试 - -
-📋 展开查看 Web 网关详细任务表 - -| 编号 | 任务 | 前置依赖 | 耗时估计 | -|------|------|----------|----------| -| T3C.1 | 搭建 Gin 项目框架 + router 注册 | T3B.12 | 1h | -| T3C.2 | CORS 中间件(全局跨域处理) | T3C.1 | 30min | -| T3C.3 | JWT 鉴权中间件(读取 Token + 角色校验) | T3B.10 | 1h | -| T3C.4 | 参数合法性校验中间件 | T3C.1 | 1h | -| T3C.5 | gRPC Client 连接池 + codec JSON 转换 | T3B.12 | 1.5h | -| T3C.6 | Auth Handler(POST /api/auth/login, register) | T3C.5, T3B.2 | 1h | -| T3C.7 | Job Handler(CRUD 路由映射到 gRPC) | T3C.5, T3B.3 | 1.5h | -| T3C.8 | Application Handler | T3C.5, T3B.4 | 1h | -| T3C.9 | Profile Handler | T3C.5, T3B.5 | 1h | -| T3C.10 | Resume Handler(获取签名 URL 返回给前端) | T3C.5, T3B.6 | 1.5h | -| T3C.11 | Chat Handler(AI 对话转发) | T3C.5, T3B.7 | 1.5h | -| T3C.12 | Web 服务独立启动,用 curl/postman 测试 | T3C.6 ~ T3C.11 | 1h | - -
- -#### Web 端架构自查 - -- [ ] Handler 中没有任何核心业务逻辑——只有参数解析 + gRPC 调用转发 -- [ ] JWT 中间件在路由注册前挂载,未携带 Token 的请求一律返回 401 -- [ ] 所有 gRPC 调用都有 context deadline(防止阻塞) -- [ ] 错误码统一通过 CommonResponse.code 传递 - -## 五、阶段四:前端一日冲刺 - -> [!tip] 技术选型确认 -> 前端统一使用 **React + Vite + TypeScript + Axios**,UI 组件库推荐 **Ant Design**(开箱即用、减少造轮子时间)。前端只负责页面渲染和接口请求,所有业务逻辑在后端处理。 - -### 4.1 核心策略:共享基础层 + 下午独立填页 - -两个前端共用了同一套后端 API 契约(`api/proto/v1/`),因此**不需要各自从零搭架子**。关键压缩思路: - -```mermaid -graph LR - subgraph Foundation["Foundation(共享基础层)"] - A1["hr-frontend 脚手架初始化"] --> A2["user-frontend 脚手架初始化"] - A2 --> A3["双方同步搭建: Router/Axios/AuthContext/布局组件"] - A3 --> A4["Ant Design 主题配置"] - end - - subgraph Candidate["候选人用户端(独立完成)"] - B1["公开岗位列表"] --> B2["登录注册"] --> B3["档案表单"] --> B4["OSS直传"] --> B5["投递记录"] - end - - subgraph HR["HR 管理端(独立完成)"] - C1["登录注册"] --> C2["岗位CRUD"] --> C3["候选人列表"] --> C4["AI对话窗口"] - end - - A4 -.-> B1 - A4 -.-> C1 -``` - -| 模块 | 策略 | 复用收益 | -|------|------|----------| -| Foundation | 两个项目同时跑:Vite 初始化 + Router + Axios client + AuthContext + 布局组件 | 每个文件只写一次,两份代码各自 `cp -r` 起步,省去重复劳动 | -| 候选人 / HR | 各自独立填充页面——候选人端 6 页 / HR 端 7 页,互不干扰 | 后端 API 已完成,前端只对接已有接口 | - -### 4.2 Foundation 搭建 - -> [!note] 进度追踪 -> - [ ] T4.AM.1 hr-frontend 脚手架初始化 (`create-vite`) -> - [ ] T4.AM.1b user-frontend 脚手架初始化 (`create-vite`) -> - [ ] T4.AM.2 安装依赖 (antd, icons, react-router-dom) -> - [ ] T4.AM.3 路由配置 + Layout 组件(hr: Sidebar/Header, user: Navbar/Footer) -> - [ ] T4.AM.4 Axios client 封装(baseURL / interceptor / token 注入) -> - [ ] T4.AM.5 AuthContext(useContext 管理 JWT,isAuthenticated / role 状态) -> - [ ] T4.AM.6 Ant Design 全局主题 & 全局 CSS - -
-📋 展开查看详细任务表 - -| 编号 | 任务 | 耗时 | 说明 | -|------|------|------|------| -| T4.AM.1 | `npx create-vite hr-frontend --template react-ts` | 5min | 同步创建两个项目 | -| T4.AM.1b | `npx create-vite user-frontend --template react-ts` | 5min | — | -| T4.AM.2 | 安装依赖 (`antd`, `@ant-design/icons`, `react-router-dom`) | 10min | 两端同步操作 | -| T4.AM.3 | 路由配置 + Layout 组件(hr: Sidebar/Header, user: Navbar/Footer) | 45min | 各自按风格定制 | -| T4.AM.4 | Axios client 封装(baseURL / interceptor / token 注入) | 30min | 模板几乎相同,改 baseURL 即可 | -| T4.AM.5 | AuthContext(useContext 管理 JWT,isAuthenticated / role 状态) | 30min | 两端复制 + 角色名差异 | -| T4.AM.6 | Ant Design 全局主题 & 全局 CSS | 20min | hr 用深色主题, user 用浅色主题 | - -
- -#### AM 成果验证 - -- [ ] 两个 `npm run dev` 均能启动,显示空白布局框架 -- [ ] 路由切换正常,Axios 请求可发送到 `http://localhost:8080/api` - -> [!note] 思考题 -> 为什么 AuthContext 只需要改"角色名"就可以复用?因为前后端的鉴权模型是一致的——都靠 JWT 中的 `role` 字段区分 HR/候选人。这个设计体现了"接口契约驱动开发"的思想:proto 定义了统一的用户身份模型,前端只需消费它。 - -### 4.3 候选人用户端 - -> [!note] 进度追踪 -> - [ ] T4.U.1 HomePage — 公开岗位列表(免登录浏览) -> - [ ] T4.U.2 LoginPage + RegisterPage — 表单 + 提交到 `/api/auth/login` -> - [ ] T4.U.3 JobDetailPage — 岗位详情 + 条件渲染投递按钮 -> - [ ] T4.U.4 SetupProfilePage — 结构化档案表单 -> - [ ] T4.U.5 ResumeUploadPage — OSS 签名 URL 直传组件 -> - [ ] T4.U.6 ApplicationPage + WarningModal — 投递记录 + 拦截弹窗 - -
-📋 展开查看详细任务表 - -| 编号 | 任务 | 前置依赖 | 耗时 | 页面数 | -|------|------|----------|------|--------| -| T4.U.1 | HomePage:调用 `/api/jobs` 展示岗位卡片列表(免登录) | T4.AM | 40min | 1 | -| T4.U.2 | LoginPage + RegisterPage:表单 + 提交到 `/api/auth/login` | T4.AM | 40min | 2 | -| T4.U.3 | JobDetailPage:岗位详情 + 条件渲染投递按钮(已登录 && 有档案 && 有简历) | T4.U.1 | 40min | 1 | -| T4.U.4 | SetupProfilePage:Ant Design Form 表格单(姓名/电话/学历/院校/经历/技能) | T4.AM | 30min | 1 | -| T4.U.5 | ResumeUploadPage:选择文件 → 调 `/api/resume/upload` 拿签名 URL → 客户端 PUT 到 OSS | T4.AM, T3C.10 | 50min | 1 | -| T4.U.6 | ApplicationPage + WarningModal:投递记录 + 拦截弹窗 | T4.AM | 30min | 1+1 | - -
- -#### 候选人端关键交互逻辑 - -```mermaid -flowchart TD - A["游客打开 /user/"] --> B["查看岗位列表"] - B --> C["点击投递按钮"] - C --> D{"是否已登录?"} - D -->|"否"| E["跳转 /user/login"] - D -->|"是"| F{"是否完善档案?"} - F -->|"否"| G["提示跳转到 /user/profile/setup"] - F -->|"是"| H{"是否有合规简历?"} - H -->|"否"| I["提示 /user/resume/upload
PDF/DOC/DOCX 格式校验"] - H -->|"是"| J["发送 POST /api/applications"] - J --> K["投递成功 ✅"] -``` - -### 4.4 HR 管理端 - -> [!note] 进度追踪 -> - [ ] T4.H.1 LoginPage + RegisterPage(HR 账号体系) -> - [ ] T4.H.2 Dashboard/HomePage — 工作台概览 -> - [ ] T4.H.3 JobListPage — 本人岗位列表(分页 + 搜索) -> - [ ] T4.H.4 JobEditPage — 新建/编辑岗位表单 -> - [ ] T4.H.5 CandidatePage — 岗位下候选人列表 -> - [ ] T4.H.6 ProfileViewPage — 候选人结构化档案只读展示 -> - [ ] T4.H.7 ChatPage — AI 智能对话窗口 - -
-📋 展开查看详细任务表 - -| 编号 | 任务 | 前置依赖 | 耗时 | 页面数 | -|------|------|----------|------|--------| -| T4.H.1 | LoginPage + RegisterPage(HR 账号体系) | T4.AM | 30min | 2 | -| T4.H.2 | Dashboard/HomePage:工作台概览(数据卡片) | T4.AM | 30min | 1 | -| T4.H.3 | JobListPage:本人岗位列表(Ant Design Table + 分页 + 搜索) | T4.AM | 50min | 1 | -| T4.H.4 | JobEditPage:新建/编辑岗位表单 | T4.AM | 40min | 1 | -| T4.H.5 | CandidatePage:岗位下候选人列表 + 跳转档案详情页 | T4.AM | 50min | 1 | -| T4.H.6 | ProfileViewPage:候选人结构化档案只读展示 | T4.H.5 | 30min | 1 | -| T4.H.7 | ChatPage:AI 智能对话窗口(消息气泡 + 历史加载) | T4.AM, T3C.11 | 80min | 1 | - -
- -#### ChatPage 核心交互时序 - -```mermaid -sequenceDiagram - participant P as ChatPage - participant S as ChatContext - participant A as Axios - participant W as Web-Gin - - Note over P,W: 页面加载时自动拉历史 - P->>S: useEffect 触发 - S->>A: GET /api/chat/history - A->>W: 带 JWT - W-->>A: 历史消息数组 - A-->>S: state.push(...records) - S-->>P: 渲染对话流 - - Note over P,W: 用户输入新提问 - P->>A: POST /api/chat/messages {question} - A->>W: 经 gRPC 转发到 Logic - W-->>A: {reply, records[]} - A-->>S: 追加 AI 回复 - S-->>P: 自动滚动到底部 -``` - -### 4.5 收尾 - -> [!note] 进度追踪 -> - [ ] T4.Z.1 两端 `npm run build` 验证无编译错误 -> - [ ] T4.Z.2 检查 .env 是否正确指向后端地址 -> - [ ] T4.Z.3 git add + commit 本阶段变更 - -> [!warning] 前端权限边界 -> 前端只做视觉展示和跳转控制。**真正的权限校验必须发生在后端**。例如:即使前端显示了投递按钮,后端也应该再次校验候选人是否满足条件;不应依赖前端隐藏按钮来保护安全。验收时评委可能会故意绕过前端直接调接口测试。 - -> [!tip] 加速技巧速查 -> - Ant Design 的 `Form` + `Input` + `Select` 可以直接粘贴文档示例修改 -> - 岗位列表复用同一个 `JobCard` 组件(候选人端和 HR 端都可以引用) -> - Axios interceptor 写一次就能用在两个项目中 -> - 如果某个页面实在写不完,先用 `console.log` 占位,确保核心功能优先上线 - -## 六、阶段五:前后端联调与自测 - -> [!note] 进度追踪 -> - [ ] T5.1 全链路跑通:注册 → 登录 → 岗位发布 → 浏览 -> - [ ] T5.2 OSS 签名上传端到端测试(候选人端直传) -> - [ ] T5.3 AI 对话完整流程测试(提问 → 数据查询 → 回答 → 历史加载) -> - [ ] T5.4 权限隔离测试(非创建者操作他人岗位应被拒绝) -> - [ ] T5.5 文件格式拦截测试(上传图片/TXT应为非法) -> - [ ] T5.6 游客 vs 登录态功能边界测试 -> - [ ] T5.7 Bug 修复与体验优化 - -### 6.1 详细任务清单 - -| 编号 | 任务 | 前置依赖 | 耗时估计 | -|------|------|----------|----------| -| T5.1 | 全链路跑通:注册 → 登录 → 岗位发布 → 浏览 | T4.Z.3 | 2h | -| T5.2 | OSS 签名上传端到端测试(候选人端直传) | T5.1 | 2h | -| T5.3 | AI 对话完整流程测试(提问 → 数据查询 → 回答 → 历史加载) | T5.1 | 2h | -| T5.4 | 权限隔离测试(非创建者操作他人岗位应被拒绝) | T5.1 | 1h | -| T5.5 | 文件格式拦截测试(上传图片/TXT应为非法) | T5.2 | 1h | -| T5.6 | 游客 vs 登录态功能边界测试 | T5.1 | 1h | -| T5.7 | Bug 修复与体验优化 | T5.1 ~ T5.6 | 2h | - -### 6.2 验收 Checklist - -对照以下每一项逐项打勾: - -- [ ] gRPC 分层:Web 与 Logic 之间全部通过 gRPC 调用,无同工程内部函数调用 -- [ ] OSS 签名 URL:文件不落地本地,客户端直传 OSS -- [ ] Eino 框架:使用了 Eino 封装的 Chat,非裸写 HTTP -- [ ] JWT 鉴权:HR 只能管理自己发布的岗位,候选人未完善资料不可投递 -- [ ] AI 对话持久化:每条问答对入 MySQL,刷新页面自动加载历史上下文 -- [ ] 双端前端独立运行:hr-frontend 和 user-frontend 各自 `npm run dev` 均可启动 -- [ ] 四个源码目录完整、三个文档文件齐全 - -## 七、阶段六:视频录制与提交 - -> [!note] 进度追踪 -> - [ ] T6.1 撰写 answer.md(拓展设计方案:OpenClaw/Hermes 集成思路) -> - [ ] T6.2 完善 README.md(启动部署指南 + 项目亮点) -> - [ ] T6.3 完善 api.md(前后端接口说明) -> - [ ] T6.4 完善 db.md(数据库设计文档补充) -> - [ ] T6.5 整理代码仓库(检查 .gitignore、清理临时文件) -> - [ ] T6.6 录制演示视频(原生实操、口述两大核心技术点) -> - [ ] T6.7 提交至 KDoc 表单 - -### 7.1 详细任务清单 - -| 编号 | 任务 | 耗时估计 | -|------|------|----------| -| T6.1 | 撰写 answer.md(拓展设计方案:OpenClaw/Hermes 集成思路) | 2h | -| T6.2 | 完善 README.md(启动部署指南 + 项目亮点) | 1.5h | -| T6.3 | 完善 api.md(前后端接口说明) | 1.5h | -| T6.4 | 完善 db.md(数据库设计文档补充) | 1h | -| T6.5 | 整理代码仓库(检查 .gitignore、清理临时文件) | 1h | -| T6.6 | 录制演示视频(原生实操、口述两大核心技术点) | 2h | -| T6.7 | 提交至 KDoc 表单 | 30min | - -### 7.2 视频录制脚本大纲 - -| 时间段 | 内容 | 口述要点 | -|--------|------|----------| -| 0:00-0:30 | 开场 + 服务启动演示 | "我现在依次启动 Logic 服务和 Web 网关服务..." | -| 0:30-2:00 | 候选人端操作流程 | "候选人浏览岗位、注册、完善档案、上传简历、投递..." | -| 2:00-3:00 | 口述核心技术点①:gRPC 两层架构 | "Web 网关接收 HTTP 请求后,通过 gRPC 远程调用 Logic 服务...这两个是独立进程..." | -| 3:00-4:00 | 口述核心技术点②:OSS 签名 URL | "简历文件先获取签名URL,然后客户端直传 OSS,服务端零缓存..." | -| 4:00-5:30 | HR 管理端操作流程 | "HR 登录、发布岗位、查看候选人、AI 对话..." | -| 5:30-6:30 | AI 对话演示 | "输入自然语言提问,后端查询 MySQL 真实数据,通过 Eino 推送大模型..." | -| 6:30-7:00 | 总结与结尾 | 简要说明个人开发收获和优化方向 | - -> [!important] 视频硬性要求 -> - **禁止剪辑拼接**:一次录完,保证画面真实 -> - **全程配语音解说**:不能无声黑屏 -> - **必录两大技术点口述**:gRPC 分层调用逻辑、OSS 签名 URL 安全上传下载 -> - **文件命名**:`姓名_学号_全栈大作业.mp4` - -## 八、风险识别与应对 - -| 风险 | 影响范围 | 概率 | 应对措施 | -|------|----------|------|----------| -| OSS 平台注册审核慢 | 整个项目 | 中 | 提前注册,使用 MinIO 本地替代方案作为兜底 | -| Eino 框架学习成本超预期 | AI 对话模块 | 中 | 官方文档优先,只使用基础 Chat 组件,不做复杂编排 | -| Protobuf 字段频繁变更 | 前后端同步 | 高 | Proto 先行、冻结接口后再写业务代码;做好注释 | -| gRPC 调试困难 | 后端通信 | 中 | 启用 grpcurl 命令行工具进行 RPC 调用调试 | -| 前端样式适配耗时过长 | UI 呈现 | 中 | 使用现成 UI 组件库(Ant Design),不自研 CSS | -| JWT Token 过期处理遗漏 | 用户体验 | 低 | 初期可不实现自动刷新,手动重新登录即可 | - -## 九、里程碑检查点 - -> [!note] 里程碑进度追踪 -> 点击以下复选框标记里程碑完成情况: -> - [ ] **M0** — 环境就绪 -> - [ ] **M1** — Proto 冻结 -> - [ ] **M2** — 后端可独立运行 -> - [ ] **M3** — 双端前端完成 -> - [ ] **M4** — 全链路联调通过 -> - [ ] **M5** — 视频已录制 -> - [ ] **M6** — 正式提交 - -```mermaid -graph LR - M0["环境就绪
阶段一"] --> M1["Proto 冻结
阶段二"] - M1 --> M2["后端可独立运行
阶段三"] - M2 --> M3["双端前端完成
阶段四"] - M3 --> M4["全链路联调通过
阶段五"] - M4 --> M5["视频已录制
待提交"] - M5 --> M6["正式提交
KDoc表单"] - - classDef done fill:#c8e6c9; - classDef current fill:#fff9c4; - classDef future fill:#eceff1; - class M0,M1,M2,M3,M4 done; - class M5 current; - class M6 future; -``` - -## 关联笔记 - -- [[大作业项目要求]] — 作业完整需求文档 -- [[思维导图]] — 双端 gRPC HR 系统的知识体系与核心考点 -- [[工程结构]] — 详细的仓库目录结构与代码组织 diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md index 4e58725..3c183d9 100644 --- a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md +++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册.md @@ -13,8 +13,8 @@ create time: 2026-05-11 16:00 ### 入门 -| # | 笔记 | 读它如果... | -|---|------|------------| +| # | 笔记 | 读它如果... | +| ----------------------------------------------------------------- | ------------------------------ | ---------------------- | | [[hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/01-Hello World 最小可运行 Server]] | **第一课**:三段式模板,从零搭起一个能跑的 server | 你是 gRPC 新手,想先看到"能跑的代码" | ### 核心概念 diff --git a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md index e47e15b..03bf2ea 100644 --- a/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md +++ b/hhs/gRPC/3. 服务端实现/08-Server 搭建与注册/00-Unimplemented 零-Stub 模式.md @@ -1,20 +1,22 @@ --- tags: [gRPC, Go, Server, Best Practice] -create time: 2026-05-13 14:30 +create time: 2026-05-15 10:00 --- # Unimplemented 零-Stub 模式 ## 概述 -`UnimplementedXxxServer` 是 protoc-gen-go 为每个 gRPC service 自动生成的"空实现"结构体。通过嵌入它到自定义的 Service 结构体中,开发者只需编写实际需要的方法,其余方法自动获得 `codes.Unimplemented` 默认响应——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。 +`UnimplementedXxxServer` 是 `protoc-gen-go` 为每个 gRPC service 自动生成的"空实现"结构体。通过将其嵌入自定义 Service 结构体,开发者只需编写实际需要的方法,未覆写的方法自动返回 `codes.Unimplemented` 错误——这是一种兼顾开发便利性与接口契约安全的 Go 语言惯用模式。 > [!question] 如果没有这个机制会怎样? -> 想象你实现了三个 RPC method,后来 proto 新增了一个,编译器不报错——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。 +> 假设你实现了三个 RPC method,后来 proto 新增了一个。**编译器不会报错**——你的新方法永远不会被调用,直到线上某个 client 发出请求后才暴露出 Bug。这就是"静默失败"陷阱。 + +核心思想:**把运行时 Bug 提前到编译期发现**。 ## Protobuf 生成的代码结构 -当你写一个 proto service: +当你定义一个 proto service: ```proto service UserService { @@ -23,12 +25,12 @@ service UserService { } ``` -protoc 会生成三类关键代码: +protoc 生成三类关键代码: | 生成物 | 作用 | |--------|------| -| `UserServiceServer` interface | 你必须实现的方法集合 | -| `UnimplementedUserServiceServer` struct | 所有方法都返回 `Unimplemented` 错误的默认实现 | +| `UserServiceServer` interface | 你必须满足的方法集合 | +| `UnimplementedUserServiceServer` struct | 所有方法都返回 `codes.Unimplemented` 错误的默认实现 | | `RegisterUserServiceServer()` 函数 | 将你的实现注册到 server 分发表 | 重点看 `UserServiceServer` interface 的定义: @@ -41,13 +43,14 @@ type UserServiceServer interface { } ``` -`mustEmbedUnimplementedUserServiceServer()` 是一个**无参数的空方法**。它的存在让 interface 成为封闭类型——其他任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)反模式之一,但在 protobuf 场景下它是有意为之的设计。 +`mustEmbedUnimplementedUserServiceServer()` 是一个无参空方法。它让 interface 成为**封闭类型**——任何想满足此接口的类型都必须显式嵌入 `UnimplementedUserServiceServer`,否则编译期直接拦截。这是 Go 社区著名的"封闭接口"(unexported method trick)设计手法,在 protobuf 场景下是有意为之。 -## 为什么必须嵌入 +> [!note] 为什么叫 "closed set of methods"? +> Go 的 interface 是 implicit(隐式实现),正常情况下你可以随时"默默"实现一个新接口。但 protobuf 通过注入一个只能由生成的 struct 提供的方法,把这个隐式契约变成了**显式嵌入**——你必须主动写下一行 `pb.UnimplementedUserServiceServer`,这种"被迫直面"就是安全感的来源。 -### 1. 减少样板代码 +## 嵌入用法 -嵌入后只需要实现你需要的那些方法: +嵌入后只需实现需要的业务方法: ```go type userService struct { @@ -62,89 +65,119 @@ func (s *userService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) return &pb.CreateUserResponse{Id: generatedId}, nil } -// GetUser can be omitted — UnimplementedUserServiceServer returns the default error: +// GetUser 不写 — UnimplementedUserServiceServer 自动返回: // status.Error(codes.Unimplemented, "method GetUser not implemented") ``` -### 2. Proto 新增 RPC Method → 编译期告警 +### Proto 新增 Method → 编译期告警 -这是该模式最大的价值。假设后续你在 proto 里加了一个方法: +这是该模式最大的价值。**先回答一个疑惑**:proto 新增了 `DeleteUser`,重新运行 protoc 后 `UnimplementedUserServiceServer` 自身也会多出对应的默认实现,你的嵌入结构体照样能编译通过——那为什么说有风险前置? -```proto -service UserService { - rpc CreateUser(...) returns (...); - rpc GetUser(...) returns (...); - rpc DeleteUser(...) returns (...); // ← 新增 -} -``` +原因在于两个实际场景: -重新运行 protoc 后,`UserServiceServer` interface 会多出 `DeleteUser` 签名。**此时编译就会失败**: +**场景 A:protoc 重新生成后忘记更新依赖** +如果你在同一份代码库里只改了 proto 文件,却忘了对所有 micro-service 跑 protoc,那些没更新的 service 就会报编译错误: ``` cannot use &userService{} as UserServiceServer: -missing method DeleteUser in receiver type userService + missing method DeleteUser in receiver type userService ``` -从"线上偶发 Bug"变成了"提交前就发现"——把风险前置到了编译期。 +CI 的编译检查会直接拦住——**某个服务漏跑 protoc**,而不是等到线上被调用的时候才发现这个方法从未被实现。 -### 3. 强制接口契约意识 +**场景 B:忘记嵌入 → 退化回普通接口** +如果不 embed `UnimplementedXxxServer`,proto 新增 method 时编译器照样不拦你。新方法永远不会被调用,直到线上某个 client 触发了它——这就退化成了"没这个机制之前"的样子。这也是为什么"正确嵌入"是这套模式生效的前提。 -如果不用 embed,你可以"选择性实现"——只有某些方法、其他全忽略。这会导致服务方和客户端对接口理解不一致。Zero-stub 模式强迫每次新增 method 时都做一次有意识的决策:**实现它**,还是保持 Unimplemented? +--- -## 与反射型语言的对比 +> [!question] 那"强制接口契约意识"怎么理解? +> +> 正确用法下,Proto 新增 method 后,`UnimplementedXxxServer` 也会跟着长出新的兜底实现,编译是**通过的**。但这正是设计者想要的效果: +> - **不强制你立刻实现**(不影响正常请求) +> - **但强制你在 protoc 重新生成的那一刻"看到"变更** +> - 从此时起你有两次决策机会:① 跑 protoc 时意识到契约变了;② 决定是否要把 `codes.Unimplemented` 替换成真实逻辑 +> - 对比非类型安全语言(Java 等),新方法可以永远不被实现且永远不被发现——Go 版本至少让每次 proto 变更都变得**可追踪** -在 Java 等语言中,gRPC 通常要求你继承一个基类(如 `UserServiceImplBase`),效果类似: +## 嵌入策略对比 -```java -public class UserServiceImpl extends UserServiceImplBase { - @Override - public void createUser(..., StreamObserver observer) { - // 只重写需要的 - } - // getUser 自动走父类的 Unimplemented +不同语言的 gRPC 框架提供了类似的保底机制,但 Go 的嵌入方式有其独特优势: + +| 维度 | Go(嵌入) | Java / C++(继承基类) | Python(抽象基类) | +|------|-----------|----------------------|-------------------| +| **语法** | `struct` 嵌入 | 继承 `ServiceImplBase` | 继承 `Servicer` + `@abc.abstractmethod` | +| **单继承限制** | 无(可嵌多个类型) | 有 | 有 | +| **方法优先级控制** | 嵌入顺序决定 | 虚函数重写 | MRO 决定 | +| **扩展性** | 可同时混入 store、logger 等依赖 | 只能通过构造函数传递 | 同上 | + +Go 版本的优势在于**嵌入比继承更灵活**——可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。 + +## Streaming RPC 下的行为 + +上面的例子都是 unary RPC。对于 streaming RPC,behavior 完全一致: + +```go +type MessageService struct { + pb.UnimplementedMessageServiceServer } + +// 只实现 stream-send 的场景 +func (s *MessageService) StreamMessages(req *StreamRequest, ss grpc.ServerStream) error { + for i := 0; i < req.Count; i++ { + if err := ss.SendMsg(&StreamResponse{Data: fmt.Sprintf("msg-%d", i)}); err != nil { + return status.Errorf(codes.Internal, "send failed: %v", err) + } + } + return nil +} + +// BidirectionalStreaming 不写 → 自动返回 codes.Unimplemented ``` -Go 版本的优势在于**嵌入比继承更灵活**——你可以同时嵌入多个类型、控制方法优先级(离当前结构体越近优先匹配),并且没有单继承限制。 +关键点: + +- UnimplementedStub 对 unary、server-streaming、client-streaming、bidirectional-streaming **统一处理** +- Streaming handler 的错误返回值仍然是 `error`,未覆写时同样走 `codes.Unimplemented` +- 如果你在 CI 中有编译检查,streaming 方法的遗漏也能被捕获 ## 执行流程图解 ```mermaid flowchart TD - Client["Client RPC Call"] --> Lookup["Server 查找 dispatch table"] + Client["Client RPC Call"] --> Lookup["Server dispatch table"] Lookup --> Found{"Method 是否被覆写?"} Found -->|"是"| Handler["Handler 执行业务逻辑"] - Found -->|"否"| Fallback["UnimplementedStub
codes.Unimplemented 错误"] + Found -->|"否"| Fallback["UnimplementedStub
codes.Unimplemented"] Handler --> Resp["Response to Client"] Fallback --> Resp ProtoChanged["Proto 新增 Method"] --> GenCode["Protoc 重新生成"] - GenCode --> CompileErr["userService 缺少新方法,编译失败 ✅"] + GenCode --> AllUpdated["所有 service 同步更新
Embed 自动继承新方法,编译通过"] + GenCode --> PartialUpdate["部分 service 漏跑 protoc
CI 拦截 ✅"] style Client fill:#E3F2FD,stroke:#1976D2 style Handler fill:#A8E6CF,stroke:#2E7D32 style Fallback fill:#FFB3BA,stroke:#C62828 - style CompileErr fill:#FFF3E0,color:#000,stroke:#E65100 + style AllUpdated fill:#FFF3E0,color:#000,stroke:#E65100 + style PartialUpdate fill:#FFF3E0,color:#000,stroke:#E65100 ``` ## 实际使用中的注意事项 > [!warning] 不要手动实现 Unimplemented 方法 +> +> 如果在自己的结构体中也定义了同名 method(比如不小心写了 `MustEmbedUnimplementedUserServiceServer()`),它会覆盖嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。 -如果你在自己的结构体中也定义了一个同名 method(比如不小心写了 `func (s *userService) MustEmbedUnimplementedUserServiceServer()`),它会**覆盖**嵌入类型的空方法。虽然功能上不影响(interface 检查只看是否存在),但语义混乱,应避免。 - -> [!tip] 如何快速确认某个 Service 的所有方法都被覆盖了? - -可以用 IDE 的 "implement interface" 功能列出未实现的方法,或者写一个简单的 compile test: - -```go -var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查 -``` - -加上这一行后,任何遗漏的方法都会在这句编译报错,适合作为 CI 的一部分。 - -## 常见陷阱 +> [!warning] embed vs pointer embed 的选择 +> +> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同: +> +> | 嵌入方式 | `*userService` 是否满足接口 | 说明 | +> |---------|---------------------------|------| +> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 | +> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 | +> +> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。 > [!danger] 嵌入顺序决定方法优先级 > @@ -157,46 +190,38 @@ var _ pb.UserServiceServer = (*userService)(nil) // 编译期检查 > } > ``` > -> 养成**将 Unimplemented 嵌入放在最后**的习惯,避免意外覆盖: +> 养成**将 Unimplemented 嵌入放在最后**的习惯: > > ```go > type userService struct { > *UserStore // 业务依赖先放前面 -> someHelper // 工具类 +> logger // 工具类 > pb.UnimplementedUserServiceServer // 永远放最后 > } > ``` -> [!warning] `embed` vs `pointer embed` 的选择 -> -> gRPC 生成的 `UnimplementedXxxServer` **值类型和指针类型都可以嵌入**,但效果不同: -> -> | 嵌入方式 | `*userService` 是否满足接口 | 说明 | -> |---------|---------------------------|------| -> | `pb.UnimplementedXxxServer`(值) | `*userService` ✅ | 推荐,指针接收者可调用值方法 | -> | `*pb.UnimplementedXxxServer`(指针) | `*userService` ✅ 且 `userService` ❌ | 仅指针满足接口 | -> -> **最佳实践**: 大多数 gRPC server 方法签名是 `func (s *Service) Method(...)`,所以用指针 receiver。嵌入值类型的 `Unimplemented` 即可同时支持两种调用方式。 - ## Server 集成示例 -下面是一个完整的、可直接运行的 Server 搭建片段,展示 UnimplementedStub 在整个链路中的位置: +下面展示 UnimplementedStub 在整个服务链路中的位置(核心逻辑已简化): ```go package server import ( "context" + "fmt" "log" "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" pb "your/proto/package" ) // UserServiceImpl — 只需实现实际需要的方法 type UserServiceImpl struct { - store *UserStore // 业务依赖 - pb.UnimplementedUserServiceServer // 放最后 + store *UserStore // 业务依赖 + pb.UnimplementedUserServiceServer // 永远放最后 } func (s *UserServiceImpl) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { @@ -222,9 +247,39 @@ func Register(srv *grpc.Server, store *UserStore) { ``` 关键要点: -- `RegisterUserServiceServer()` 内部会校验你传入的类型是否实现了 `UserServiceServer` interface + +- `RegisterUserServiceServer()` 内部会校验传入的类型是否实现了 `UserServiceServer` interface - 由于 `UnimplementedUserServiceServer` 提供了所有方法的默认实现,你只需编写实际需要的 method -- Proto 新增 method → protoc 重新生成 → 编译失败 → **有意识的决策点** +- Proto 新增 method → protoc 重新生成 → **变更暴露** → 有意识的决策点(是否把 `codes.Unimplemented` 替换成真实逻辑) + +## 常见排错 + +> [!bug] 忘记 embed → "missing method xxx" 编译错误 +> +> 如果你看到类似下面的错误,首先检查是否忘写了嵌入行: +> +> ``` +> cannot use &userService{} as UserServiceServer: +> missing method CreateUser in receiver type userService +> ``` +> +> 确认嵌入 `pb.UnimplementedXxxServer` 后即可解决。 + +> [!bug] embed 的是指针而不是值 → `userService` 类型不满足接口 +> +> ```go +> type userService struct { +> *pb.UnimplementedUserServiceServer // 指针嵌入 +> } +> var _ UserServiceServer = &userService{} // ✅ +> var _ UserServiceServer = userService{} // ❌ compile error +> ``` +> +> 如果你的某些代码以值类型传 service,建议改用值嵌入。 + +> [!bug] 自定义方法与嵌入类型冲突 +> +> 如果你在结构体上手动定义了 `mustEmbedUnimplementedUserServiceServer()` 或其他与嵌入类型同名的方法,它会在方法解析时**遮蔽**嵌入类型的方法——虽然通常不会影响最终行为,但会造成混淆和潜在 bug。 ## 关联笔记 diff --git a/hzh/DEV/跨域问题调试.md b/hzh/DEV/跨域问题调试.md index 83e7d2e..af7f988 100644 --- a/hzh/DEV/跨域问题调试.md +++ b/hzh/DEV/跨域问题调试.md @@ -166,6 +166,8 @@ log.Printf("Response headers: Allow-Origin=%s", c.Writer.Header().Get("Access-Co #### 场景 4:Nginx / 反向代理挡住了 CORS header +> 详细的 Nginx 反向代理处理跨域的三种方案,详见 [[hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors]]。 + 当你使用 Nginx 做反向代理时,它默认会过滤掉上游没有明确声明的 header。 ```nginx @@ -264,6 +266,7 @@ export default { ## 关联笔记 +- [[hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors]] — Nginx 反向代理处理跨域的三种方案 - [[GIN/3-middleware/cors-registration-scope]] — 全局 vs 分组注册 CORS 中间件的取舍 - [[GIN/3-middleware/cors-preflight]] — OPTIONS 预检请求的触发条件与两轮对话流程 - [[GIN/3-middleware]] — 中间件三级作用域机制 \ No newline at end of file diff --git a/hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors.md b/hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors.md new file mode 100644 index 0000000..7ab7e1f --- /dev/null +++ b/hzh/DEV/跨域问题调试/nginx-reverse-proxy-cors.md @@ -0,0 +1,244 @@ +--- +tags: [Nginx, 反向代理, 跨域, CORS, 运维] +create time: 2026-05-15 18:00 +--- + +# Nginx 反向代理处理跨域 + +## 概述 + +本文档讲解如何利用 Nginx 反向代理解决浏览器端跨域(CORS)问题,从"最干净的方案"到"兜底方案"逐一展开,覆盖生产环境的典型场景。 + +> [!TIP] 核心思路 +> 跨域是浏览器的安全策略,不是后端的限制。Nginx 做反向代理的核心思想:**让浏览器看到的地址和页面同源**,或者 **由 Nginx 在响应头中注入合法的 CORS header**。 + +## 正文 + +### 三种方案总览 + +```mermaid +flowchart LR + A[遇到跨域问题] --> B{能否统一域名?} + B -- 能 --> C["方案一: 同源代理
零跨域, 最干净"] + B -- 不能 --> D{后端已设 CORS header?} + D -- 能控制后端 --> E["方案二: Nginx 注入
网关统一管理"] + D -- 无法改后端 --> F["方案三: 透传 Header
保留上游配置"] +``` + +--- + +### 方案一:同源代理 — 从根本上消灭跨域(推荐) + +请求的域名和页面域名完全一致,浏览器根本不认为这是跨域请求。 + +```nginx +server { + listen 80; + server_name app.example.com; + + # 前端静态资源 + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + } + + # API 代理 — 浏览器看到 /api/xxx,和页面同源 + location /api/ { + proxy_pass http://backend:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +**请求链路示意:** + +```mermaid +sequenceDiagram + participant B as 浏览器 + participant N as Nginx + participant S as 后端服务 + + B->>N: GET /api/user (同源,无预检) + N->>S: GET /api/user + S-->>N: 200 OK (body) + N-->>B: 200 OK (body,不含 CORS header) +``` + +| 维度 | 说明 | +|------|------| +| 是否真正解决跨域 | ✅ 根本解决,浏览器不认为存在跨域 | +| Cookie / Session | 天然支持,无需额外配置 | +| 安全性 | 最高,不存在任何跨域风险 | +| 适用前提 | 你能控制前端部署域名 | + +> **思考**:如果前端和后端分别部署在不同的子域名(如 `app.example.com` 和 `api.example.com`),这个方案还适用吗?不适用——这就是下面两种方案的用武之地。 + +--- + +### 方案二:Nginx 注入 CORS header(前后端无法同域时) + +当 API 确实需要独立域名时,Nginx 充当"CORS 网关",负责注入正确的响应头。 + +```nginx +server { + listen 80; + server_name api.example.com; + + # 允许的合法前端域名列表 + set $cors_origin ""; + + if ($http_origin ~* "^https://(app\.example\.com|dev\.example\.com)$") { + set $cors_origin $http_origin; + } + + location /api/ { + proxy_pass http://backend:8080; + + # 白名单校验后的 Origin(未匹配则不注入,防止随意跨域) + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Token" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + add_header Access-Control-Max-Age "3600" always; + add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-Id" always; + + # 预检请求直接响应 204 + if ($request_method = OPTIONS) { + return 204; + } + } +} +``` + +**为什么这里用了正则白名单而不是直接回显 `$http_origin`?** + +因为直接回显虽然省事,但意味着**任意域名**都能通过白名单校验拿到 CORS header,等同于变相开放。通过 `~*` 正则限定具体域名,是安全加固的一层。 + +#### 关键参数解释 + +| 参数 | 作用 | 注意事项 | +|------|------|---------| +| `add_header ... always` | `always` 确保即使后端返回 4xx/5xx 也加上 CORS header | 不加 `always` 时错误响应会丢失跨域信息 | +| `set $cors_origin ""` | 初始化为空,默认不允许跨域 | 这是一种"拒绝所有、放行白名单"的安全策略 | +| `if ($request_method = OPTIONS)` | 拦截预检请求,避免转发给后端 | Nginx 的 `if` 在此处使用是安全的(官方推荐用法) | +| `Access-Control-Max-Age` | 浏览器缓存预检结果的时间(秒) | 开发时建议设为 `0`,生产可设 `3600`~`86400` | + +> [!WARNING] Credential 模式陷阱 +> 如果设置了 `Access-Control-Allow-Credentials: true`,`Access-Control-Allow-Origin` **绝对不能是 `*`**。必须动态回显具体域名或走白名单——上面的配置已经通过白名单规避了这个问题。 + +--- + +### 方案三:透传后端 CORS header + +当后端已经正确配置了 CORS header 时,**默认情况下 Nginx 不会移除这些响应头**,所以大多数场景你不需要任何额外配置。 + +> [!TIP] Nginx Header 传递规则(重点) +> - **无 `add_header` 时**:Nginx 完整透传上游响应头(包括 CORS header),且子级 `location` 会继承父级的 `add_header`。 +> - **有 `add_header` 时**:Nginx **丢弃所有父级 `add_header` + 上游的 CORS header**。此时必须显式重新添加需要的头。 +> +> 这是最常见的坑——你在某个 `location` 里加了一个自定义 header,结果发现 CORS header 突然消失了。 + +```nginx +# 如果你的 Nginx 配置里只有纯粹的 proxy_pass,无需额外操作: +location /api/ { + proxy_pass http://backend:8080; +} +``` + +但如果因为其他原因你已经在这个 `location` 里有 `add_header`(比如加了自定义 X-Custom-Header),就需要把 CORS header 一并补上: + +```nginx +location /api/ { + proxy_pass http://backend:8080; + + # 原有自定义 header + add_header X-Request-Id $request_id always; + + # 补充 CORS header(否则会被 Nginx 吞掉) + proxy_hide_header Access-Control-Allow-Origin; + if ($http_origin ~* "^https://.+\.example\.com$") { + add_header Access-Control-Allow-Origin $http_origin always; + add_header Access-Control-Allow-Credentials "true" always; + } +} +``` + +| 做法 | 效果 | 适用场景 | +|------|------|---------| +| 不加任何 header | ✅ 完全透传后端 CORS | 后端已配置 CORS,Nginx 只做纯代理 | +| 仅加 `proxy_hide_header` | ⚠️ 不发送 CORS header | 需要临时屏蔽后端的 CORS | +| 加 `add_header` + `proxy_hide_header` | ✅ 手动重建 CORS | 已在该 location 有其他 header 需共存时 | + +> [!NOTE] 最佳实践建议 +> 如果后端和前端都在你的控制范围内,**统一将 CORS 配置放在后端中间件层**是最推荐的做法。Nginx 只负责路由转发,不做跨域决策——这样配置清晰、易于维护和审计。只有当你无法修改后端代码时,才需要在 Nginx 层"代劳"设置 CORS header(即回到方案二)。 + +--- + +### 常见陷阱与调试技巧 + +#### 陷阱一:`proxy_pass` 尾部斜杠的语义差异 + +`location /api/` 和 `proxy_pass` URL 末尾的 `/` 组合会产生不同行为,这是最容易踩的坑。 + +| `location` | `proxy_pass` 结尾 | 请求 `/api/user` → 转发给后端的路径 | +|------------|------------------|-----------------------------------| +| `/api/` | 无 `/`(`http://backend:8080`) | `/api/user`(原样转发) | +| `/api/` | 有 `/`(`http://backend:8080/`) | `/user`(`/api/` 被截断替换) | + +> **记忆口诀**:`proxy_pass` URL 末尾有没有 `/`,决定了 location 匹配部分是否会被截断。 +> - 没 `/` → 保持前端路径前缀不变(适合前后端共用同一个 API 前缀) +> - 有 `/` → 自动去掉 location 前缀(适合后端路由不需要前缀的场景) + +#### 陷阱二:CORS header 突然消失 — Nginx 的继承规则 + +Nginx 的 `add_header` 有一个非常反直觉的行为:**一旦你在某个层级定义了 `add_header`,该层级及以下会丢弃所有父级的 header + 上游响应头。** + +排查方法: +1. 用 `curl -vI https://api.example.com/api/test` 直接看响应头 +2. 如果能看到 CORS header → 是 Nginx 配置层覆盖问题,回到方案三的规则修复 +3. 如果看不到 → 检查后端实际返回了哪些 header(可能后端根本没设) + +#### 陷阱三:`Access-Control-Allow-Headers` 遗漏字段 + +浏览器预检时会带上自定义 header(如 `X-Token`, `Authorization`),如果 Nginx 白名单中不包含这些字段,预检直接失败: + +```bash +# 在控制台观察到的典型错误 +# Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy: +# Request header field x-token is not allowed by Access-Control-Allow-Headers in preflight response. +``` + +解决:确保 `Access-Control-Allow-Headers` 包含前端所有可能发送的自定义 header。 + +#### 调试 Checklist + +```mermaid +flowchart TD + A[浏览器报 CORS 错误] --> B{curl 能正常访问?} + B -- No --> C["检查后端服务是否存活
及健康检查路径"] + B -- Yes --> D{"Response Header 有 CORS?"} + D -- No --> E["后端未设置 CORS header
→ 在后端中间件中添加"] + D -- Yes --> F{"curl 有但浏览器没有?"} + F -- Yes --> G["Nginx add_header 覆盖了上游
→ 检查方案三的继承规则"] + F -- No --> H["检查 Response Status Code
是否为 2xx/3xx?
(4xx/5xx 需加 always)"] +``` + +--- + +### 对比总结 + +| 维度 | 方案一(同源代理) | 方案二(Nginx 注入) | 方案三(透传后端) | +|------|------------------|--------------------|-------------------| +| 是否真正解决跨域 | ✅ 根本解决 | ⚠️ 表面绕过 | ⚠️ 取决于后端 | +| 安全性 | 最高(无跨域) | 高(白名单可控) | 低(信任后端) | +| Cookie 支持 | ✅ 天然支持 | ✅ 配合白名单可用 | ✅ 天然支持 | +| 维护成本 | 低 | 中等(域名变更需改配置) | 最低 | +| 推荐优先级 | 🥇 首选 | 🥈 次选 | 🥉 兜底 | + +## 关联笔记 + +- [[hzh/DEV/跨域问题调试]] — 跨域排查完整指南 +- [[GIN/3-middleware/cors-registration-scope]] — 后端 Go/Gin 端 CORS 中间件配置参考 diff --git a/hzh/GIN/11-static-files.md b/hzh/GIN/11-static-files.md index 1a87263..dff6bf9 100644 --- a/hzh/GIN/11-static-files.md +++ b/hzh/GIN/11-static-files.md @@ -7,7 +7,7 @@ create time: 2026-04-28 00:00 ## 概述 -Gin 提供了三种方式提供静态资源:`Static`(目录映射)、`StaticFS`(自定义文件系统)和 `StaticFile`(单文件)。理解它们的区别和使用场景,是构建完整 Web 服务的基础。 +Gin 提供了三种核心 API 提供静态资源:`Static`(目录映射)、`StaticFS`(自定义文件系统)和 `StaticFile`(单文件)。此外,通过 `io.Reader` 直接返回文件流以及自定义中间件也是实际项目中常见的手段。理解它们的区别和使用场景,是构建完整 Web 服务的基础。 思考题:为什么生产环境通常不推荐用 Go 直接提供静态文件?Nginx/CDN 相比有什么优势? @@ -94,9 +94,9 @@ r.StaticFile("/robots.txt", "./static/robots.txt") r.Run(":8080") ``` -### 4. `io.Reader` 直接返回文件流 +### 4. 从内存直接返回文件 -对于不需要落盘的文件(如数据库读取的图片、动态生成的 PDF),可以直接从 Reader 输出: +对于不需要落盘的文件(如数据库读取的图片、动态生成的 PDF),可以直接从内存输出到响应体: ```go func getFileFromDB(c *gin.Context) { @@ -107,14 +107,14 @@ func getFileFromDB(c *gin.Context) { return } - // 直接用 io.Reader 写入响应 + // DataBytes 直接写入 body,比 SetHeader + Write 更简洁 c.DataBytes(http.StatusOK, contentType, fileData) - // 或者 - // c.Writer.Header().Set("Content-Type", contentType) - // c.Writer.Write(fileData) } ``` +> [!tip] Reader vs []byte +> Gin 没有提供专门的 `Reader` 接口写入流式数据。大文件场景建议配合 `c.File()`(内部使用 `sendfile`)或手动实现分块写入,避免一次性加载整个文件到内存。 + ### 5. 自定义文件服务器 如果需要控制缓存头、限速、访问权限等,可以自己实现 `StaticFS`: @@ -124,19 +124,22 @@ func secureStatic() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path - // 安全检查:防止目录遍历攻击 + // 安全检查:确保请求路径在允许的目录范围内 + // filepath.Clean 会解析 "..",防止目录遍历攻击 cleanPath := filepath.Clean(path) - if strings.Contains(cleanPath, "..") { + baseDir, _ := filepath.Abs("./static") // 允许的基础目录 + targetPath, _ := filepath.Abs(filepath.Join(baseDir, cleanPath)) + if !strings.HasPrefix(targetPath, baseDir+"/") && targetPath != baseDir { c.AbortWithStatus(http.StatusForbidden) return } - // 设置缓存头 + // 设置缓存和安全头 c.Header("Cache-Control", "public, max-age=31536000") // 一年 c.Header("X-Content-Type-Options", "nosniff") // 交给内置文件服务器 - http.FileServer(http.Dir("./static")).ServeHTTP(c.Writer, c.Request) + http.FileServer(http.Dir(baseDir)).ServeHTTP(c.Writer, c.Request) } } @@ -148,6 +151,9 @@ func main() { } ``` +> [!warning] 常见错误 +> 不要这样写:`strings.Contains(path, "..")`。因为 `filepath.Clean()` 已经解析了 `..`,清理后的路径中不会出现 `..`。正确做法是比较**解析后**的绝对路径是否仍在允许的基目录内。 + ### 6. 静态文件 vs API 性能对比 ```mermaid diff --git a/hzh/GIN/12-server-config.md b/hzh/GIN/12-server-config.md index 17da547..76aa1fe 100644 --- a/hzh/GIN/12-server-config.md +++ b/hzh/GIN/12-server-config.md @@ -157,7 +157,7 @@ func main() { > **安全警告:** 如果不设置可信代理,攻击者可以伪造 `X-Forwarded-For` 头注入任意 IP,绕过 IP 白名单限流。务必只信任你知道的代理 IP 段。 -### 5. 完整的生產環境啟動範例 +### 5. 完整的生产环境启动规范 ```go func main() { diff --git a/hzh/MS/02-服务治理/API网关/README.md b/hzh/MS/02-服务治理/01-API网关.md similarity index 96% rename from hzh/MS/02-服务治理/API网关/README.md rename to hzh/MS/02-服务治理/01-API网关.md index 8735d63..1c88bcd 100644 --- a/hzh/MS/02-服务治理/API网关/README.md +++ b/hzh/MS/02-服务治理/01-API网关.md @@ -148,6 +148,6 @@ graph TB ## 关联笔记 -- [[02-服务治理/服务发现/README]] — 网关需要订阅服务实例列表 -- [[02-服务治理/流量治理/README]] — 高级路由和灰度发布的延伸 +- [[02-服务治理/04-服务发现]] — 网关需要订阅服务实例列表 +- [[02-服务治理/08-流量治理]] — 高级路由和灰度发布的延伸 - [[hzh/MS/API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型 diff --git a/hzh/MS/02-服务治理/安全机制/README.md b/hzh/MS/02-服务治理/02-安全机制.md similarity index 96% rename from hzh/MS/02-服务治理/安全机制/README.md rename to hzh/MS/02-服务治理/02-安全机制.md index af8aa58..4bc12ea 100644 --- a/hzh/MS/02-服务治理/安全机制/README.md +++ b/hzh/MS/02-服务治理/02-安全机制.md @@ -176,6 +176,6 @@ a1b2c3d4e5f6... ## 关联笔记 -- [[02-服务治理/API网关/README]] — 网关层的统一鉴权和限流 -- [[02-服务治理/服务发现/README]] — 服务注册时也需要安全认证 +- [[02-服务治理/01-API网关]] — 网关层的统一鉴权和限流 +- [[02-服务治理/04-服务发现]] — 服务注册时也需要安全认证 - [[hzh/MS/API 设计原则]] — API 设计中的安全考量 diff --git a/hzh/MS/02-服务治理/分布式追踪/README.md b/hzh/MS/02-服务治理/03-分布式追踪.md similarity index 96% rename from hzh/MS/02-服务治理/分布式追踪/README.md rename to hzh/MS/02-服务治理/03-分布式追踪.md index 97ae778..518ff49 100644 --- a/hzh/MS/02-服务治理/分布式追踪/README.md +++ b/hzh/MS/02-服务治理/03-分布式追踪.md @@ -204,6 +204,6 @@ flowchart LR ## 关联笔记 -- [[02-服务治理/API网关/README]] — API Gateway 可以在入口处注入 trace_id -- [[04-可观测性/链路追踪/README]] — 更详细的链路追踪设计方法论 -- [[02-服务治理/配置管理/README]] — 配置中心的动态刷新可以联动调整采样率 +- [[02-服务治理/01-API网关]] — API Gateway 可以在入口处注入 trace_id +- [[04-可观测性/03-链路追踪]] — 更详细的链路追踪设计方法论 +- [[02-服务治理/07-配置管理]] — 配置中心的动态刷新可以联动调整采样率 diff --git a/hzh/MS/02-服务治理/服务发现/README.md b/hzh/MS/02-服务治理/04-服务发现.md similarity index 96% rename from hzh/MS/02-服务治理/服务发现/README.md rename to hzh/MS/02-服务治理/04-服务发现.md index dda7e13..764d307 100644 --- a/hzh/MS/02-服务治理/服务发现/README.md +++ b/hzh/MS/02-服务治理/04-服务发现.md @@ -200,6 +200,6 @@ sequenceDiagram ## 关联笔记 -- [[02-服务治理/API网关/README]] — API Gateway 是服务发现的用户之一 -- [[02-服务治理/容错模式/README]] — 健康检查是容错体系的基础 -- [[05-部署运维/Kubernetes/README]] — K8s Service 的服务发现原理 +- [[02-服务治理/01-API网关]] — API Gateway 是服务发现的用户之一 +- [[02-服务治理/06-容错模式]] — 健康检查是容错体系的基础 +- [[05-部署运维/02-Kubernetes]] — K8s Service 的服务发现原理 diff --git a/hzh/MS/02-服务治理/服务间通信/README.md b/hzh/MS/02-服务治理/05-服务间通信.md similarity index 96% rename from hzh/MS/02-服务治理/服务间通信/README.md rename to hzh/MS/02-服务治理/05-服务间通信.md index 411db8e..231587d 100644 --- a/hzh/MS/02-服务治理/服务间通信/README.md +++ b/hzh/MS/02-服务治理/05-服务间通信.md @@ -199,6 +199,6 @@ sequenceDiagram ## 关联笔记 -- [[02-服务治理/API网关/README]] — API Gateway 负责协议转换 -- [[03-数据一致性/分布式事务/README]] — 消息投递的一致性保障 -- [[05-部署运维/Kubernetes/README]] — Sidecar 代理透明拦截通信流量 +- [[02-服务治理/01-API网关]] — API Gateway 负责协议转换 +- [[03-数据一致性/02-分布式事务]] — 消息投递的一致性保障 +- [[05-部署运维/02-Kubernetes]] — Sidecar 代理透明拦截通信流量 diff --git a/hzh/MS/02-服务治理/容错模式/README.md b/hzh/MS/02-服务治理/06-容错模式.md similarity index 98% rename from hzh/MS/02-服务治理/容错模式/README.md rename to hzh/MS/02-服务治理/06-容错模式.md index e3afc12..3cb09a6 100644 --- a/hzh/MS/02-服务治理/容错模式/README.md +++ b/hzh/MS/02-服务治理/06-容错模式.md @@ -536,6 +536,6 @@ sequenceDiagram ## 关联笔记 -- [[02-服务治理/API网关/README]] — 网关层的限流和熔断插件 -- [[02-服务治理/服务发现/README]] — 健康检查是服务发现的基础 -- [[04-可观测性/Metrics监控/README]] — 熔断器的指标暴露和告警联动 +- [[02-服务治理/01-API网关]] — 网关层的限流和熔断插件 +- [[02-服务治理/04-服务发现]] — 健康检查是服务发现的基础 +- [[04-可观测性/01-Metrics监控]] — 熔断器的指标暴露和告警联动 diff --git a/hzh/MS/02-服务治理/配置管理/README.md b/hzh/MS/02-服务治理/07-配置管理.md similarity index 96% rename from hzh/MS/02-服务治理/配置管理/README.md rename to hzh/MS/02-服务治理/07-配置管理.md index 468e784..2d4ae4c 100644 --- a/hzh/MS/02-服务治理/配置管理/README.md +++ b/hzh/MS/02-服务治理/07-配置管理.md @@ -169,5 +169,5 @@ flowchart LR ## 关联笔记 -- [[02-服务治理/服务发现/README]] — Nacos 同时提供服务发现和配置管理 -- [[05-部署运维/Kubernetes/README]] — ConfigMap/Secret 是 K8s 的配置注入方式 +- [[02-服务治理/04-服务发现]] — Nacos 同时提供服务发现和配置管理 +- [[05-部署运维/02-Kubernetes]] — ConfigMap/Secret 是 K8s 的配置注入方式 diff --git a/hzh/MS/02-服务治理/流量治理/README.md b/hzh/MS/02-服务治理/08-流量治理.md similarity index 93% rename from hzh/MS/02-服务治理/流量治理/README.md rename to hzh/MS/02-服务治理/08-流量治理.md index 2ccc6e9..425f209 100644 --- a/hzh/MS/02-服务治理/流量治理/README.md +++ b/hzh/MS/02-服务治理/08-流量治理.md @@ -138,9 +138,9 @@ http: | 能力 | 说明 | |------|------| -| **熔断降级** | 下游不可用时自动降级,参见 [[02-服务治理/容错模式/README]] | -| **限流** | 保护上游不因过量请求被打垮,参见 [[02-服务治理/容错模式/README]] | -| **重试** | 对瞬态故障自动恢复,参见 [[02-服务治理/容错模式/README]] | +| **熔断降级** | 下游不可用时自动降级,参见 [[02-服务治理/06-容错模式]] | +| **限流** | 保护上游不因过量请求被打垮,参见 [[02-服务治理/06-容错模式]] | +| **重试** | 对瞬态故障自动恢复,参见 [[02-服务治理/06-容错模式]] | | **黑白名单** | IP 级别访问控制 | | **A/B Testing** | 基于用户分组的长期实验 | @@ -159,6 +159,6 @@ http: ## 关联笔记 -- [[02-服务治理/API网关/README]] — API Gateway 的路由和权重分发 -- [[04-可观测性/Metrics监控/README]] — 灰度期间的监控面板设计 -- [[05-部署运维/SRE实践/README]] — 基于 SLO 的发布决策 +- [[02-服务治理/01-API网关]] — API Gateway 的路由和权重分发 +- [[04-可观测性/01-Metrics监控]] — 灰度期间的监控面板设计 +- [[05-部署运维/04-SRE实践]] — 基于 SLO 的发布决策 diff --git a/hzh/MS/02-服务治理/网关鉴权策略.md b/hzh/MS/02-服务治理/09-网关鉴权策略.md similarity index 100% rename from hzh/MS/02-服务治理/网关鉴权策略.md rename to hzh/MS/02-服务治理/09-网关鉴权策略.md diff --git a/hzh/MS/02-服务治理/README.md b/hzh/MS/02-服务治理/README.md index ffe8e33..33c2a64 100644 --- a/hzh/MS/02-服务治理/README.md +++ b/hzh/MS/02-服务治理/README.md @@ -29,14 +29,14 @@ graph LR | # | 主题 | 核心问题 | |---|------|----------| -| 1 | [[02-服务治理/服务发现/README]] | 服务如何找到彼此?注册中心的工作原理是什么? | -| 2 | [[02-服务治理/API网关/README]] | 统一入口承担哪些职责?网关应该瘦还是胖? | -| 3 | [[02-服务治理/服务间通信/README]] | RPC vs RESTful?同步 vs 异步怎么选? | -| 4 | [[02-服务治理/容错模式/README]] | 网络不可靠,故障怎么隔离和恢复? | -| 5 | [[02-服务治理/配置管理/README]] | 上百个服务的配置如何统一管理? | -| 6 | [[02-服务治理/分布式追踪/README]] | 请求穿越多个服务后,如何追踪链路? | -| 7 | [[02-服务治理/流量治理/README]] | 灰度发布如何做?高级路由策略有哪些? | -| 8 | [[02-服务治理/安全机制/README]] | 服务间调用如何认证和授权?mTLS 是什么? | +| 1 | [[02-服务治理/04-服务发现]] | 服务如何找到彼此?注册中心的工作原理是什么? | +| 2 | [[02-服务治理/01-API网关]] | 统一入口承担哪些职责?网关应该瘦还是胖? | +| 3 | [[02-服务治理/05-服务间通信]] | RPC vs RESTful?同步 vs 异步怎么选? | +| 4 | [[02-服务治理/06-容错模式]] | 网络不可靠,故障怎么隔离和恢复? | +| 5 | [[02-服务治理/07-配置管理]] | 上百个服务的配置如何统一管理? | +| 6 | [[02-服务治理/03-分布式追踪]] | 请求穿越多个服务后,如何追踪链路? | +| 7 | [[02-服务治理/08-流量治理]] | 灰度发布如何做?高级路由策略有哪些? | +| 8 | [[02-服务治理/02-安全机制]] | 服务间调用如何认证和授权?mTLS 是什么? | ### 学习建议 diff --git a/hzh/MS/03-数据一致性/数据库拆分/README.md b/hzh/MS/03-数据一致性/01-数据库拆分.md similarity index 98% rename from hzh/MS/03-数据一致性/数据库拆分/README.md rename to hzh/MS/03-数据一致性/01-数据库拆分.md index a1609e7..089e18a 100644 --- a/hzh/MS/03-数据一致性/数据库拆分/README.md +++ b/hzh/MS/03-数据一致性/01-数据库拆分.md @@ -194,5 +194,5 @@ db/migration/ ## 关联笔记 -- [[03-数据一致性/分布式事务/README]] — 拆分后的数据一致性问题 +- [[03-数据一致性/02-分布式事务]] — 拆分后的数据一致性问题 - [[01-基础概念]] — DDD 限界上下文与数据库拆分的对应关系 diff --git a/hzh/MS/03-数据一致性/分布式事务/README.md b/hzh/MS/03-数据一致性/02-分布式事务.md similarity index 96% rename from hzh/MS/03-数据一致性/分布式事务/README.md rename to hzh/MS/03-数据一致性/02-分布式事务.md index 030c5cb..02297b8 100644 --- a/hzh/MS/03-数据一致性/分布式事务/README.md +++ b/hzh/MS/03-数据一致性/02-分布式事务.md @@ -244,6 +244,6 @@ sequenceDiagram ## 关联笔记 -- [[03-数据一致性/数据库拆分/README]] — 数据库拆分是分布式事务的前提 -- [[02-服务治理/容错模式/README]] — 熔断器和重试在分布式事务中的作用 -- [[02-服务治理/服务间通信/README]] — 消息投递的一致性保障 +- [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提 +- [[02-服务治理/06-容错模式]] — 熔断器和重试在分布式事务中的作用 +- [[02-服务治理/05-服务间通信]] — 消息投递的一致性保障 diff --git a/hzh/MS/03-数据一致性/ID生成/README.md b/hzh/MS/03-数据一致性/03-ID生成.md similarity index 96% rename from hzh/MS/03-数据一致性/ID生成/README.md rename to hzh/MS/03-数据一致性/03-ID生成.md index 076fa0c..0f476e5 100644 --- a/hzh/MS/03-数据一致性/ID生成/README.md +++ b/hzh/MS/03-数据一致性/03-ID生成.md @@ -153,5 +153,5 @@ sequenceDiagram ## 关联笔记 -- [[03-数据一致性/数据库拆分/README]] — 分库分表场景下的 ID 生成 -- [[03-数据一致性/分布式事务/README]] — Outbox 消息 ID 也需要全局唯一 +- [[03-数据一致性/01-数据库拆分]] — 分库分表场景下的 ID 生成 +- [[03-数据一致性/02-分布式事务]] — Outbox 消息 ID 也需要全局唯一 diff --git a/hzh/MS/03-数据一致性/README.md b/hzh/MS/03-数据一致性/README.md index e191e2c..d92ff77 100644 --- a/hzh/MS/03-数据一致性/README.md +++ b/hzh/MS/03-数据一致性/README.md @@ -23,9 +23,9 @@ graph LR | # | 主题 | 核心问题 | |---|------|----------| -| 1 | [[03-数据一致性/数据库拆分/README]] | 每个服务独立 DB,表大了怎么拆分?跨库查询怎么做? | -| 2 | [[03-数据一致性/分布式事务/README]] | 本地事务 + MQ、Saga、TCC,哪个方案最合适? | -| 3 | [[03-数据一致性/ID生成/README]] | 没有自增主键了,全局唯一 ID 怎么生成? | +| 1 | [[03-数据一致性/01-数据库拆分]] | 每个服务独立 DB,表大了怎么拆分?跨库查询怎么做? | +| 2 | [[03-数据一致性/02-分布式事务]] | 本地事务 + MQ、Saga、TCC,哪个方案最合适? | +| 3 | [[03-数据一致性/03-ID生成]] | 没有自增主键了,全局唯一 ID 怎么生成? | ### 学习建议 diff --git a/hzh/MS/04-可观测性/Metrics监控/README.md b/hzh/MS/04-可观测性/01-Metrics监控.md similarity index 95% rename from hzh/MS/04-可观测性/Metrics监控/README.md rename to hzh/MS/04-可观测性/01-Metrics监控.md index 54871b8..3a05ef2 100644 --- a/hzh/MS/04-可观测性/Metrics监控/README.md +++ b/hzh/MS/04-可观测性/01-Metrics监控.md @@ -155,6 +155,6 @@ flowchart TB ## 关联笔记 -- [[04-可观测性/告警管理/README]] — Metrics 是告警的基础数据来源 -- [[04-可观测性/链路追踪/README]] — Tracing 与 Metrics 互补,定位具体故障 -- [[05-部署运维/SRE实践/README]] — SLO 基于 Metrics 数据 +- [[04-可观测性/04-告警管理]] — Metrics 是告警的基础数据来源 +- [[04-可观测性/03-链路追踪]] — Tracing 与 Metrics 互补,定位具体故障 +- [[05-部署运维/04-SRE实践]] — SLO 基于 Metrics 数据 diff --git a/hzh/MS/04-可观测性/日志系统/README.md b/hzh/MS/04-可观测性/02-日志系统.md similarity index 94% rename from hzh/MS/04-可观测性/日志系统/README.md rename to hzh/MS/04-可观测性/02-日志系统.md index 8eb443c..d272d27 100644 --- a/hzh/MS/04-可观测性/日志系统/README.md +++ b/hzh/MS/04-可观测性/02-日志系统.md @@ -144,6 +144,6 @@ DB 慢查询 → 100% 全量采集 → 重点关注 ## 关联笔记 -- [[04-可观测性/Metrics监控/README]] — Metrics + Logging = 完整的问题定位能力 -- [[04-可观测性/链路追踪/README]] — trace_id 是日志与追踪的桥梁 -- [[04-可观测性/告警管理/README]] — 日志可以触发基于模式的告警 +- [[04-可观测性/01-Metrics监控]] — Metrics + Logging = 完整的问题定位能力 +- [[04-可观测性/03-链路追踪]] — trace_id 是日志与追踪的桥梁 +- [[04-可观测性/04-告警管理]] — 日志可以触发基于模式的告警 diff --git a/hzh/MS/04-可观测性/链路追踪/README.md b/hzh/MS/04-可观测性/03-链路追踪.md similarity index 95% rename from hzh/MS/04-可观测性/链路追踪/README.md rename to hzh/MS/04-可观测性/03-链路追踪.md index 5492fec..9aed60b 100644 --- a/hzh/MS/04-可观测性/链路追踪/README.md +++ b/hzh/MS/04-可观测性/03-链路追踪.md @@ -161,6 +161,6 @@ flowchart TD ## 关联笔记 -- [[04-可观测性/Metrics监控/README]] — Tracing 与 Metrics 互补 -- [[04-可观测性/日志系统/README]] — trace_id 串联日志和追踪 -- [[04-可观测性/告警管理/README]] — 基于 Trace 数据的异常检测告警 +- [[04-可观测性/01-Metrics监控]] — Tracing 与 Metrics 互补 +- [[04-可观测性/02-日志系统]] — trace_id 串联日志和追踪 +- [[04-可观测性/04-告警管理]] — 基于 Trace 数据的异常检测告警 diff --git a/hzh/MS/04-可观测性/告警管理/README.md b/hzh/MS/04-可观测性/04-告警管理.md similarity index 96% rename from hzh/MS/04-可观测性/告警管理/README.md rename to hzh/MS/04-可观测性/04-告警管理.md index 56babf8..356c7f4 100644 --- a/hzh/MS/04-可观测性/告警管理/README.md +++ b/hzh/MS/04-可观测性/04-告警管理.md @@ -181,6 +181,6 @@ flowchart LR ## 关联笔记 -- [[04-可观测性/Metrics监控/README]] — Metrics 是告警的数据来源 -- [[05-部署运维/SRE实践/README]] — SLO/Error Budget 的详细方法论 -- [[02-服务治理/容错模式/README]] — 熔断器状态可以作为告警信号 +- [[04-可观测性/01-Metrics监控]] — Metrics 是告警的数据来源 +- [[05-部署运维/04-SRE实践]] — SLO/Error Budget 的详细方法论 +- [[02-服务治理/06-容错模式]] — 熔断器状态可以作为告警信号 diff --git a/hzh/MS/04-可观测性/README.md b/hzh/MS/04-可观测性/README.md index 8c12aef..92d9448 100644 --- a/hzh/MS/04-可观测性/README.md +++ b/hzh/MS/04-可观测性/README.md @@ -25,10 +25,10 @@ graph LR | # | 主题 | 核心问题 | |---|------|----------| -| 1 | [[04-可观测性/Metrics监控/README]] | 怎么量化系统的健康度?RED/USE 方法怎么用? | -| 2 | [[04-可观测性/日志系统/README]] | 日志怎么集中收集?结构化日志的最佳实践是什么? | -| 3 | [[04-可观测性/链路追踪/README]] | trace_id 如何贯穿跨服务调用链?OpenTelemetry 怎么用? | -| 4 | [[04-可观测性/告警管理/README]] | 如何设计告警避免疲劳?SLO/Error Budget 怎么做? | +| 1 | [[04-可观测性/01-Metrics监控]] | 怎么量化系统的健康度?RED/USE 方法怎么用? | +| 2 | [[04-可观测性/02-日志系统]] | 日志怎么集中收集?结构化日志的最佳实践是什么? | +| 3 | [[04-可观测性/03-链路追踪]] | trace_id 如何贯穿跨服务调用链?OpenTelemetry 怎么用? | +| 4 | [[04-可观测性/04-告警管理]] | 如何设计告警避免疲劳?SLO/Error Budget 怎么做? | ### 为什么需要独立的"可观测性"体系? @@ -74,5 +74,5 @@ flowchart LR ### 关联笔记 - [[02-服务治理]] — 熔断器可以触发告警,与监控系统联动 -- [[05-部署运维/Kubernetes/README]] — K8s Liveness/Readiness Probe 是可观测性的基础 +- [[05-部署运维/02-Kubernetes]] — K8s Liveness/Readiness Probe 是可观测性的基础 - [[hzh/MS/README.md]] — 完整微服务知识索引 diff --git a/hzh/MS/05-部署运维/容器化/README.md b/hzh/MS/05-部署运维/01-容器化.md similarity index 95% rename from hzh/MS/05-部署运维/容器化/README.md rename to hzh/MS/05-部署运维/01-容器化.md index f735b7c..e8014d0 100644 --- a/hzh/MS/05-部署运维/容器化/README.md +++ b/hzh/MS/05-部署运维/01-容器化.md @@ -151,5 +151,5 @@ flowchart LR ## 关联笔记 -- [[05-部署运维/Kubernetes/README]] — K8s 以 Pod 为部署单元,镜像来自 Docker -- [[05-部署运维/CI-CD与GitOps/README]] — CI/CD 流水线中的镜像构建环节 +- [[05-部署运维/02-Kubernetes]] — K8s 以 Pod 为部署单元,镜像来自 Docker +- [[05-部署运维/03-CICD与GitOps]] — CI/CD 流水线中的镜像构建环节 diff --git a/hzh/MS/05-部署运维/Kubernetes/README.md b/hzh/MS/05-部署运维/02-Kubernetes.md similarity index 96% rename from hzh/MS/05-部署运维/Kubernetes/README.md rename to hzh/MS/05-部署运维/02-Kubernetes.md index e45b5a0..4e1d4cb 100644 --- a/hzh/MS/05-部署运维/Kubernetes/README.md +++ b/hzh/MS/05-部署运维/02-Kubernetes.md @@ -261,6 +261,6 @@ spec: ## 关联笔记 -- [[02-服务治理/服务发现/README]] — K8s Service 是服务端发现模式的代表 -- [[02-服务治理/流量治理/README]] — Istio VirtualService 在 K8s 上的高级路由 -- [[05-部署运维/SRE实践/README]] — SLO/Error Budget 在 K8s 中的落地 +- [[02-服务治理/04-服务发现]] — K8s Service 是服务端发现模式的代表 +- [[02-服务治理/08-流量治理]] — Istio VirtualService 在 K8s 上的高级路由 +- [[05-部署运维/04-SRE实践]] — SLO/Error Budget 在 K8s 中的落地 diff --git a/hzh/MS/05-部署运维/CI-CD与GitOps/README.md b/hzh/MS/05-部署运维/03-CICD与GitOps.md similarity index 95% rename from hzh/MS/05-部署运维/CI-CD与GitOps/README.md rename to hzh/MS/05-部署运维/03-CICD与GitOps.md index 9f10bd1..7bedec1 100644 --- a/hzh/MS/05-部署运维/CI-CD与GitOps/README.md +++ b/hzh/MS/05-部署运维/03-CICD与GitOps.md @@ -171,6 +171,6 @@ helm upgrade --install order-service ./charts/order-service \ ## 关联笔记 -- [[05-部署运维/容器化/README]] — Docker 镜像构建是 CI/CD 的第一步 -- [[05-部署运维/Kubernetes/README]] — K8s 是部署的目标平台 -- [[05-部署运维/SRE实践/README]] — 错误预算影响发布策略 +- [[05-部署运维/01-容器化]] — Docker 镜像构建是 CI/CD 的第一步 +- [[05-部署运维/02-Kubernetes]] — K8s 是部署的目标平台 +- [[05-部署运维/04-SRE实践]] — 错误预算影响发布策略 diff --git a/hzh/MS/05-部署运维/SRE实践/README.md b/hzh/MS/05-部署运维/04-SRE实践.md similarity index 95% rename from hzh/MS/05-部署运维/SRE实践/README.md rename to hzh/MS/05-部署运维/04-SRE实践.md index 920736f..1f78689 100644 --- a/hzh/MS/05-部署运维/SRE实践/README.md +++ b/hzh/MS/05-部署运维/04-SRE实践.md @@ -155,6 +155,6 @@ groups: ## 关联笔记 -- [[04-可观测性/告警管理/README]] — SLO/Error Budget 与告警体系的联动 -- [[05-部署运维/Kubernetes/README]] — K8s HPA 弹性伸缩支撑 SLO 保障 -- [[05-部署运维/CI-CD与GitOps/README]] — GitOps 支持安全的持续交付 +- [[04-可观测性/04-告警管理]] — SLO/Error Budget 与告警体系的联动 +- [[05-部署运维/02-Kubernetes]] — K8s HPA 弹性伸缩支撑 SLO 保障 +- [[05-部署运维/03-CICD与GitOps]] — GitOps 支持安全的持续交付 diff --git a/hzh/MS/05-部署运维/README.md b/hzh/MS/05-部署运维/README.md index 873aef6..4490150 100644 --- a/hzh/MS/05-部署运维/README.md +++ b/hzh/MS/05-部署运维/README.md @@ -25,10 +25,10 @@ graph LR | # | 主题 | 核心问题 | |---|------|----------| -| 1 | [[05-部署运维/容器化/README]] | Docker 镜像怎么优化?最佳实践有哪些? | -| 2 | [[05-部署运维/Kubernetes/README]] | K8s 核心概念和资源管理怎么做? | -| 3 | [[05-部署运维/CI-CD与GitOps/README]] | 自动化流水线怎么设计?GitOps 流程怎么走? | -| 4 | [[05-部署运维/SRE实践/README]] | SLI/SLO/Error Budget 如何落地? | +| 1 | [[05-部署运维/01-容器化]] | Docker 镜像怎么优化?最佳实践有哪些? | +| 2 | [[05-部署运维/02-Kubernetes]] | K8s 核心概念和资源管理怎么做? | +| 3 | [[05-部署运维/03-CICD与GitOps]] | 自动化流水线怎么设计?GitOps 流程怎么走? | +| 4 | [[05-部署运维/04-SRE实践]] | SLI/SLO/Error Budget 如何落地? | ### 学习建议 diff --git a/hzh/MS/06-gRPC/01-协议与架构.md b/hzh/MS/06-gRPC/01-协议与架构.md index 32ec369..a4be0fa 100644 --- a/hzh/MS/06-gRPC/01-协议与架构.md +++ b/hzh/MS/06-gRPC/01-协议与架构.md @@ -150,12 +150,12 @@ graph LR > > 既然 gRPC 这么多优势,是不是所有场景都应该用 gRPC?什么情况下 HTTP/JSON 仍然更合适? -答案见 [[02-服务治理/服务间通信]]。对外暴露 API 时,HTTP/JSON 仍然不可替代——因为浏览器的 Native Fetch 无法直接调用 gRPC,第三方接入者也不想安装 Proto 编译器。 +答案见 [[02-服务治理/05-服务间通信]]。对外暴露 API 时,HTTP/JSON 仍然不可替代——因为浏览器的 Native Fetch 无法直接调用 gRPC,第三方接入者也不想安装 Proto 编译器。 ## 关联笔记 -- [[02-服务治理/服务间通信]] — gRPC 与 REST 的基础对比及选型建议 -- [[02-服务治理/容错模式/README]] — 基于此架构的重试、熔断等治理机制 +- [[02-服务治理/05-服务间通信]] — gRPC 与 REST 的基础对比及选型建议 +- [[02-服务治理/06-容错模式]] — 基于此架构的重试、熔断等治理机制 - [[02-Proto设计]] — Proto 文件设计的进阶实践 - [[03-RPC模式]] — 四种 RPC 模式的深度用法 - [[04-拦截器]] — 切面编程和上下文传播 diff --git a/hzh/MS/06-gRPC/02-Proto设计.md b/hzh/MS/06-gRPC/02-Proto设计.md index d344104..924127d 100644 --- a/hzh/MS/06-gRPC/02-Proto设计.md +++ b/hzh/MS/06-gRPC/02-Proto设计.md @@ -279,4 +279,4 @@ proto/ - [[01-协议与架构]] — Proto 文件最终服务于协议栈中的 Generated Stub 层 - [[07-最佳实践]] — Buf 代码生成流程和生产配置 -- [[02-服务治理/服务间通信]] — 不同序列化方案的性能对比 +- [[02-服务治理/05-服务间通信]] — 不同序列化方案的性能对比 diff --git a/hzh/MS/06-gRPC/04-拦截器.md b/hzh/MS/06-gRPC/04-拦截器.md index 5f4c12d..0bb67b2 100644 --- a/hzh/MS/06-gRPC/04-拦截器.md +++ b/hzh/MS/06-gRPC/04-拦截器.md @@ -156,7 +156,7 @@ conn, _ := grpc.Dial(target, > [!keypoint] 为什么需要上下文传播 > -> 当一个请求穿越 5 个微服务时,如果没有上下文传播,你在日志系统中看到的是 5 条孤立的记录。有了 W3C Trace Context,每层服务自动将 `traceparent` 透传给下一跳——最终汇聚成一条完整的调用链路,这就是 [[02-服务治理/分布式追踪]] 的核心能力。 +> 当一个请求穿越 5 个微服务时,如果没有上下文传播,你在日志系统中看到的是 5 条孤立的记录。有了 W3C Trace Context,每层服务自动将 `traceparent` 透传给下一跳——最终汇聚成一条完整的调用链路,这就是 [[02-服务治理/03-分布式追踪]] 的核心能力。 ## 拦截器最佳实践 @@ -172,5 +172,5 @@ conn, _ := grpc.Dial(target, - [[01-协议与架构]] — 拦截器位于 gRPC Framework 层,在 HTTP/2 之前处理请求 - [[05-错误处理]] — 拦截器经常需要根据错误码决定重试或降级策略 -- [[02-服务治理/分布式追踪]] — Context 传播是实现分布式追踪的前提 -- [[02-服务治理/安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式 +- [[02-服务治理/03-分布式追踪]] — Context 传播是实现分布式追踪的前提 +- [[02-服务治理/02-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式 diff --git a/hzh/MS/06-gRPC/05-错误处理.md b/hzh/MS/06-gRPC/05-错误处理.md index f355fe0..394dd21 100644 --- a/hzh/MS/06-gRPC/05-错误处理.md +++ b/hzh/MS/06-gRPC/05-错误处理.md @@ -174,5 +174,5 @@ default: - [[04-拦截器]] — 拦截器根据错误码决定是否触发自动重试 - [[07-最佳实践]] — 重试策略中与错误状态的配合配置 -- [[02-服务治理/容错模式]] — 熔断器在收到特定错误码后触发熔断 -- [[02-服务治理/分布式追踪]] — 错误链路在追踪系统中的标注方式 +- [[02-服务治理/06-容错模式]] — 熔断器在收到特定错误码后触发熔断 +- [[02-服务治理/03-分布式追踪]] — 错误链路在追踪系统中的标注方式 diff --git a/hzh/MS/06-gRPC/06-连接管理.md b/hzh/MS/06-gRPC/06-连接管理.md index ca94a1f..9054e8a 100644 --- a/hzh/MS/06-gRPC/06-连接管理.md +++ b/hzh/MS/06-gRPC/06-连接管理.md @@ -151,5 +151,5 @@ Kubernetes 的 DNS 控制器会自动将 Service 的 Endpoints 更新到 DNS 记 - [[01-协议与架构]] — Name Resolver 是架构图中连接管理层的第一环 - [[07-最佳实践]] — Keepalive、负载均衡在生产环境的配合配置 -- [[02-服务治理/服务发现/README]] — 更深度的服务发现机制对比(Nacos / Consul / K8s) -- [[02-服务治理/容错模式/README]] — 负载均衡 + 熔断的组合效果 +- [[02-服务治理/04-服务发现]] — 更深度的服务发现机制对比(Nacos / Consul / K8s) +- [[02-服务治理/06-容错模式]] — 负载均衡 + 熔断的组合效果 diff --git a/hzh/MS/06-gRPC/07-最佳实践.md b/hzh/MS/06-gRPC/07-最佳实践.md index f4726f1..9d29968 100644 --- a/hzh/MS/06-gRPC/07-最佳实践.md +++ b/hzh/MS/06-gRPC/07-最佳实践.md @@ -216,5 +216,5 @@ resp, err := client.GetBigData( - [[04-拦截器]] — 重试策略可以通过 interceptor 实现更复杂的逻辑 - [[05-错误处理]] — 重试策略根据错误状态码来决定是否重试 - [[06-连接管理]] — Keepalive、负载均衡、Name Resolver 的详细配置 -- [[02-服务治理/安全机制]] — mTLS 与服务间身份认证的更深内容 -- [[02-服务治理/容错模式]] — 熔断器、限流器与重试策略的组合配置 +- [[02-服务治理/02-安全机制]] — mTLS 与服务间身份认证的更深内容 +- [[02-服务治理/06-容错模式]] — 熔断器、限流器与重试策略的组合配置 diff --git a/hzh/MS/06-gRPC/README.md b/hzh/MS/06-gRPC/README.md index 4df98a6..1a8bb6f 100644 --- a/hzh/MS/06-gRPC/README.md +++ b/hzh/MS/06-gRPC/README.md @@ -7,7 +7,7 @@ create time: 2026-05-07 16:30 ## 概述 -本目录系统整理 **gRPC** 的核心知识点,从底层协议到生产实践,由浅入深覆盖 gRPC 的每一个关键领域。与 [[02-服务治理/服务间通信]] 中的入门对比不同,这里聚焦于「用了 gRPC 之后」—— 如何设计 Proto、如何编写拦截器、如何处理流式调用、连接怎么管、出错怎么查。 +本目录系统整理 **gRPC** 的核心知识点,从底层协议到生产实践,由浅入深覆盖 gRPC 的每一个关键领域。与 [[02-服务治理/05-服务间通信]] 中的入门对比不同,这里聚焦于「用了 gRPC 之后」—— 如何设计 Proto、如何编写拦截器、如何处理流式调用、连接怎么管、出错怎么查。 ```mermaid graph LR @@ -126,8 +126,8 @@ graph LR ## 关联笔记 -- [[02-服务治理/服务间通信]] — gRPC 与 REST 的基础对比及混合通信模式 -- [[02-服务治理/服务发现/README]] — Nacos / Consul / K8s Service 深度对比 -- [[02-服务治理/容错模式/README]] — 重试、熔断、限流、降级的完整治理 -- [[02-服务治理/分布式追踪/README]] — OpenTelemetry 链路追踪 -- [[02-服务治理/安全机制/README]] — mTLS、JWT、RBAC +- [[02-服务治理/05-服务间通信]] — gRPC 与 REST 的基础对比及混合通信模式 +- [[02-服务治理/04-服务发现]] — Nacos / Consul / K8s Service 深度对比 +- [[02-服务治理/06-容错模式]] — 重试、熔断、限流、降级的完整治理 +- [[02-服务治理/03-分布式追踪]] — OpenTelemetry 链路追踪 +- [[02-服务治理/02-安全机制]] — mTLS、JWT、RBAC diff --git a/hzh/MS/README.md b/hzh/MS/README.md index c4c3155..0c4853a 100644 --- a/hzh/MS/README.md +++ b/hzh/MS/README.md @@ -39,40 +39,40 @@ graph LR | 子主题 | 核心内容 | |--------|---------| -| [[02-服务治理/服务发现/README]] | Nacos / Consul / K8s Service,客户端 vs 服务端发现模式 | -| [[02-服务治理/API网关/README]] | 统一入口的职责边界、路由策略、插件体系 | -| [[02-服务治理/服务间通信/README]] | gRPC vs REST,同步 vs 异步组合拳 | -| [[02-服务治理/容错模式/README]] | 超时 / 重试 / 熔断 / 限流 / 舱壁隔离 / 降级 | -| [[02-服务治理/配置管理/README]] | Nacos Config / Apollo,动态刷新与版本管理 | -| [[02-服务治理/分布式追踪/README]] | OpenTelemetry,trace/span 概念,采样策略 | -| [[02-服务治理/流量治理/README]] | 灰度发布策略,蓝绿 vs 金丝雀,Istio VirtualService | -| [[02-服务治理/安全机制/README]] | mTLS, JWT, RBAC/ABAC, 输入防护 | +| [[02-服务治理/04-服务发现]] | Nacos / Consul / K8s Service,客户端 vs 服务端发现模式 | +| [[02-服务治理/01-API网关]] | 统一入口的职责边界、路由策略、插件体系 | +| [[02-服务治理/05-服务间通信]] | gRPC vs REST,同步 vs 异步组合拳 | +| [[02-服务治理/06-容错模式]] | 超时 / 重试 / 熔断 / 限流 / 舱壁隔离 / 降级 | +| [[02-服务治理/07-配置管理]] | Nacos Config / Apollo,动态刷新与版本管理 | +| [[02-服务治理/03-分布式追踪]] | OpenTelemetry,trace/span 概念,采样策略 | +| [[02-服务治理/08-流量治理]] | 灰度发布策略,蓝绿 vs 金丝雀,Istio VirtualService | +| [[02-服务治理/02-安全机制]] | mTLS, JWT, RBAC/ABAC, 输入防护 | ### 3. [[03-数据一致性]] — 跨服务数据一致性怎么保证? | 子主题 | 核心内容 | |--------|---------| -| [[03-数据一致性/数据库拆分/README]] | 垂直拆分 / 水平分片,ShardingSphere,跨库查询方案 | -| [[03-数据一致性/分布式事务/README]] | Outbox 模式,Saga,TCC,AT 模式,幂等设计 | -| [[03-数据一致性/ID生成/README]] | Snowflake, UUID v7, 号段模式 | +| [[03-数据一致性/01-数据库拆分]] | 垂直拆分 / 水平分片,ShardingSphere,跨库查询方案 | +| [[03-数据一致性/02-分布式事务]] | Outbox 模式,Saga,TCC,AT 模式,幂等设计 | +| [[03-数据一致性/03-ID生成]] | Snowflake, UUID v7, 号段模式 | ### 4. [[04-可观测性]] — 请求穿越多个服务后如何排查问题? | 子主题 | 核心内容 | |--------|---------| -| [[04-可观测性/Metrics监控/README]] | Prometheus, RED/USE 方法, Counter/Gauge/Histogram | -| [[04-可观测性/日志系统/README]] | JSON 结构化日志, ELK vs Loki, 采集管线, 采样策略 | -| [[04-可观测性/链路追踪/README]] | OpenTelemetry, W3C Trace Context, 上下文传播, 采样 | -| [[04-可观测性/告警管理/README]] | SLO/Error Budget 告警, 分级降噪, On-Call 最佳实践 | +| [[04-可观测性/01-Metrics监控]] | Prometheus, RED/USE 方法, Counter/Gauge/Histogram | +| [[04-可观测性/02-日志系统]] | JSON 结构化日志, ELK vs Loki, 采集管线, 采样策略 | +| [[04-可观测性/03-链路追踪]] | OpenTelemetry, W3C Trace Context, 上下文传播, 采样 | +| [[04-可观测性/04-告警管理]] | SLO/Error Budget 告警, 分级降噪, On-Call 最佳实践 | ### 5. [[05-部署运维]] — 上百个服务如何容器化编排与发布? | 子主题 | 核心内容 | |--------|---------| -| [[05-部署运维/容器化/README]] | Dockerfile 最佳实践, 镜像安全, distroless | -| [[05-部署运维/Kubernetes/README]] | Pod/Deployment/Service/Ingress, Probe, HPA, 资源配置 | -| [[05-部署运维/CI-CD与GitOps/README]] | GitHub Actions Pipeline, GitOps (ArgoCD), Helm | -| [[05-部署运维/SRE实践/README]] | SLI/SLO/SLA, Error Budget, Blameless Postmortem, DORA Metrics | +| [[05-部署运维/01-容器化]] | Dockerfile 最佳实践, 镜像安全, distroless | +| [[05-部署运维/02-Kubernetes]] | Pod/Deployment/Service/Ingress, Probe, HPA, 资源配置 | +| [[05-部署运维/03-CICD与GitOps]] | GitHub Actions Pipeline, GitOps (ArgoCD), Helm | +| [[05-部署运维/04-SRE实践]] | SLI/SLO/SLA, Error Budget, Blameless Postmortem, DORA Metrics | ## 学习建议