# mianshiti **Repository Path**: loticket/mianshiti ## Basic Information - **Project Name**: mianshiti - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-25 - **Last Updated**: 2026-10-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Golang + MySQL + Redis 最全面面试题库 > 整理自一线后端面试高频考点,覆盖原理、底层实现、场景题与实战题。每个主题按「核心高频 → 深入原理 → 场景实战 → 高频易错」组织。 > 建议配合代码动手验证,面试时能讲出底层原理 + 场景取舍 = 加分项。 --- # 第一部分:Golang ## 一、基础语法与数据类型 ### 1. 变量与常量 **Q1:Go 中变量声明的几种方式?区别是什么?** - `var a int = 10`:完整声明 - `var a = 10`:类型推断 - `a := 10`:短变量声明(只能在函数内部使用,必须至少一个新变量) - `const` 声明常量,常量必须是编译期可确定的值 - 多个变量:`var a, b, c = 1, 2, 3` 或 `a, b := 1, 2` **Q2:`:=` 和 `=` 的区别?** - `:=` 是声明并赋值(short variable declaration),至少声明一个新变量 - `=` 是纯赋值,变量必须已声明 - 混合场景:`a, b := 1, 2` 中 a 是新的;`a, b := 2, 3` 中若 a 已声明则只新建 b **Q3:`iota` 的用法?** - 在 const 块中,iota 从 0 开始,每行递增 1 ```go const ( A = iota // 0 B // 1 C // 2 ) ``` ### 2. 基本类型 **Q4:Go 的基本数据类型有哪些?** - 布尔:`bool` - 数值:`int/int8/int16/int32/int64`、`uint`、`float32/float64`、`complex64/complex128` - 字符串:`string` - 派生:数组、切片、map、结构体、接口、函数、通道、指针 **Q5:`int` 和 `int64` 的区别?** - `int` 在 32 位系统占 4 字节,64 位系统占 8 字节(平台相关) - `int64` 固定 8 字节 - 跨平台程序注意:`int` 在不同平台长度不同 **Q6:`string` 的底层结构是什么?字符串可以修改吗?** - 底层是 `struct { str unsafe.Pointer; len int }`,即指向字节数组的指针 + 长度 - 字符串不可变,修改会 panic(编译期报错) - 转字节数组 `[]byte(s)` 会触发拷贝 - 拼接使用 `+` 或 `strings.Builder` **Q7:`rune` 和 `byte` 的区别?** - `byte` = `uint8`,表示一个 ASCII 字符/一个字节 - `rune` = `int32`,表示一个 Unicode 码点,一个汉字占一个 rune - 遍历中文字符串要用 `for range`,它会按 rune 遍历 ```go s := "你好" fmt.Println(len(s)) // 6,字节长度 for _, r := range s { fmt.Printf("%c", r) // 你好 } ``` ### 3. 数组与切片 **Q8:数组和切片的区别?** - 数组:长度固定,是值类型,作为参数传递会整体拷贝 - 切片:长度可变,是引用类型(底层指针+长度+容量),作为参数传递的是切片头,修改底层数组会相互影响 **Q9:切片的底层结构?`append` 扩容机制?** - 切片底层:`type slice struct { array unsafe.Pointer; len int; cap int }` - `append` 扩容规则: - 新容量小于 1024:容量翻倍 - 新容量大于等于 1024:按 1.25 倍(原 1.25)增长(实际为 `newcap = oldcap + oldcap/4`) - 扩容后会开辟新数组并拷贝,旧数组交由 GC 回收 - 扩容是针对 cap 的整数倍,具体倍数因元素大小有细微差异 **Q10:`make` 和 `new` 的区别?** - `new(T)`:分配内存,返回 `*T` 指针,零值填充 - `make(T, ...)`:只用于 slice、map、channel,返回初始化后的引用类型本身 - new 用于值类型(int、struct 等),make 用于引用类型 **Q11:切片作为参数传递时,函数内 append 为什么外层的长度没变?** - 切片头按值传递(拷贝了指针、len、cap),函数内 append 修改的是副本的 len - 若没触发扩容,底层数组共享,元素修改会影响外部;但外部 len 不变 - 若触发扩容,函数内指向新数组,外部不受影响 - 正确做法:返回新切片或传入 `*[]T` **Q12:数组是值类型,那么 `[3]int` 和 `[4]int` 是不同的类型吗?** - 是,数组长度是类型的一部分,`[3]int` 和 `[4]int` 是不同类型,不能相互赋值 **Q13:切片之间能直接比较吗?** - 切片只能用 `== nil` 判断是否为 nil,不能直接比较内容 - 可用 `reflect.DeepEqual` 或 `slices.Equal`(Go 1.21) ### 4. Map **Q14:map 的底层结构?** - Go 1.6+ 基于哈希表实现 - 底层:`hmap` 包含 `buckets`(桶数组,默认 8 个 kv 对/桶),每个桶是一个 `bmap` - 桶满了通过 overflow 指针链到下一个桶 - 查找:先算 hash,取低 8 位定位桶,高 8 位比对 tophash **Q15:map 是线程安全的吗?如何保证并发安全?** - 不是。并发读写 map 会 panic("fatal error: concurrent map read and map write") - 解决方案: 1. `sync.Mutex` / `sync.RWMutex` 手动加锁 2. `sync.Map`(适合读多写少的场景,读用原子操作) 3. 分片锁(sharding) **Q16:map 为什么遍历是无序的?** - 遍历时随机从某个桶开始,并随机偏移,目的是防止程序员依赖遍历顺序 - 需要有序需自行排序 key **Q17:map 的 key 可以是什么类型?** - 必须是可比较类型(== 和 != 可用):基本类型、指针、channel、接口、结构体(成员都可比较) - 不能是 slice、map、func(不可比较) **Q18:如何删除 map 中的元素?`delete` 之后内存会释放吗?** - `delete(m, key)` - 只删除逻辑,不释放底层 bucket 内存(桶数组保留),内存不会立即归还,扩容收缩机制:删除元素超过一定阈值才触发缩容 **Q19:map 的扩容机制?** - 触发条件:装载因子(元素数/桶数)超过 6.5,或溢出桶过多 - 扩容方式:等量扩容(rehash,整理溢出桶)和翻倍扩容(数据重新分布) - 采用渐进式搬迁,不一次性完成,避免阻塞 ### 5. 函数、闭包、defer **Q20:Go 是值传递还是引用传递?** - Go 只有值传递。所谓"引用传递"是传递了指针的副本(引用类型本身是结构,含指针) - 结构体传参默认值拷贝;传指针则修改会影响原对象 **Q21:defer 的执行顺序和时机?** - LIFO(后进先出),在函数返回前执行 - 参数在 defer 声明时求值,函数体在函数返回后求值 - defer 常用于资源释放、recover ```go func main() { defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") // 输出 3 2 1 } ``` **Q22:defer + return 的执行顺序?defer 能修改返回值吗?** - 顺序:先执行 return 语句(为返回值赋值),再执行 defer,最后函数返回 - 有名返回值(`func f() (n int)`)时,defer 可以修改返回值 ```go func f() (n int) { defer func() { n++ }() return 1 // 最终返回 2 } ``` **Q23:闭包是什么?有什么坑?** - 闭包 = 函数 + 捕获的外部变量(引用捕获) - 经典坑:for 循环 + 闭包变量引用 ```go // 错误:全部输出 3(Go 1.21 之前) for i := 0; i < 3; i++ { defer func() { fmt.Println(i) }() } // 解决:Go 1.22 已修复(每次循环独立变量);或传参 for i := 0; i < 3; i++ { defer func(i int) { fmt.Println(i) }(i) } ``` **Q24:`defer`、`recover`、`panic` 的配合?recover 只能在哪里生效?** - `recover` 只能在 `defer` 函数中生效(defer 中调用),用于捕获 panic - recover 恢复后程序继续执行(从 defer 之后的调用处) - 未 recover 的 panic 会导致整个进程崩溃 ```go func safeCall() { defer func() { if r := recover(); r != nil { fmt.Println("recovered:", r) } }() panic("boom") } ``` **Q25:`init()` 函数?多个文件的执行顺序?** - 每个包可以有多个 init,执行顺序:包级变量初始化 → init 函数(按文件编译顺序) - 多个包:先执行依赖包的 init,再执行当前包 - main 包 init 先于 main 函数执行 - init 不能被显式调用 **Q26:Go 函数可以返回多个值吗?** - 可以,常用于返回 `(result, error)` 模式 ### 6. 结构体、方法、接口 **Q27:结构体是值类型还是引用类型?什么时候需要传指针?** - 值类型。传递结构体会拷贝 - 需要修改原对象、结构体较大(避免拷贝开销)、实现接口方法时通常用指针接收者 **Q28:值接收者和指针接收者的区别?** - 值接收者:调用不修改原对象,方法作用于副本 - 指针接收者:方法可修改原对象 - 接口实现判断:值接收者的方法,值类型和指针类型都能调用;指针接收者的方法,只有指针类型能作为接口实现 - 一个对象同时有值方法和指针方法时,通常只用指针类型调用避免歧义 **Q29:Go 接口的底层结构?空接口 `interface{}` 和 `any`?** - 接口包含类型信息 `type` 和数据指针 `data` - 空接口 `interface{}` = `any`(Go 1.18 起),可以容纳任何类型 - 接口的动态类型、动态值;接口判等要注意 nil 陷阱 **Q30:接口判 nil 的坑?** ```go var p *int = nil var i interface{} = p if i == nil { // false!接口不为 nil } ``` - 接口为 nil 当且仅当 type 和 value 都为 nil;把 nil 指针赋给接口后,接口的 type 不为 nil,接口不为 nil **Q31:类型断言 `i.(T)` 和类型切换 `switch v := i.(type)`?** - 类型断言:`v, ok := i.(T)`,ok 表示是否成功 - 类型切换:`switch i.(type) { case int: ... }` **Q32:struct 的 `==` 比较?** - 所有字段都可比较时,结构体可以 `==` - 含 slice/map/func 字段的结构体不可直接比较 **Q33:空结构体 `struct{}` 的作用?** - 不占内存(0 字节) - 常用于:`chan struct{}` 作为信号,map 作为 set(`map[string]struct{}`) **Q34:结构体方法重载?** - Go 不支持方法重载,同名方法只能有一个 --- ## 二、并发编程(重点) ### 35. goroutine **Q35:什么是 goroutine?和线程的区别?** - goroutine 是 Go 的轻量级并发单位,栈初始 2KB(可动态增长),创建成本低 - 由 Go runtime 调度,M:N 调度(多个 goroutine 映射到少数 OS 线程) - 线程由操作系统调度,栈默认 1MB+,创建和切换成本高 **Q36:GMP 调度模型?** - G:goroutine - M:OS 线程(machine),真正执行 - P:Processor,本地队列(存放可运行的 G),默认数量 = CPU 核数(`GOMAXPROCS`) - 调度流程:M 从 P 的本地队列取 G 执行;本地队列空则去全局队列取;全局也空则从其他 P 窃取(work stealing) - M 数量默认上限 10000,P 数量 = GOMAXPROCS - 阻塞系统调用时 M 会释放 P 并解耦,调用结束后找新 P 或重新进入 **Q37:`GOMAXPROCS` 的作用?** - 控制并发执行 goroutine 的 P 数量,默认 = CPU 核数 - 可以通过 `runtime.GOMAXPROCS(n)` 修改 **Q38:goroutine 泄漏怎么排查?** - 场景:goroutine 阻塞在 channel 上没人发送/接收、忘记 close、死循环 - 排查:`runtime.NumGoroutine()` 看数量,pprof 看 goroutine 栈 - 防止:context 超时、select + timeout、合理设计 channel 关闭时机 ### 36. Channel **Q39:channel 的底层结构和分类?** - 底层:`hchan`,包含环形缓冲区、发送/接收等待队列、锁 - 有缓冲:`make(chan T, n)`;无缓冲:`make(chan T)` - 无缓冲 channel:发送和接收必须同步配对,否则阻塞 **Q40:无缓冲和有缓冲 channel 的区别?** - 无缓冲:发送者必须等待接收者,同步通信 - 有缓冲:发送者写入缓冲区,缓冲区满才阻塞;接收者从缓冲区读,空才阻塞 **Q41:向已关闭的 channel 发送/接收会发生什么?** - 发送:panic("send on closed channel") - 接收:返回零值,第二个返回值(ok)为 false,表示 channel 已关闭且无数据 ```go v, ok := <-ch if !ok { // 已关闭 } ``` **Q42:channel 什么时候会被 GC?关闭 nil channel 会怎样?** - channel 不再被引用时会被 GC,但不推荐依赖 - 向 nil channel 发送/接收都会永久阻塞 - 关闭 nil channel 会 panic **Q43:`select` 的使用?select 是随机还是顺序?** - select 用于多路复用,多个 case 同时就绪时随机选择一个执行 - 没有就绪的 case 时,若有 default 则执行 default,否则阻塞 - select 可以处理 channel 关闭:配合 `for { select { case v, ok := <-ch } }` **Q44:如何优雅关闭 channel?如何避免重复关闭?** - 原则:只在发送方关闭 channel;不要从接收方关闭 - 避免重复关闭/向关闭的 channel 发送:用 `sync.Once` 或 context - 模式:`for v := range ch` 自动处理关闭 **Q45:channel 和 goroutine 结合的场景题:两个 goroutine 交替打印 1-100?** ```go func main() { ch1, ch2 := make(chan int, 1), make(chan int) ch1 <- 1 done := make(chan struct{}) go func() { for i := 1; i <= 50; i++ { <-ch1 fmt.Println("A:", i*2-1) ch2 <- 1 } }() go func() { for i := 1; i <= 50; i++ { <-ch2 fmt.Println("B:", i*2) if i < 50 { ch1 <- 1 } } done <- struct{}{} }() <-done } ``` ### 37. sync 包 **Q46:Mutex 的底层实现?什么是公平锁?** - Mutex 基于 CAS + 自旋 + 信号量实现 - 状态:`state` 字段的低 1 位表示锁是否被持有,高 31 位记录等待 goroutine 数 - 普通模式:新 goroutine 抢锁可能抢赢等待队列里的(饥饿) - 饥饿模式:等待超过 1ms 自动进入饥饿模式,新来的 goroutine 直接进队列尾部排队,保证公平 - Go 1.9+ 提供 `sync.Mutex`(默认)和饥饿模式切换 **Q47:`sync.RWMutex` 读锁写锁的区别?** - 读锁可多个 goroutine 同时持有;写锁独占 - 写锁优先级:写锁等待时,新到的读锁会被阻塞(防止写饥饿) **Q48:`sync.WaitGroup` 的用法和坑?** - `Add(delta)` / `Done()` / `Wait()` - 坑:Add 必须在 Wait 之前完成计数;Done 次数不能超过 Add;WaitGroup 不可拷贝(会 panic) **Q49:`sync.Once` 的作用?** - 保证函数只执行一次,即使并发调用 - 底层:原子标志位 + Mutex,用于单例初始化 **Q50:`sync.Map` 和普通 map + 锁的对比?适用场景?** - sync.Map 适合:读多写少、key 固定、并发访问 - 底层:双 map(read 用原子读,dirty 用锁),读操作无锁 - 写多读少、key 频繁变化的场景,用普通 map + Mutex 更好 **Q51:`sync.Cond` 的用途?** - 条件变量,用于 goroutine 等待某个条件成立 - `Wait()`(自动释放锁并阻塞)、`Signal()`(唤醒一个)、`Broadcast()`(唤醒所有) **Q52:`atomic` 包的用途?CAS 是什么?** - 原子操作,底层依赖 CPU 指令(如 LOCK CMPXCHG),无锁 - CAS:Compare And Swap,`atomic.CompareAndSwapInt64` - 适用:简单的计数、标志位;复杂逻辑用 Mutex **Q53:CAS 的 ABA 问题?Go 中如何解决?** - ABA:值从 A 变 B 再变回 A,CAS 误判未改变 - 解决:加版本号/时间戳(如 `atomic.Value` 存储带版本的结构体) ### 38. Context **Q54:Context 的作用?有哪几种?** - 用于传递取消信号、超时控制、携带值 - `context.Background()`:根 Context - `context.TODO()`:占位 - `context.WithCancel`:取消 - `context.WithTimeout` / `context.WithDeadline`:超时 - `context.WithValue`:携带值 - 原则:Context 作为函数第一个参数传递,向下传递取消信号 **Q55:如何避免 context 泄漏?** - `WithCancel` 创建的 cancel 必须被调用(defer cancel()) - 超时 Context 也要 defer cancel 释放资源 **Q56:context 传值有什么限制?** - key 最好是自定义类型(避免 string 类型碰撞) - 值不可变 ### 39. 并发经典场景题 **Q57:如何用 goroutine 实现并发求和?** ```go func sum(arr []int) int { var wg sync.WaitGroup var mu sync.Mutex total := 0 chunk := len(arr) / 4 for i := 0; i < 4; i++ { wg.Add(1) go func(start, end int) { defer wg.Done() s := 0 for _, v := range arr[start:end] { s += v } mu.Lock() total += s mu.Unlock() }(i*chunk, (i+1)*chunk) } wg.Wait() return total } ``` **Q58:什么是竞态条件(data race)?如何检测?** - 多个 goroutine 同时读写同一变量,结果不确定 - 检测:`go run -race` / `go test -race` - 解决:加锁、原子操作、channel、只读共享 **Q59:实现一个只运行一次的单例(双重检查锁)?** ```go type singleton struct{} var instance *singleton var mu sync.Mutex var once sync.Once func GetInstance() *singleton { once.Do(func() { instance = &singleton{} }) return instance } ``` **Q60:生产者消费者模型用 channel 实现?** ```go func main() { ch := make(chan int, 10) done := make(chan struct{}) // 生产者 go func() { for i := 0; i < 20; i++ { ch <- i } close(ch) }() // 消费者 go func() { for v := range ch { fmt.Println(v) } done <- struct{}{} }() <-done } ``` **Q61:控制并发数(信号量/令牌桶)怎么实现?** - 用带缓冲 channel 控制: ```go sem := make(chan struct{}, 10) // 最多 10 个并发 for _, task := range tasks { sem <- struct{}{} go func(t Task) { defer func() { <-sem }() do(t) }(task) } ``` **Q62:`for range` + goroutine 闭包捕获问题?** - Go 1.22 之前,range 变量复用导致 goroutine 捕获同一变量;Go 1.22 起每次迭代独立变量 - 兼容写法:`go func(v := v) {...}` 传参 --- ## 三、内存与性能 ### 40. 内存管理与 GC **Q63:Go 的垃圾回收原理?(三色标记)** - 并发标记清除(CMS),基于三色标记法 - 白:未被扫描;灰:被标记但未扫描其引用;黑:已标记已扫描 - 根对象(全局变量、栈、寄存器)为灰色起点 - 标记过程并发进行,通过写屏障保证正确性(插入写屏障+删除写屏障) - 清除阶段并发回收 **Q64:Go GC 的触发时机?** - 内存分配达到阈值(当前堆大小的目标,默认 `GOGC=100`,堆增长 100% 时触发) - 主动 `runtime.GC()` - 定时/周期性后台触发 **Q65:Go 1.5+ 的 GC 有什么优化?** - 从 1.5 的 stop-the-world 到并发标记 - 引入 write barrier,标记与业务并发 - `GOGC` 可调,`GODEBUG=gctrace=1` 查看 GC 日志 - Go 1.19+ 引入软内存限制 `GOMEMLIMIT` **Q66:什么是逃逸分析?什么情况会逃逸?** - 编译器分析变量是否逃出函数作用域 - 逃逸场景:返回局部变量指针、把变量赋给全局、接口动态类型(可能逃逸)、闭包捕获、变量地址传给其他函数 - 逃逸到堆上则由 GC 管理;栈上分配则无需 GC - 用 `go build -gcflags="-m"` 查看逃逸分析结果 - 影响:频繁分配导致 GC 压力,可考虑对象池 `sync.Pool` **Q67:`sync.Pool` 的用途和原理?** - 对象池,减少 GC 压力,复用临时对象 - 底层:每个 P 有私有对象 + 共享池(无锁优先),GC 时会清空 pool - 适合:创建成本高、可复用的对象(如 bytes.Buffer、数据库连接包装对象) - 不适合:持久状态(连接、socket)——pool 会被 GC 清空 **Q68:内存对齐?** - 结构体字段按对齐规则排列,`unsafe.Sizeof` 计算大小 - 字段顺序影响结构体大小,合理排序可节省内存 - 空结构体、对齐到最宽成员 ### 41. 反射与泛型 **Q69:反射的原理?`reflect` 的使用?** - 通过接口的 type 和 value 动态操作对象 - `reflect.TypeOf(x)` / `reflect.ValueOf(x)` - 性能低于直接调用,避免在热路径频繁使用 **Q70:泛型的基本用法?(Go 1.18+)** ```go func Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b } ``` - 类型参数 `[T any]`,类型约束(`constraints.Ordered`、自定义 interface) - 泛型函数、泛型类型、泛型方法(方法不能有类型参数) - 泛型 vs interface{}:泛型编译期确定类型,性能更好、类型安全 ### 42. 错误处理 **Q71:Go 的错误处理方式?error 接口?** - `type error interface { Error() string }` - 函数返回 `(result, error)`,调用方判断 - `fmt.Errorf("...: %w", err)` 用 `%w` 包装错误 - `errors.Is`(判断错误链中是否有目标)、`errors.As`(断言错误类型) - Go 1.13+ 错误链 **Q72:panic 和 error 的使用场景?** - error:预期内、可恢复的错误 - panic:程序 bug、不可恢复错误(如索引越界);业务逻辑一般不主动 panic - 可通过 recover 在顶层兜底防止崩溃 ### 43. 标准库与常用工具 **Q73:`fmt` 的格式化占位符?** - `%v` 默认、`%+v` 带字段名、`%#v` Go 语法、`%T` 类型、`%d`、`%s`、`%f`、`%x` **Q74:如何实现优雅退出(信号处理)?** ```go ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() <-ctx.Done() ``` **Q75:字符串拼接性能对比?** - `+` 拼接:每 + 一次分配新字符串,O(n²),适合少量 - `strings.Builder`:内部 bytes.Buffer 动态扩容,适合大量拼接 - `strings.Join`:适合切片拼接 **Q76:JSON 序列化:`encoding/json` 的注意事项?** - 结构体 tag:`json:"name,omitempty"` - 大结构体、频繁序列化性能瓶颈可用 `jsoniter`、`easyjson` - 注意字段导出(大写开头才能序列化) **Q77:如何实现定时任务/超时控制?** - `time.Ticker`(周期)、`time.Timer`(一次性) - `time.After`(channel 超时) - `select` + `time.After` 做超时 **Q78:`context` 和 `time.After` 超时选择的区别?** - `time.After` 每次调用创建一个 Timer,超时后需注意是否泄漏(未到期的 After 的 timer 会一直持有直到到期) - context 更可控,能传递取消信号 ### 44. 测试与性能分析 **Q79:Go 单元测试怎么写?表驱动测试?** - `_test.go` 文件,`func TestXxx(t *testing.T)` - 表驱动:`tests := []struct{...}{...}` 遍历执行 - `go test` / `go test -v` **Q80:基准测试 benchmark?** - `func BenchmarkXxx(b *testing.B)` - `go test -bench=. -benchmem` - 查看内存分配 **Q81:pprof 性能分析?** - 引入 `net/http/pprof`,访问 `/debug/pprof/` - 看 CPU:`go tool pprof` 分析 profile - 关注 goroutine 数量、堆内存、CPU 热点 ### 45. 工程实践与框架 **Q82:Gin 框架的特点?中间件机制?** - 高性能 HTTP Web 框架,基于 httprouter - 中间件:请求前/后执行,`c.Next()` 控制流程 - 支持路由分组、参数绑定、JSON 校验 **Q83:如何实现一个简单的中间件(洋葱模型)?** ```go func Logger() gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() fmt.Println("cost:", time.Since(start)) } } ``` **Q84:gRPC 和 REST 的区别?** - gRPC:HTTP/2、protobuf 二进制、强类型接口、支持流式,适合内部服务 - REST:HTTP/1.1、JSON、通用性高 **Q85:Go 微服务的服务发现、负载均衡常见方案?** - 服务注册发现:etcd、consul、nacos - 负载均衡:客户端(gRPC LB)、服务端(Nginx)、注册中心集成 - RPC 框架:gRPC、go-micro、Kitex、Dubbo-go **Q86:`select` 在数据库/网络 IO 中的应用?(结合连接池)** - 数据库连接池、网络连接池常用 - 获取连接:select + context.Done + 连接获取 **Q87:如何避免 goroutine 泄漏的实践?** - 使用 context 传递取消 - channel 有明确的关闭方 - worker pool + WaitGroup - 工具:pprof、go.uber.org/goleak 检测 --- # 第二部分:MySQL ## 一、存储引擎与架构 ### 88. 存储引擎 **Q88:InnoDB 和 MyISAM 的区别?** - InnoDB:支持事务、行级锁、外键,聚簇索引,支持崩溃恢复(redo log),默认引擎 - MyISAM:不支持事务、表级锁、只支持非聚簇索引,查询快但并发差,无崩溃恢复 - 现在基本都推荐 InnoDB **Q89:InnoDB 和 MyISAM 索引结构区别?** - InnoDB 用 B+ 树聚簇索引(主键索引叶子存整行数据) - MyISAM 索引文件和数据文件分离(非聚簇),索引叶子存数据地址 **Q90:InnoDB 为什么用 B+ 树而不用 B 树/红黑树/Hash?** - B+ 树:叶子节点存数据且用链表连接,范围查询友好;中间节点只存 key,相同页能存更多索引,减少 IO - Hash:等值查询 O(1),但不支持范围查询、无顺序 - B 树:数据分布在各节点,范围查询需要回溯 - 红黑树:树太高,磁盘 IO 次数多 - 数据库落盘场景,减少磁盘 IO 是核心 **Q91:为什么主键推荐自增整数而不是 UUID?** - 自增主键插入时追加到 B+ 树末尾,避免页分裂 - UUID 无序,插入频繁导致页分裂、索引碎片、随机 IO ### 89. 事务 **Q92:事务的四大特性 ACID?** - 原子性(Atomicity):全部成功或全部回滚——靠 undo log - 一致性(Consistency):事务前后数据一致——应用层+数据库保证 - 隔离性(Isolation):事务之间隔离——靠锁+MVCC - 持久性(Durability):提交后永久保存——靠 redo log **Q93:MySQL 的四种隔离级别?各解决什么问题?** 1. 读未提交(Read Uncommitted):允许读未提交数据,产生脏读 2. 读已提交(Read Committed):解决脏读,产生不可重复读(每次读最新已提交) 3. 可重复读(Repeatable Read):解决不可重复读,产生幻读;InnoDB 默认级别,通过 MVCC+间隙锁可避免幻读 4. 串行化(Serializable):解决幻读,全部加锁,性能最差 **Q94:脏读、不可重复读、幻读的区别?** - 脏读:读到其他事务未提交的数据 - 不可重复读:同一事务内两次读同一行,结果不同(其他事务修改并提交) - 幻读:同一事务内两次查询同一条件,返回行数不同(其他事务插入/删除) **Q95:MVCC 原理?(多版本并发控制)** - 每行记录有两个隐藏列:`trx_id`(最近修改事务 ID)、`roll_pointer`(回滚指针指向 undo log 旧版本) - 每个事务创建时生成 ReadView(活跃事务列表) - 通过 ReadView 判断可见性:比较版本链中每个版本的事务 ID 是否在 ReadView 中 - 可重复读:事务开始时创建 ReadView,整个事务期间复用(解决了不可重复读) - 读已提交:每次读都创建新 ReadView - 快照读用 MVCC,当前读(`SELECT ... FOR UPDATE`、`UPDATE`、`DELETE`)用加锁 **Q96:`current read` 和 `snapshot read` 的区别?** - 快照读:普通 SELECT,通过 MVCC 读历史版本,无锁 - 当前读:SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE、INSERT,读最新并加锁 **Q97:redo log、undo log、binlog 的区别?** - redo log(InnoDB,物理日志):记录页的物理修改,用于崩溃恢复,保证持久性。WAL(Write Ahead Log,先写日志再写数据) - undo log(InnoDB,逻辑日志):记录数据旧版本,用于回滚和 MVCC - binlog(MySQL Server 层,逻辑日志):记录所有变更,用于主从复制、数据恢复、归档 - 两阶段提交:redo log 和 binlog 保持一致性 **Q98:什么是 WAL?为什么用 WAL?** - Write Ahead Log:先写 redo log(顺序 IO)再刷脏页到磁盘(随机 IO) - 减少随机 IO,提升性能,同时保证崩溃可恢复 ### 90. 锁 **Q99:MySQL 有哪些锁?** - 按粒度:表级锁(表锁、MDL 锁、意向锁)、行级锁(记录锁、间隙锁、临键锁) - 按类型:共享锁(S)、排他锁(X) - 意向锁:表级的意向共享/排他锁,用于快速判断表是否有行锁(避免逐行检查) **Q100:记录锁、间隙锁、临键锁(Next-Key Lock)的区别?** - 记录锁(Record Lock):锁单行 - 间隙锁(Gap Lock):锁一个范围(开区间),阻止其他事务在该范围插入,用于解决幻读 - 临键锁(Next-Key Lock):记录锁 + 间隙锁组合,左开右闭 `(a, b]` - 可重复读级别下 InnoDB 默认使用临键锁 **Q101:死锁如何产生?如何解决?** - 两个事务互相持有对方需要的锁 - 示例:事务 A 锁行 1 想要行 2,事务 B 锁行 2 想要行 1 - InnoDB 自动检测死锁,回滚其中一个(报 `Deadlock found`) - 预防:按固定顺序加锁、减少事务持有锁时间、索引设计避免全表扫描加表锁 **Q102:`SELECT ... FOR UPDATE` 的用途?** - 悲观锁:当前读,锁住行直到事务提交,防止并发修改 - 适合业务中需要先查后改、数据一致性要求高的场景 ### 91. 索引 **Q103:索引的分类?** - 按数据结构:B+ 树索引、Hash 索引、全文索引(FULLTEXT)、空间索引 - 按物理存储:聚簇索引(主键)、非聚簇索引(二级索引) - 按逻辑:普通索引、唯一索引、主键索引、联合索引(组合索引)、覆盖索引 - 前缀索引 **Q104:聚簇索引和非聚簇索引的区别?回表是什么?** - 聚簇索引(主键索引):叶子节点存整行数据,一张表只能一个 - 非聚簇索引(二级索引):叶子节点存主键值;通过二级索引查数据需要先用索引查到主键,再回聚簇索引查,叫回表 - 覆盖索引:查询的列都在索引里,无需回表,性能高 **Q105:最左前缀原则?** - 联合索引 `(a, b, c)`,查询条件必须从左往右匹配才能走索引 - 跳过中间列则后续列失效;最左列是 b 而不含 a 则索引失效 - 范围查询(`>`、`<`、`BETWEEN`)之后的列索引失效(等值可继续) **Q106:哪些情况索引会失效?** - 对索引列使用函数/运算(`WHERE YEAR(create_time)=2023`) - 隐式类型转换(`WHERE phone = 123`,phone 是 varchar) - 使用 `%` 开头的 LIKE(`LIKE '%abc'`) - OR 连接的条件未全部加索引 - 违反最左前缀 - 列参与表达式、`IS NULL`/`IS NOT NULL` 在某些情况 - 索引列上加函数、使用 `!=`/`<>`(有时可用范围优化) - 全表扫描比走索引更快(数据量小或选择性差) **Q107:如何判断 SQL 是否走了索引?EXPLAIN 的关键字段?** - `EXPLAIN SELECT ...` - `type`:const > eq_ref > ref > range > index > ALL(性能从好到差) - `key`:实际使用的索引;`possible_keys`:可能使用的索引 - `rows`:预估扫描行数 - `Extra`:`Using index`(覆盖索引)、`Using where`、`Using temporary`(临时表)、`Using filesort`(文件排序) **Q108:索引的优缺点?** - 优点:加快查询、唯一索引保证唯一性、加速排序分组(ORDER BY、GROUP BY) - 缺点:占用空间、增删改需维护索引降低写性能、太多索引增加优化器成本 - 原则:选择性高的列、where/join/order by 用到的列建索引;避免过多索引 **Q109:什么是回表优化、索引下推(ICP)?** - 索引下推(Index Condition Pushdown):在二级索引层面过滤记录,减少回表次数 - MySQL 5.6+ 默认开启,`Using index condition` **Q110:大表如何建索引?在线 DDL?** - 在线 DDL:`ALTER TABLE ... ADD INDEX ...`(InnoDB 5.6+ 支持 ALGORITHM=INPLACE,减少锁表时间) - 工具:pt-online-schema-change、gh-ost(无锁迁移) ### 92. SQL 优化 **Q111:慢查询如何排查?** - 开启慢查询日志:`slow_query_log`、`long_query_time` - 分析慢查询日志、`mysqldumpslow` 工具 - 对慢 SQL 用 EXPLAIN 分析执行计划,针对性优化 **Q112:分页查询 `LIMIT` 慢如何优化?** - 深分页问题:`LIMIT 100000, 20` 需要扫描前 100000 行 - 优化: 1. 利用覆盖索引 + 延迟关联:`WHERE id > (上次最大id) LIMIT 20` 2. 子查询先取 id:`SELECT * FROM t WHERE id IN (SELECT id FROM t ORDER BY id LIMIT 100000,20)` 3. 记录上次游标 **Q113:`JOIN` 优化原则?** - 小表驱动大表(优化器自动选择驱动表) - join 的字段建索引 - 避免多表笛卡尔积 - 大批量关联考虑冗余字段、反范式化 **Q114:`ORDER BY` 出现 filesort 如何优化?** - 给排序字段建索引(联合索引按顺序) - 减少排序列 - 覆盖索引避免回表 **Q115:`GROUP BY` 优化?** - 建索引 - 用 `DISTINCT` 或临时表 - 大数据量考虑物化 ### 93. 主从复制与高可用 **Q116:主从复制的原理?** - 主库写 binlog,从库 IO 线程拉取 binlog 到 relay log,SQL 线程重放 - 三种复制模式:异步复制、半同步复制、全同步复制(组复制) - 主从延迟:SQL 线程串行重放导致的延迟 **Q117:主从延迟如何解决?** - 优化从库性能、并行复制(多线程) - 读写分离策略:强一致读走主库,可容忍延迟走从库 - 引入中间件(如 sharding-proxy、one proxy)路由 **Q118:什么是读写分离?如何实现?** - 写主库、读从库,降低主库压力 - 实现:应用层路由、中间件(MyCat、ShardingSphere、ProxySQL) - 注意:主从延迟导致读到旧数据,需合理设计 **Q119:分库分表的策略?** - 垂直拆分:按业务模块拆库、按字段拆表(大字段单独表) - 水平拆分:按 hash、range、时间取模分片 - 分片键选择:均匀分布、业务查询友好 - 中间件:ShardingSphere、MyCat;或应用层分片 **Q120:分库分表后面临的问题?** - 分布式事务(两阶段提交、TCC、Saga、可靠消息) - 全局唯一 ID(雪花算法、号段模式) - 跨库 join 变应用层组装 - 跨库分页、排序困难 - 数据迁移、扩容复杂 ### 94. 其他高频 **Q121:什么是覆盖索引?** - 查询所需的列全部在索引中,无需回表,`Extra` 显示 `Using index` **Q122:`char` 和 `varchar` 的区别?** - char:定长,浪费空间但访问快 - varchar:变长,节省空间,需额外 1-2 字节存长度 - char 尾部空格会去掉,varchar 保留 - 长度短、固定长度的用 char **Q123:`varchar(255)` 和 `varchar(256)` 的区别?** - 256 以上需要 2 字节存长度,255 以下 1 字节 **Q124:int(11) 的 11 是什么意思?** - 显示宽度,配合 zerofill,不影响存储范围(int 始终 4 字节) **Q125:什么情况下适合用 null,什么情况下用 not null?** - 尽量避免 NULL:NULL 查询无法走索引(部分情况)、统计聚合跳过 NULL、占更多空间(需要标志位) - 设计上尽量 `NOT NULL DEFAULT` 默认值 **Q126:唯一索引和普通索引的区别?重复数据场景?** - 唯一索引插入前检查唯一性;普通索引通过 change buffer 优化(写缓冲,非唯一索引可缓存) **Q127:Change Buffer 是什么?** - 写缓冲,针对非唯一二级索引,合并多次修改减少磁盘 IO - 适合写多读少的场景 **Q128:如何排查 MySQL 性能问题?** - `SHOW PROCESSLIST` / `SHOW FULL PROCESSLIST`:看正在执行的 SQL - 慢查询日志 - `EXPLAIN` 执行计划 - `SHOW STATUS`、`performance_schema`、`sys` 库 - 连接数、buffer pool 命中率 **Q129:`IN` 和 `EXISTS` 的区别?** - 小表驱动大表:IN 适合外表数据少,EXISTS 适合内表数据少(旧版语义) - MySQL 优化器已能自动改写,现代版本差别不大 - 实际看执行计划 **Q130:`UNION` 和 `UNION ALL`?** - UNION 去重(需要排序去重,慢),UNION ALL 不去重 - 明确无重复用 UNION ALL 更快 **Q131:什么是 SQL 注入?如何预防?** - 拼接用户输入进 SQL 导致注入 - 预防:参数化查询(prepared statement)、ORM 绑定参数、白名单校验、最小权限账号 --- # 第三部分:Redis ## 一、数据类型与底层实现 ### 132. 核心数据类型 **Q132:Redis 有哪些数据类型?各自的应用场景?** 1. **String**(字符串):缓存、计数器(INCR)、分布式锁、Session 2. **List**(列表):消息队列(LPUSH/BRPOP)、最新列表、时间线 3. **Hash**(哈希):对象存储(用户信息、商品详情)、购物车 4. **Set**(集合):去重、交集并集(共同好友)、标签、抽奖 5. **ZSet**(有序集合):排行榜、延时队列、优先级队列 6. **BitMap**:签到、在线状态、布隆过滤器实现 7. **HyperLogLog**:基数统计(UV) 8. **Geo**:地理位置(附近的人、打车) 9. **Stream**:消息队列(5.0+,支持消费组) **Q133:String 的底层实现?SDS 为什么好?** - 底层 SDS(简单动态字符串),不是 C 字符串 - 优点:O(1) 获取长度、避免缓冲区溢出(自动扩容)、二进制安全、减少内存重分配(预分配+惰性释放) - int 编码:整数用 long 存储 **Q134:List 的底层实现?** - 压缩列表(ziplist,元素少时)→ 双向链表(linkedlist)→ 3.2+ 统一为 quicklist - 6.0+ 引入 listpack 替代 ziplist **Q135:Hash 的底层实现?** - 元素少且值小时用 listpack/ziplist(紧凑编码),否则用哈希表(hashtable) - 扩容/缩容:渐进式 rehash **Q136:Set 的底层实现?** - 全整数且数量少:整数集合(intset) - 否则:哈希表(value 为 null) **Q137:ZSet 的底层实现?** - 元素少:listpack - 否则:跳跃表(skiplist)+ 哈希表(hash 存 member→score) - 跳跃表:多层有序链表,支持 O(logN) 查找,用于范围查询 - 为什么用跳表不用平衡树/红黑树?跳表实现简单、范围查询方便、内存可控 **Q138:为什么 ZSet 用跳表而不用 B+ 树/红黑树?** - 跳表实现简单、范围查询高效、可灵活调整层数 - 红黑树实现复杂、范围查询需中序遍历 - 相比 B+ 树(数据库用),Redis 数据在内存,不需要磁盘 IO 优化 ### 139. 持久化 **Q139:Redis 持久化有哪几种?** 1. **RDB**:快照,全量数据二进制文件(dump.rdb),适合备份恢复、启动快 2. **AOF**:追加写命令日志,数据更安全,可配置 fsync 策略 3. **混合持久化**(4.0+):RDB 快照 + AOF 增量命令,兼顾恢复速度与数据安全 **Q140:RDB 和 AOF 的优缺点?** - RDB:恢复快、文件小;但可能丢失最近数据(快照间隔内)、fork 子进程有内存开销 - AOF:数据安全(可配置 everysec/always)、文件大、恢复慢、写性能有影响 - 生产建议:RDB 备份 + AOF 保证,或混合持久化 **Q141:AOF 的三种 fsync 策略?** - `always`:每次写都 fsync,最安全最慢 - `everysec`:每秒 fsync,最多丢 1 秒数据(默认,推荐) - `no`:交给操作系统,最快最不安全 **Q142:RDB 触发时机?** - 手动 `SAVE`(阻塞)或 `BGSAVE`(后台 fork) - 配置 `save 900 1` 等自动触发 - 关闭时/主从复制时 **Q143:AOF 重写(rewrite)?** - 用当前数据集生成最小化命令集,压缩 AOF 文件 - BGREWRITEAOF,后台重写 ### 144. 过期与内存淘汰 **Q144:Redis 的过期删除策略?** - 惰性删除:读时发现过期才删,省 CPU 但可能有脏数据残留 - 定期删除:周期性随机抽取部分 key 检查删除 - 两者结合 **Q145:Redis 内存淘汰策略(maxmemory-policy)?** 1. `noeviction`:不淘汰,写失败报错(默认) 2. `allkeys-lru`:所有 key 中 LRU 3. `volatile-lru`:设了过期时间的 key 中 LRU 4. `allkeys-lfu`:所有 key 中 LFU(4.0+) 5. `volatile-lfu`:过期 key 中 LFU 6. `allkeys-random`、`volatile-random` 7. `volatile-ttl`:优先淘汰即将过期的 - 业务无热数据区分用 allkeys-lru;有明确冷热用 LFU **Q146:LRU 和 LFU 的区别?** - LRU:最近最少使用(按时间),基于访问时间,可能被批量扫描污染 - LFU:最不经常使用(按频率),基于访问频率,更适合有冷热之分的场景 **Q147:Redis 的 key 过期了会立即释放内存吗?** - 惰性删除 + 定期删除,不会立即释放,可能短暂存在 ### 148. 缓存三兄弟 **Q148:缓存穿透、击穿、雪崩?如何解决?** - **穿透**:查询不存在的 key,绕过缓存打数据库 - 解决:缓存空值(带较短过期)、布隆过滤器、参数校验 - **击穿**:单个热点 key 过期瞬间,大量请求打数据库 - 解决:互斥锁(只允许一个请求重建缓存)、逻辑过期、热点 key 永不过期 - **雪崩**:大量 key 同时过期,或 Redis 宕机,导致请求全部打数据库 - 解决:过期时间加随机值、多级缓存(本地缓存 + Redis)、Redis 高可用(主从/哨兵/集群)、限流降级 **Q149:如何保证缓存和数据库的一致性?(经典难题)** - 常见方案: 1. **Cache Aside(旁路缓存)**:先更新数据库,再删缓存;读时未命中则回源 2. **延迟双删**:更新库后删缓存,再延迟一段时间再删一次(解决并发读旧) 3. **先删缓存后更新库**:有风险,一般不推荐 - 最终一致性:消息队列异步重试删除 - 强一致(极端):读改写串行化、加分布式锁、或引入 binlog 订阅(Canal) - 推荐:Cache Aside + 删缓存 + 保证删除成功的补偿机制 **Q150:为什么先更新数据库再删缓存,而不是先删缓存?** - 先删缓存后更新库:更新期间读请求会读到旧值并回填缓存,导致脏数据 - 先更库再删缓存:只在删除缓存那一刻有极小窗口不一致,且可用延迟双删兜底 **Q151:缓存雪崩中,如何做限流降级?** - 本地缓存兜底、Sentinel/Hystrix 限流、熔断、接口幂等降级 ### 152. 分布式锁 **Q152:Redis 实现分布式锁的正确姿势?** - 基于 SETNX + 过期时间: ```bash SET lock_key request_id NX PX 30000 # 原子操作,NX 不存在才设置,PX 过期 ``` - 释放:Lua 脚本原子地校验 value 再删除(防止误删别人的锁) ```lua if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end ``` - value 用唯一标识(request_id/线程 id),防止自己过期后误删他人锁 **Q153:Redis 分布式锁的缺点?RedLock?** - 单点故障:master 宕机锁丢失(主从异步复制) - RedLock(Redisson 实现):向多个 Redis 节点申请锁,过半成功才算获取,解决主从故障 - RedLock 本身有争议(时钟跳跃、GC 停顿),多数场景用单机 + 主从哨兵即可 - 业务应加"看门狗"续期防止锁过期导致并发问题(Redisson WatchDog) **Q154:分布式锁的续期机制?** - Redisson WatchDog:默认 30s 锁,后台每 10s 续期,防止业务未执行完锁就过期 ### 155. 集群与高可用 **Q155:Redis 主从复制的原理?** - 全量复制:从库连主库,主库 BGSAVE 生成 RDB 快照,传给从库加载;复制期间的写命令通过 buffer 补发 - 增量复制:复制积压缓冲区(repl_backlog) - 主从异步复制,可能丢数据 **Q156:哨兵(Sentinel)的作用?** - 监控、自动故障转移(选新主)、通知 - 哨兵集群(奇数个)防止脑裂 - 客户端通过哨兵获取主从地址 **Q157:Redis Cluster 的分片原理?** - 16384 个哈希槽(slot),`CRC16(key) % 16384` 定位槽 - 每个节点负责一部分槽 - 客户端重定向:MOVED(槽迁移到别的节点) - 支持水平扩展 **Q158:集群中的多 key 操作问题?** - 多 key 需在同一个槽才能原子操作 - 用 hash tag:`{user1}:info` 保证相同 tag 落在同一槽 **Q159:Redis 和 MySQL 的一致性如何保证?(缓存一致性的综合场景)** - 参考 Q149,重点:Cache Aside、删缓存、补偿机制、Canal 订阅 binlog ### 160. 其他高频 **Q160:Redis 是单线程为什么还快?** - 基于内存、IO 多路复用(epoll)、避免线程切换和锁竞争、单线程避免竞态 - 6.0+ 引入多线程处理网络 IO(读命令、写回复),命令执行仍单线程(保证原子性) **Q161:Redis IO 多路复用原理?** - epoll:内核监听多个 fd,事件就绪才回调 - Redis 单线程事件循环:读事件 → 处理命令 → 写事件 **Q162:Redis 的过期 key 通知、Pub/Sub?** - 发布订阅(Pub/Sub):即时推送,不持久化,客户端断开丢消息 - 5.0+ Stream:可持久化、消费组、ACK,更适合消息队列 **Q163:Redis 事务?为什么不用事务做复杂逻辑?** - MULTI/EXEC:命令入队后批量执行,但只是"顺序执行",不支持回滚(命令错误时其余继续执行) - 用 Lua 脚本实现原子操作更灵活 **Q164:Redis 的 Pipeline 是什么?** - 批量发送命令减少 RTT(网络往返) - 不是原子事务,只是批量传输 **Q165:如何统计 UV(去重计数)?** - HyperLogLog:`PFADD` / `PFCOUNT`,误差约 0.81%,节省内存(12KB 左右统计上亿) - 精确场景用 Set **Q166:如何实现附近的人(Geo)?** - `GEOADD` / `GEORADIUS`,底层基于 ZSet + geohash 编码 **Q167:如何实现排行榜?** - ZSet:`ZADD` 分数,`ZREVRANGE` 取前 N,`ZINCRBY` 加分 **Q168:布隆过滤器?为什么 Redis 4.0 前的布隆问题?** - 用多个 hash 位数组,判断"一定不存在"和"可能存在" - 有误判率,不能删除(或需计数布隆) - 可用于穿透防护:缓存 key 是否存在判断 **Q169:Redis 内存碎片怎么处理?** - `INFO memory` 查看 `mem_fragmentation_ratio` - `activedefrag yes` 自动整理(4.0+) - 重启清空整理 **Q170:Redis 的 OOM 或内存满了会怎样?** - 按 maxmemory-policy 淘汰;noeviction 时报错 `OOM command not allowed` **Q171:Redis 大 key、热 key 的问题?** - 大 key:阻塞命令(删除、序列化慢)、内存不均。解决:拆分、渐进删除(UNLINK) - 热 key:单节点压力大。解决:本地缓存、多副本、热点 key 打散(加后缀) **Q172:Redis 冷热数据分离、多级缓存架构?** - L1 本地缓存(Caffeine/GoCache)+ L2 Redis + DB - 一致性:L1 失效策略、广播失效 **Q173:Redis 如何做持久化到磁盘的性能优化?** - AOF everysec、RDB 配置合理、避免频繁 BGSAVE、内存碎片整理 **Q174:Redis 6.0 新特性?** - 多线程 IO、ACL 权限控制、Client-side caching、RESP3 协议 **Q175:Redis 与消息队列(对比 Kafka/RabbitMQ)?** - Redis Stream 适合轻量、低延迟、对可靠性要求不高的场景 - Kafka/RabbitMQ 适合大数据量、持久化、可靠投递、复杂路由 --- ## 附:高频综合场景题(融合三技术栈) **Q176:设计一个高并发秒杀系统,Redis + MySQL 怎么协作?** 1. 前端限流(验证码、令牌桶) 2. 请求先到 Redis 预扣库存(Lua 原子减库存,防止超卖) 3. Redis 扣减成功才写 MySQL 订单(最终一致性) 4. 异步下单(消息队列)削峰 5. 订单状态用 Redis 缓存,数据库落库 - 关键:Redis 原子扣库存防止超卖、消息队列削峰、数据库最终一致性 **Q177:如何设计一个分布式 ID 生成器?** - 方案:雪花算法(Snowflake)、号段模式(Redis 或 DB 发号)、UUID(无序)、Redis INCR - 雪花:时间戳 + 机器 ID + 序列号,趋势递增、无依赖 **Q178:用户登录 Session 用 Redis 存还是 JWT?** - Redis Session:可服务端踢下线、精确控制过期,需存储 - JWT:无状态、跨服务,但无法主动失效、无法踢下线 - 高安全场景常用 Redis Session + 短 token **Q179:实现一个限流器(Redis)?** - 令牌桶 / 漏桶 / 固定窗口 / 滑动窗口(ZSet) - 简单实现:`INCR` + 过期时间(固定窗口) - 滑窗:ZSet 记录时间戳,`ZREMRANGEBYSCORE` + `ZCARD` **Q180:Go 中如何封装 Redis 连接池?** - 使用 go-redis / redigo 自带连接池 - 配置 MaxIdle、MaxActive、IdleTimeout - 注意:连接泄漏、命令超时、重试策略 **Q181:Go + MySQL 的 ORM 选择?GORM 的坑?** - 常用:GORM、sqlx、database/sql - GORM 坑:自动迁移、`Where` 的零值问题(结构体查询零值字段被忽略)、N+1 查询、preload 使用 **Q182:Go 中如何优雅处理数据库连接池?** - `sql.DB.SetMaxOpenConns`(最大连接)、`SetMaxIdleConns`、`SetConnMaxLifetime` - 监控 `db.Stats()` **Q183:一个完整的服务如何做缓存一致性、幂等、重试?** - 缓存一致性:Cache Aside + 删缓存 + 补偿 - 幂等:唯一请求 ID、分布式锁、数据库唯一索引 - 重试:指数退避 + 上限,MQ 重试队列 **Q184:如何排查线上慢接口(Go + MySQL + Redis)?** - 链路追踪(OpenTelemetry/Jaeger)、日志 - pprof 看 CPU/内存/goroutine - MySQL 慢查询 + EXPLAIN、Redis 慢日志 `SLOWLOG` - 缓存命中率、DB 连接池、GC 压力 --- > **面试技巧补充** > - 能讲原理(底层数据结构、源码)比背结论更有竞争力 > - 场景题要有取舍思维:"我选 A,因为 B 场景下 C 更合适" > - 数字要准确:B+ 树每页 16KB、Redis 16384 槽、GC 触发 GOGC=100、channel 无缓冲同步等 > - 简历里写过的技术栈,每个都要能对答 3 层"为什么"