diff --git a/docs/interview/index.md b/docs/interview/index.md index 654a4d7..f328cf2 100644 --- a/docs/interview/index.md +++ b/docs/interview/index.md @@ -1,3 +1,8 @@ # 面经 面试经验记录与总结。 + +| 公司 | 岗位 | 轮次 | 日期 | +|------|------|------|------| +| [去哪儿](qunar-ai-fullstack.md) | AI 全栈 | 一面 | — | +| [奇安信](qianxin-ai-fullstack-round1.md) | AI 全栈开发 | 一面 | 2025-09-10 | diff --git a/docs/interview/qianxin-ai-fullstack-round1.md b/docs/interview/qianxin-ai-fullstack-round1.md new file mode 100644 index 0000000..e6821aa --- /dev/null +++ b/docs/interview/qianxin-ai-fullstack-round1.md @@ -0,0 +1,784 @@ +# 奇安信 AI 全栈开发 — 一面 + +> 9.10 面试,耗时一小时,体验不错,面试官非常友好 +> 以下内容根据回忆记录,未录音 + +--- + +## 1. 自我介绍 + +常规开场,简要介绍个人背景、技术栈和项目经历。 + +!!! tip "回答框架" + - **Who**:姓名、学历、工作年限 + - **What**:核心技术栈(如 Go + Redis + MySQL + AI) + - **Highlight**:1-2 个最有亮点的项目 + - **Why**:为什么投这个岗位 + +--- + +## 2. 两段实习哪一段最具有挑战性 & 若干实习追问 + +面试官希望了解实习经历中的深度思考和技术挑战,而非简单罗列做了什么。 + +!!! tip "STAR 法则回答框架" + - **Situation**:项目的背景和业务上下文 + - **Task**:你负责的具体任务和目标 + - **Action**:你采取的技术方案和关键决策 + - **Result**:量化的成果 + +**回答要点:** + +- 选择技术难度最高或业务影响最大的那段实习 +- 重点描述遇到的技术挑战和解决方案 +- 面试官会根据回答进行深入追问,需要对项目细节了然于胸 + +--- + +## 3. 滑动窗口分布式限流 & 数据结构 & 过期时间设置 + +### 3.1 滑动窗口限流原理 + +滑动窗口限流是对固定窗口的改进,解决了固定窗口在窗口边界处可能出现的突发流量问题。 + +```mermaid +graph LR + subgraph 固定窗口 + W1[窗口1
00:00-01:00
计数=100] --> W2[窗口2
01:00-02:00
计数=0] + end + subgraph 滑动窗口 + S1[00:30-01:30
统计最近60s] + S2[00:45-01:45
统计最近60s] + end +``` + +**核心思想**:统计当前时刻往前推 N 秒内的请求数量,窗口随时间持续滑动。 + +### 3.2 Redis 实现方案 — Sorted Set + +最常用的实现方式是使用 Redis 的 **ZSET(有序集合)**: + +| 字段 | 说明 | +|------|------| +| **Member** | 请求唯一标识(UUID 或雪花 ID) | +| **Score** | 请求的时间戳(毫秒级) | + +**核心逻辑:** + +```go +// 滑动窗口限流 — Lua 脚本保证原子性 +local key = KEYS[1] -- 限流 key +local now = tonumber(ARGV[1]) -- 当前时间戳(ms) +local window = tonumber(ARGV[2]) -- 窗口大小(ms) +local limit = tonumber(ARGV[3]) -- 窗口内最大请求数 + +-- 1. 移除窗口外的过期成员 +redis.call('ZREMRANGEBYSCORE', key, 0, now - window) + +-- 2. 统计窗口内的请求数 +local count = redis.call('ZCARD', key) + +-- 3. 判断是否限流 +if count < limit then + redis.call('ZADD', key, now, ARGV[4]) -- 添加当前请求 + redis.call('PEXPIRE', key, window) -- 设置过期时间 + return 1 -- 允许 +else + return 0 -- 拒绝 +end +``` + +### 3.3 数据结构选择 + +| 方案 | 数据结构 | 精确度 | 内存开销 | 复杂度 | +|------|---------|--------|---------|--------| +| **ZSET** | 有序集合 | 精确 | 较高(每请求一个 member) | O(log N) | +| **Bitmap** | 位图 | 时间片级别 | 极低 | O(1) | +| **滑动窗口计数器** | Hash | 近似 | 低 | O(1) | + +### 3.4 过期时间设置 + +!!! warning "关键细节" + ZSET 本身不会自动删除过期 member,需要配合 `PEXPIRE` 设置 key 的 TTL。 + +- **TTL 设置为窗口大小**:每次写入时用 `PEXPIRE key window_ms` 刷新 key 的过期时间 +- **惰性清理**:每次请求时先执行 `ZREMRANGEBYSCORE` 清理窗口外的数据 +- **避免内存泄漏**:如果 key 长期无访问,TTL 到期后 Redis 会自动删除整个 key + +--- + +## 4. MULTI 能否保证原子性 & MULTI/EXEC 和 Lua 有什么区别 + +### 4.1 MULTI/EXEC 的原子性 + +`MULTI/EXEC` 提供的是**部分原子性**(也称为"事务"),但与关系型数据库的事务有本质区别: + +| 特性 | MULTI/EXEC | 数据库事务 | +|------|-----------|-----------| +| 命令入队 | 命令在 EXEC 之前只是入队,不执行 | 语句逐条执行 | +| 执行方式 | EXEC 时一次性按序执行 | 逐条执行,可回滚 | +| 回滚 | **不支持回滚**,某条命令失败不影响其他命令 | 支持回滚 | +| 隔离性 | 单线程保证不会被其他命令插入 | 通过锁和 MVCC 保证 | + +```mermaid +sequenceDiagram + participant C as 客户端 + participant R as Redis + C->>R: MULTI + R-->>C: OK + C->>R: SET key1 value1 + R-->>C: QUEUED + C->>R: SET key2 value2 + R-->>C: QUEUED + C->>R: EXEC + R-->>C: [OK, OK] +``` + +!!! warning "MULTI 不支持回滚" + 如果 EXEC 中某条命令执行失败(如对 string 执行 LPUSH),Redis **不会回滚**其他已执行的命令。这是 Redis 的设计哲学:保持简单和高性能。 + +### 4.2 MULTI/EXEC vs Lua 脚本 + +| 维度 | MULTI/EXEC | Lua 脚本 | +|------|-----------|---------| +| **原子性** | 命令按序执行,但不支持条件逻辑 | 完全原子,支持条件分支 | +| **条件逻辑** | 不支持(无法根据前一条结果决定后续操作) | 支持 if/else/循环 | +| **网络开销** | 多条命令 + EXEC,多次网络往返(入队阶段) | 一次性发送整个脚本 | +| **错误处理** | 某条失败继续执行后续命令 | 脚本内可处理错误逻辑 | +| **可复用** | 不支持 | 可用 `EVALSHA` 缓存脚本,减少传输 | +| **阻塞风险** | 不会长时间阻塞 | 复杂脚本可能阻塞 Redis | + +**选择建议:** + +- **简单批量操作**(无条件逻辑)→ MULTI/EXEC +- **需要条件判断**(如限流:先查计数再决定是否放行)→ Lua 脚本 +- **高性能要求** → Lua 脚本(一次网络往返 + 原子执行) + +--- + +## 5. Redis 单线程为什么快 & Redis 不可用如何处理 + +### 5.1 Redis 单线程为什么快 + +| 因素 | 说明 | +|------|------| +| **纯内存操作** | 数据存储在内存中,读写速度是磁盘的 10 万倍 | +| **单线程避免上下文切换** | 不存在多线程的锁竞争和上下文切换开销 | +| **I/O 多路复用** | 使用 epoll/kqueue 实现单线程同时处理大量连接 | +| **高效数据结构** | SDS、ZIPLIST、SKIPLIST 等针对场景优化的数据结构 | +| **简单通信协议** | RESP 协议解析简单高效 | + +```mermaid +graph TD + subgraph Redis 单线程模型 + EP[epoll 多路复用] -->|就绪事件| EL[事件循环 Event Loop] + EL -->|读事件| RH[命令解析与执行] + EL -->|写事件| WH[响应发送] + RH -->|写回客户端| WH + end +``` + +!!! note "Redis 6.0+ 的多线程" + Redis 6.0 引入了 **I/O 多线程**,但仅用于网络 I/O(读写 socket),**命令执行仍然是单线程**。这样既利用多核加速网络处理,又保持了命令执行的简单性和原子性。 + +### 5.2 Redis 不可用如何处理 + +```mermaid +graph TD + A[Redis 不可用] --> B{部署模式?} + B -->|单机| C[降级方案] + B -->|主从| D[哨兵自动故障转移] + B -->|集群| E[Cluster 自动故障转移] + C --> C1[本地缓存兜底] + C --> C2[直接访问数据库 + 限流] + C --> C3[返回默认值/缓存空值] + D --> D1[Sentinel 检测主节点下线] + D --> D2[选举新主节点] + D --> D3[客户端自动切换] + E --> E1[节点间 Gossip 检测] + E --> E2[故障节点的从节点晋升] +``` + +**降级策略:** + +| 策略 | 适用场景 | 实现方式 | +|------|---------|---------| +| **本地缓存兜底** | 读多写少 | 使用 sync.Map 或 LRU Cache 缓存热点数据 | +| **直接查库 + 限流** | 数据一致性要求高 | 限制穿透到数据库的 QPS,防止雪崩 | +| **返回默认值** | 容忍短暂不一致 | 返回预设的默认值或静态数据 | +| **熔断降级** | 非核心功能 | 使用 Hystrix/sentinel 熔断,返回降级结果 | + +--- + +## 6. 多级缓存相关问题 + +### 6.1 多级缓存架构 + +```mermaid +graph LR + R[请求] --> L1[L1 本地缓存
进程内 / sync.Map] + L1 -->|未命中| L2[L2 分布式缓存
Redis] + L2 -->|未命中| DB[数据库 MySQL] + DB -->|回写| L2 + L2 -->|回写| L1 +``` + +### 6.2 各级缓存对比 + +| 层级 | 介质 | 延迟 | 容量 | 一致性 | 适用数据 | +|------|------|------|------|--------|---------| +| **L1** | 进程内存(Go: sync.Map/bigcache) | 纳秒级 | 小(受 JVM/Go 内存限制) | 难以保证 | 极热点、变更少 | +| **L2** | Redis | 毫秒级 | 大(集群可扩展) | 较好保证 | 通用热点数据 | +| **L3** | MySQL / 磁盘 | 十毫秒级 | 海量 | 强一致 | 全量数据 | + +### 6.3 常见问题 + +**缓存一致性问题:** + +- **写时失效**:更新数据库时同时删除缓存(Cache Aside 模式) +- **延迟双删**:先删缓存 → 更新数据库 → 延迟 N 毫秒 → 再删缓存 +- **消息队列异步**:数据库变更事件发送到 MQ,消费者更新缓存 + +**L1 缓存的特殊挑战:** + +- 多实例间 L1 缓存不一致 → 需要广播失效(如 Redis Pub/Sub) +- L1 缓存容量有限 → 需要精心设计淘汰策略(LRU / LFU / TTL) + +--- + +## 7. 认证链路 & SAML 是什么 & OAuth2.0 原理 + +### 7.1 认证(Authentication)vs 授权(Authorization) + +| 概念 | 含义 | 典型问题 | +|------|------|---------| +| **认证(AuthN)** | 你是谁 | 用户名密码、MFA、SSO | +| **授权(AuthZ)** | 你能做什么 | RBAC、ACL、OAuth Scope | + +### 7.2 SAML(Security Assertion Markup Language) + +SAML 是一种基于 XML 的**联邦身份认证**标准,主要用于企业级 SSO(单点登录)。 + +```mermaid +sequenceDiagram + participant U as 用户/浏览器 + participant SP as 服务提供方(SP) + participant IdP as 身份提供方(IdP) + U->>SP: 访问受保护资源 + SP->>U: 302 重定向到 IdP + U->>IdP: 携带 SAMLRequest + IdP->>IdP: 认证用户(登录页) + IdP->>U: 返回 SAML Response(XML 断言) + U->>SP: 提交 SAML Response + SP->>SP: 验证签名、解析断言 + SP->>U: 授权访问 +``` + +**SAML 核心组件:** + +| 组件 | 说明 | +|------|------| +| **IdP(身份提供方)** | 负责认证用户,如 Okta、ADFS | +| **SP(服务提供方)** | 需要认证的应用 | +| **Assertion(断言)** | 用户身份信息的 XML 声明 | +| **XML Signature** | 保证断言的完整性和不可否认性 | + +### 7.3 OAuth 2.0 原理 + +OAuth 2.0 是一种**授权框架**,核心目标是让第三方应用在**不接触用户密码**的情况下,获取对用户资源的有限访问权限。 + +**四种授权模式:** + +| 模式 | 适用场景 | 安全性 | +|------|---------|--------| +| **授权码模式** | 有后端的 Web 应用 | 最高(推荐) | +| **隐式模式** | 纯前端 SPA | 较低(已不推荐) | +| **密码模式** | 高度信任的第一方应用 | 中(不推荐第三方使用) | +| **客户端凭证** | 服务间通信 | 高(无用户参与) | + +**授权码模式完整流程:** + +```mermaid +sequenceDiagram + participant U as 用户 + participant C as 客户端(第三方应用) + participant A as 授权服务器 + participant R as 资源服务器 + U->>C: 点击"授权" + C->>A: 重定向:/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&scope=xxx + A->>U: 显示授权页面 + U->>A: 确认授权 + A->>C: 302 重定向 redirect_uri?code=AUTH_CODE + C->>A: POST /token (code + client_secret) + A->>C: 返回 access_token + refresh_token + C->>R: 请求资源 + Bearer access_token + R->>C: 返回受保护资源 +``` + +### 7.4 SAML vs OAuth 2.0 + +| 维度 | SAML | OAuth 2.0 | +|------|------|-----------| +| **目的** | 联邦身份认证(SSO) | 资源授权 | +| **数据格式** | XML | JSON | +| **典型场景** | 企业 SSO(登录 Google Workspace) | 第三方应用授权(GitHub 登录) | +| **Token 类型** | SAML Assertion | Access Token (JWT) | +| **复杂度** | 重(XML 解析、签名验证) | 轻(REST + JSON) | + +--- + +## 8. 缓存穿透 & 布隆过滤原理 + +### 8.1 缓存穿透 + +缓存穿透是指查询一个**一定不存在**的数据,由于缓存未命中,每次都直接查询数据库。 + +```mermaid +graph LR + R[请求 id=-1] --> C[缓存] + C -->|未命中| DB[数据库] + DB -->|不存在| C2[返回空] + C2 --> R2[下次请求 id=-1] + R2 --> C + C -->|仍未命中| DB +``` + +**解决方案:** + +| 方案 | 原理 | 适用场景 | +|------|------|---------| +| **缓存空值** | 查询为空时也写入缓存(短 TTL) | 数据量可控 | +| **布隆过滤器** | 预判断 key 是否可能存在 | 大量 key,允许一定误判 | +| **接口参数校验** | 在入口层拦截非法参数 | 参数有明确约束 | + +### 8.2 布隆过滤器原理 + +布隆过滤器是一种**概率型数据结构**,用于判断一个元素**是否可能存在于集合中**。 + +| 判断结果 | 实际含义 | +|---------|---------| +| **不存在** | 一定不存在(100% 准确) | +| **存在** | 可能存在(有假阳性率) | + +**核心原理:** + +```mermaid +graph TD + subgraph 布隆过滤器 + B[位数组 Bit Array
m 位,初始全 0] + end + subgraph 插入过程 + E[元素 x] --> H1[Hash1(x) → 位置 i1] + E --> H2[Hash2(x) → 位置 i2] + E --> H3[Hashk(x) → 位置 ik] + H1 -->|置 1| B + H2 -->|置 1| B + H3 -->|置 1| B + end + subgraph 查询过程 + Q[查询 y] --> C1[Hash1(y) → 位置 i1] + Q --> C2[Hash2(y) → 位置 i2] + Q --> C3[Hashk(y) → 位置 ik] + C1 --> J{所有位都为 1?} + C2 --> J + C3 --> J + J -->|是| R1[可能存在] + J -->|否| R2[一定不存在] + end +``` + +**关键参数:** + +| 参数 | 含义 | 关系 | +|------|------|------| +| **m** | 位数组大小 | m 越大,误判率越低 | +| **n** | 预期插入元素数量 | n 越大,误判率越高 | +| **k** | 哈希函数个数 | k = (m/n) × ln2 时最优 | +| **p** | 误判率(假阳性率) | p ≈ (1 - e^(-kn/m))^k | + +!!! warning "布隆过滤器的局限" + - **不支持删除**:置 0 可能影响其他元素(需要删除时考虑 Counting Bloom Filter) + - **有假阳性**:判断"存在"时可能误判 + - **需要预估容量**:容量不足时误判率快速上升 + +--- + +## 9. MySQL 引擎有哪些 & InnoDB 特性 + +### 9.1 MySQL 存储引擎对比 + +| 特性 | InnoDB | MyISAM | Memory | Archive | +|------|--------|--------|--------|---------| +| **事务支持** | ✅ | ❌ | ❌ | ❌ | +| **行级锁** | ✅ | ❌(表锁) | ❌(表锁) | ❌ | +| **外键** | ✅ | ❌ | ❌ | ❌ | +| **MVCC** | ✅ | ❌ | ❌ | ❌ | +| **全文索引** | ✅(5.6+) | ✅ | ❌ | ❌ | +| **崩溃恢复** | ✅(redo log) | ❌ | ❌ | ❌ | +| **存储限制** | 64TB | 256TB | 受限于内存 | 无限制 | +| **适用场景** | OLTP、高并发 | 读多写少 | 临时表、缓存 | 日志归档 | + +### 9.2 InnoDB 核心特性 + +```mermaid +graph TD + subgraph InnoDB 架构 + subgraph 内存 + BP[Buffer Pool
数据页 + 索引页] + LG[Log Buffer
redo log 缓冲] + CB[Change Buffer
非唯一索引变更缓冲] + end + subgraph 磁盘 + SYS[系统表空间 ibdata1] + UD[用户表空间 .ibd] + RL[Redo Log ib_logfile] + UL[Undo Log undo_001] + end + BP -->|脏页刷新| UD + LG -->|顺序写| RL + CB -->|合并| UD + end +``` + +**核心特性详解:** + +| 特性 | 说明 | +|------|------| +| **Buffer Pool** | 将数据页和索引页缓存在内存中,通过 LRU 变体算法管理 | +| **Change Buffer** | 对非唯一二级索引的 DML 操作先缓存,后台异步合并,减少随机 I/O | +| **Redo Log** | WAL(Write-Ahead Logging)机制,保证事务的持久性,崩溃后可恢复 | +| **Undo Log** | 保存数据修改前的版本,用于事务回滚和 MVCC 一致性读 | +| **聚簇索引** | 数据按主键顺序存储,主键查询极快 | +| **MVCC** | 多版本并发控制,读操作不阻塞写操作 | +| **自适应哈希索引** | InnoDB 自动为热点页建立哈希索引,加速等值查询 | + +--- + +## 10. 幻读 & 隔离级别 & MVCC + +### 10.1 幻读定义 + +**幻读(Phantom Read)**:在同一事务中,两次执行相同的范围查询,第二次查询返回了第一次查询没有看到的**新行**(由其他事务 INSERT 产生)。 + +```mermaid +sequenceDiagram + participant T1 as 事务 T1 + participant T2 as 事务 T2 + T1->>T1: SELECT * FROM users WHERE age > 20
结果:3 行 + T2->>T2: INSERT INTO users(name, age) VALUES('新用户', 25) + T2->>T2: COMMIT + T1->>T1: SELECT * FROM users WHERE age > 20
结果:4 行(幻读!) +``` + +### 10.2 四种隔离级别 + +| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 | +|---------|------|-----------|------|---------| +| **READ UNCOMMITTED** | ✅ 可能 | ✅ 可能 | ✅ 可能 | 无锁,直接读最新数据 | +| **READ COMMITTED (RC)** | ❌ 避免 | ✅ 可能 | ✅ 可能 | 每次读生成新 ReadView | +| **REPEATABLE READ (RR)** | ❌ 避免 | ❌ 避免 | ⚠️ 部分避免 | 事务开始时生成 ReadView | +| **SERIALIZABLE** | ❌ 避免 | ❌ 避免 | ❌ 避免 | 读加共享锁,写加排他锁 | + +!!! note "InnoDB 在 RR 级别下如何解决幻读" + InnoDB 的 RR 级别通过 **MVCC + Next-Key Lock** 两种机制配合解决幻读: + - **快照读**(普通 SELECT):MVCC 保证读到事务开始时的快照,看不到新插入的行 + - **当前读**(SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE):Next-Key Lock 锁住范围,阻止其他事务在该范围内插入新行 + +### 10.3 MVCC 机制 + +MVCC(Multi-Version Concurrency Control)通过保存数据的多个版本,实现读写不阻塞。 + +**核心组件:** + +| 组件 | 说明 | +|------|------| +| **隐藏列 DB_TRX_ID** | 最近修改该行的事务 ID | +| **隐藏列 DB_ROLL_PTR** | 回滚指针,指向 undo log 中的上一个版本 | +| **隐藏列 DB_ROW_ID** | 隐式主键(无显式主键时使用) | +| **ReadView** | 事务在某一时刻看到的数据快照 | + +**ReadView 结构:** + +| 字段 | 含义 | +|------|------| +| **m_ids** | 创建 ReadView 时,系统中活跃(未提交)的事务 ID 列表 | +| **min_trx_id** | m_ids 中的最小值 | +| **max_trx_id** | 系统应分配给下一个事务的 ID(当前最大 ID + 1) | +| **creator_trx_id** | 创建该 ReadView 的事务 ID | + +**可见性判断规则:** + +```go +func isVisible(rowTrxID uint64, rv *ReadView) bool { + // 1. 如果行的事务 ID 等于创建者 ID,可见(自己修改的) + if rowTrxID == rv.creatorTrxID { + return true + } + // 2. 如果行的事务 ID 小于最小活跃事务 ID,可见(事务已提交) + if rowTrxID < rv.minTrxID { + return true + } + // 3. 如果行的事务 ID 大于等于最大事务 ID,不可见(事务在 ReadView 之后开始) + if rowTrxID >= rv.maxTrxID { + return false + } + // 4. 如果行的事务 ID 在活跃列表中,不可见(事务未提交) + if rv.m_ids.contains(rowTrxID) { + return false + } + // 5. 否则可见(事务已提交) + return true +} +``` + +**RC vs RR 的关键区别:** + +| 隔离级别 | ReadView 生成时机 | 效果 | +|---------|------------------|------| +| **RC** | 每次 SELECT 都生成新的 ReadView | 能看到其他事务已提交的最新数据 | +| **RR** | 事务中第一次 SELECT 时生成,后续复用 | 整个事务看到一致的快照 | + +--- + +## 11. Channel 阻塞非阻塞 & 应用 & context + +### 11.1 Channel 阻塞与非阻塞 + +Go 的 channel 天然是阻塞的,但可以通过 `select` + `default` 实现非阻塞操作。 + +**阻塞行为:** + +| 操作 | 有缓冲 Channel | 无缓冲 Channel | +|------|----------------|---------------| +| **ch <- v** | 缓冲区满时阻塞 | 没有接收者时阻塞 | +| **<-ch** | 缓冲区空时阻塞 | 没有发送者时阻塞 | + +**非阻塞实现:** + +```go +// 非阻塞发送 +select { +case ch <- value: + // 发送成功 +default: + // channel 满或无接收者,跳过 +} + +// 非阻塞接收 +select { +case msg := <-ch: + // 接收到消息 +default: + // channel 空或无发送者,跳过 +} +``` + +### 11.2 Channel 的典型应用 + +| 模式 | 说明 | 示例 | +|------|------|------| +| **生产者-消费者** | 解耦生产和消费速率 | 日志收集、任务队列 | +| **扇入(Fan-in)** | 多个 channel 合并到一个 | 聚合多个数据源 | +| **扇出(Fan-out)** | 一个 channel 分发给多个消费者 | 并行处理 | +| **超时控制** | 配合 `time.After` 实现超时 | HTTP 请求超时 | +| **退出信号** | 配合 `done` channel 通知退出 | 优雅关闭 | + +```go +// 生产者-消费者模式 +func producer(ch chan<- int) { + for i := 0; i < 100; i++ { + ch <- i + } + close(ch) +} + +func consumer(ch <-chan int) { + for v := range ch { + fmt.Println(v) + } +} +``` + +### 11.3 Context 的作用 + +`context.Context` 是 Go 并发控制的核心,用于在 goroutine 之间传递**取消信号**、**超时**和**请求级值**。 + +| 方法 | 说明 | +|------|------| +| `context.Background()` | 根 Context,通常在 main/init 中使用 | +| `context.WithCancel(parent)` | 返回可手动取消的 Context | +| `context.WithTimeout(parent, d)` | 超时后自动取消 | +| `context.WithDeadline(parent, t)` | 到达指定时间后自动取消 | +| `context.WithValue(parent, k, v)` | 携带请求级键值对 | + +```go +func worker(ctx context.Context) { + for { + select { + case <-ctx.Done(): + fmt.Println("收到取消信号:", ctx.Err()) + return + default: + // 执行业务逻辑 + doWork() + } + } +} + +// 使用 +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() +go worker(ctx) +``` + +!!! warning "Context 使用规范" + - Context 应作为函数的**第一个参数**,命名为 `ctx` + - 不要将 Context 存储在 struct 中,而是显式传递 + - `context.WithValue` 只用于传递请求级元数据(如 traceID),不要传递业务参数 + +--- + +## 12. gin 特性 & 错误传递及处理 + +### 12.1 gin 核心特性 + +| 特性 | 说明 | +|------|------| +| **高性能路由** | 基于 radix tree(压缩前缀树),路由匹配 O(k) 复杂度 | +| **中间件支持** | 链式中间件,支持 `c.Next()` 和 `c.Abort()` 控制流程 | +| **参数绑定** | 自动将请求数据绑定到结构体(JSON、Query、Form、URI) | +| **分组路由** | 路由分组,支持嵌套和中间件复用 | +| **错误管理** | `c.Errors` 收集错误,支持统一错误处理 | + +### 12.2 错误传递与处理 + +gin 提供了 `c.Error()` 方法收集错误,配合 `c.Errors` 统一处理。 + +**错误收集:** + +```go +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + // 收集错误,而不是直接返回 + c.Error(errors.New("missing authorization header")) + c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"}) + return + } + c.Next() + } +} +``` + +**统一错误处理中间件:** + +```go +func ErrorHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Next() + + // 请求处理完成后,统一处理错误 + if len(c.Errors) > 0 { + err := c.Errors.Last().Err + // 根据错误类型返回不同状态码 + switch e := err.(type) { + case *ValidationError: + c.JSON(400, gin.H{"error": e.Message}) + case *NotFoundError: + c.JSON(404, gin.H{"error": e.Message}) + default: + c.JSON(500, gin.H{"error": "internal server error"}) + } + } + } +} +``` + +### 12.3 错误处理最佳实践 + +```go +// 定义业务错误类型 +type AppError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *AppError) Error() string { return e.Message } + +// Handler 中使用 +func GetUser(c *gin.Context) { + id := c.Param("id") + user, err := userService.GetByID(id) + if err != nil { + // 使用 c.Error 收集,由中间件统一处理 + _ = c.Error(&AppError{Code: 404, Message: "user not found"}) + return + } + c.JSON(200, user) +} +``` + +--- + +## 13. AI 编程工具 & 交互过程 + +### 13.1 主流 AI 编程工具 + +| 工具 | 定位 | 核心能力 | +|------|------|---------| +| **Cursor** | AI-first IDE | 内联补全、多文件编辑、上下文感知 | +| **GitHub Copilot** | 代码补全插件 | 实时代码建议、广泛语言支持 | +| **Claude Code** | CLI Agent | 深度代码理解、工具调用、项目级重构 | +| **Windsurf** | AI IDE | Cascade 流式编辑、多步推理 | + +### 13.2 AI 辅助编程交互过程 + +```mermaid +graph TD + U[开发者] -->|描述需求| AI[AI 编程工具] + AI -->|生成代码| R[代码初稿] + R -->|Review| U + U -->|反馈修改| AI + AI -->|迭代优化| R2[改进代码] + R2 -->|测试验证| U + U -->|确认| F[最终代码] +``` + +### 13.3 高效使用 AI 编程的实践 + +| 实践 | 说明 | +|------|------| +| **精准上下文** | 提供项目结构、依赖关系、编码规范,而非泛泛描述 | +| **任务拆解** | 将大任务拆成小的、可验证的子任务 | +| **迭代式开发** | AI 生成初稿 → 人工 Review → 反馈修改 → 再次验收 | +| **Prompt 模板化** | 对高频场景沉淀可复用的 Prompt | +| **验证闭环** | AI 生成的代码必须经过单测、Lint、人工 Review | + +!!! tip "面试中如何回答" + 面试官问 AI 编程工具时,重点不是列举工具名称,而是: + 1. **你用了哪些工具**,在什么场景下用 + 2. **交互过程是什么样的**,如何给 AI 提供上下文 + 3. **提效效果如何**,有没有量化数据 + 4. **遇到过什么问题**,如何保证代码质量 + +--- + +## 面试总结 + +这次奇安信一面考察面较广,涵盖了后端开发的核心知识: + +| 考察维度 | 题目 | 核心考察点 | +|---------|------|-----------| +| **缓存** | 滑动窗口限流、多级缓存、缓存穿透、布隆过滤器 | 缓存系统设计深度 | +| **Redis** | MULTI/Lua、单线程模型、高可用 | Redis 原理和实战 | +| **MySQL** | 引擎对比、幻读、隔离级别、MVCC | 数据库原理理解 | +| **认证** | SAML、OAuth 2.0 | 安全认证体系 | +| **Go 语言** | Channel、Context、gin | Go 工程实践 | +| **AI 工程** | AI 编程工具使用 | AI 辅助开发经验 | + +!!! tip "面试准备建议" + - 缓存和 Redis 是后端面试的高频考点,需要理解原理 + 能写代码 + - MySQL MVCC 需要理解到 ReadView 的可见性判断规则 + - Go 的 Channel 和 Context 是并发编程的核心,需要掌握各种模式 + - AI 编程相关问题需要有实际使用经验,不能只谈理论 diff --git a/mkdocs.yml b/mkdocs.yml index d51d1bd..3b5a1b8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -87,6 +87,7 @@ nav: - 面经: - interview/index.md - 去哪儿 AI 面-AI 全栈方向: interview/qunar-ai-fullstack.md + - 奇安信 AI 全栈-一面: interview/qianxin-ai-fullstack-round1.md - 项目: - project/index.md - Open-API 计费平台: