编程导航面试题话题讨论

面试题

2.4k 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

面试官问我"设计一个秒杀系统",我聊了40分钟

上周去面试一家中厂的后端岗,前两轮技术面都顺利通过了。算法题不难,一道中等难度一道简单,十分钟搞定。项目面也聊得挺好,面试官还夸我"工程经验扎实"。 第三轮是系统设计,面试官是个40岁左右的技术总监,头发花白但眼神很锐利。他看了眼我的简历,开口就问: "设计一个短链接系统,比如bit.ly,支持每天10万次写入、100万次读取,P99延迟要小于50毫秒。你来设计一下。" 我心想这题我刷过啊,LeetCode上有类似的,刚准备画架构图——从ID生成到存储到缓存到HTTP重定向,一套标准答案。 "等等,"他打断我,"你先别画图。你告诉我,如果让你选存储方案,你会选什么?为什么选它而不选别的?" 我愣了一秒。这和题库里不一样。题库里的系统设计题,给你一个场景,你按套路画架构图就行。但这位面试官不让你背答案——他让你做选择,然后解释为什么。 我开始分析:Redis缓存热点数据,MySQL存完整信息,用Snowflake做ID生成。我选了Redis+MySQL的组合方案,理由是读多写少,缓存命中率可以做到95%以上。 他的眼神亮了亮,追问: "缓存穿透怎么办?短链接场景下,大量请求打到不存在的key上,你的MySQL扛得住吗?" 我想了想,说用布隆过滤器先拦截一层。不存在于布隆过滤器的key直接返回404,不进MySQL。他又追问: "布隆过滤器的误判率你怎么控制?数据更新后布隆过滤器怎么同步?如果短链接被删除了但布隆过滤器里还有,怎么办?" 这个问题我确实没想过。布隆过滤器不支持删除操作,这是它的基本特性。我老实说"需要重建或者用Counting Bloom Filter"。他点点头,没追问。 然后他又问了一个更开放的问题: "如果热点数据集中在某几个链接上——比如某个短链接被大V转发了,瞬间涌入100万请求——你怎么处理?" 这一轮聊了40分钟,没有一道题有标准答案。他不是在考我知道多少,是在考我怎么权衡。每一个方案他都会追问代价——性能代价、成本代价、运维复杂度代价。 (顺手推几个技术大厂的[机会](https://jsj.top/f/o38ijj),前、后端or测试,感兴趣就试试 ) 后来他告诉我,2026年的面试趋势变了。不再是"你会不会",而是"你怎么想"。 他说他面试了三十多个候选人,能完整说出CAP定理的占一半,但能解释清楚"为什么Redis不适合做持久化"的不到三成。大部分人都停留在"知道结论"的层面,追问两层就露馅。 他们现在最看重三点: 第一,工程判断力。知道什么时候该用Redis,什么时候该上MySQL,而不是把所有东西都塞进缓存。缓存不是万能药,它有自己的代价。 第二,权衡意识。10万写入和100万读取的压力完全不一样,怎么在延迟、可用性、一致性之间做取舍,比写出某个算法的代码更重要。真实系统没有完美方案,只有最合适的妥协。 第三,开放性思维。他最怕听到"这个场景我没遇到过"就卡住了,反而更欣赏"我没遇到过这个场景,但可以从这几个方向来分析"。思路比答案重要。 回来后我复盘这次面试,发现自己最大的进步不是答对了多少题,而是学会了"先思考再动手"。以前面试遇到系统设计题,上来就画图、写代码、背套路。现在会先花30秒想清楚:这个系统的核心矛盾是什么?面试官想考我什么? 技术面试正在从"你会做题吗"变成"你会设计系统吗"。 这个变化,你准备好了吗?

面经分享

这段时间经历九次面试,通过了6家公司,最后选择了给我日薪最高,离我最近一家公司(宏图物流) 面经在我的博客中,我就不复制过来了 博客地址:http://blog.cognipix.xyz ![image.png](https://pic.code-nav.cn/post_picture/1638948184199864322/K5Sn9n3Ab7FNzmbH.webp)

经典面试题:618大促多渠道同时扣减同一仓库库存,你会怎么做?

这个是我之前面试遇到的一个题,当时回答的很笼统,但是关键点应该都回答了,现在有时间了,我梳理一下这个问题的完善答案,我感觉大家可能也用的上。 经典的问题最能暴漏出来你的设计能力和逻辑思维能力。 ## 一、最大风险是什么? 核心风险就一个:**并发写入导致的库存超卖**。 具体表现: ``` 时刻 T:仓库A某SKU库存剩余 1 渠道1(天猫) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 渠道2(京东) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 渠道3(抖音) 读库存=1 → 判断充足 → 扣减 → 写回库存=0 结果:库存只有1件,卖了3件 → 超卖 ``` 这是一个典型的 **Read-Modify-Write 竞态条件**,本质是"先读后写"缺乏原子性。618场景下 QPS 可能达到万级甚至十万级,并发窗口极小但必然被击穿。 --- ## 二、防超卖的设计方案(分层递进回答) ### 方案1:数据库行锁(兜底防线,必须要有) ```sql -- 核心:用 UPDATE 的行锁保证原子性,而不是先 SELECT 再 UPDATE UPDATE inventory SET stock = stock - #{quantity} WHERE sku_id = #{sku_id} AND warehouse_id = #{warehouse_id} AND stock >= #{quantity}; ``` - 检查 `affected rows`:返回 1 表示扣减成功,返回 0 表示库存不足 - **优点**:简单可靠,数据库 ACID 天然保证正确性 - **缺点**:行锁粒度下,高并发时数据库成为瓶颈 ### 方案2:Redis 预扣减(扛并发的核心) ``` -- Lua脚本保证原子性 local stock = redis.call('GET', KEYS[1]) if not stock or tonumber(stock) < tonumber(ARGV[1]) then return -1 -- 库存不足 end redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 -- 扣减成功 ``` **关键设计**:Redis 做快速预扣,数据库做最终持久化,两者结合。 ``` 用户下单 → Redis预扣(原子Lua) → 创建订单 ↓ 异步MQ → DB扣减(幂等) ``` - Redis 扛住 99% 的高并发流量 - DB 作为最终一致性的落地存储 - **必须保证**:Redis 库存总数 ≤ DB 库存总数(宁可少卖,不可超卖) ### 方案3:库存分桶(解决热点SKU问题) 618 爆品单品可能集中 10 万+ QPS 打在一个 key 上,单点 Redis 也扛不住。 ``` SKU_1001 → bucket_0: 500件 → bucket_1: 500件 → bucket_2: 500件 → bucket_3: 500件 渠道1 → hash(渠道1) % 4 → bucket_0 (隔离竞争) 渠道2 → hash(渠道2) % 4 → bucket_1 渠道3 → hash(渠道3) % 4 → bucket_2 ``` - 按渠道/用户 hash 分桶,将热点打散 - 桶内库存独立扣减,无竞争 - 某桶不足时可以合并/重路由 ### 方案4:渠道配额控制(多渠道协同) ``` 仓库A / SKU_1001 总库存: 10000件 ├── 天猫配额: 4000件 (Redis key: quota:tmall:sku_1001) ├── 京东配额: 3000件 (Redis key: quota:jd:sku_1001) ├── 抖音配额: 2000件 (Redis key: quota:dy:sku_1001) └── 安全余量: 1000件 (兜底池,按需动态分配) ``` - 每个渠道独立扣自己的配额,天然无跨渠道竞争 - 余量池用于动态调配(某渠道卖超预期可以追加) - 配额之和 ≤ 总库存,从根源杜绝超卖 --- ## 三、防少卖的设计要点 少卖的本质是 **预扣不释放**:用户下单后未支付,库存被锁定但未释放。 ``` 下单 → Redis预扣库存 → 等15分钟支付 ↓ 未支付 定时任务归还库存(回滚Redis + DB) ``` 关键机制: | 机制 | 说明 | | ---------------------- | --------------------------------------------------------- | | **支付超时释放** | 下单后 15~30 分钟未支付,自动归还库存 | | **订单取消归还** | 用户主动取消订单,同步归还库存 | | **Redis 库存定期对账** | 定时任务扫描预扣记录,清理孤儿锁 | | **DB 库存最终一致性** | 对账线程周期性对比 Redis 可用库存与 DB 可用库存,修正偏差 | --- ## 四、整体架构总结 ``` ┌─────────────┐ 天猫 ──┐ │ 网关/限流 │ 京东 ──┼─────────►│ (令牌桶) │ 抖音 ──┘ └──────┬──────┘ │ ┌──────▼──────┐ │ 库存服务 │ │ (无状态) │ └──┬───┬───┬──┘ │ │ │ ┌──────────┘ │ └──────────┐ ▼ ▼ ▼ ┌─────────────┐ ┌──────────┐ ┌─────────────┐ │ Redis集群 │ │ MQ队列 │ │ MySQL/DB │ │ (预扣+配额) │ │ (异步落库) │ │ (持久化+对账)│ └─────────────┘ └──────────┘ └─────────────┘ │ │ └─────── 定时对账 ◄───────────┘ ``` --- ## 五、从购物车到出库 — 库存全链路 ### 前提:库存状态拆分 ``` 总库存 (total_stock) = 10000 ├── 可售库存 (available) = 6000 ← 用户能看到的 ├── 购物车锁定 (cart_locked) = 500 ← 加购占位中 ├── 订单预扣 (order_locked) = 2500 ← 已下单未支付 ├── 已付款待发 (paid_pending) = 800 ← 已支付待发货 └── 安全余量 (safety_buffer) = 200 ← 不对外暴露 ``` 每一步流转都是 **从一个状态减、往另一个状态加**,总量不变,可对账。 ### 第一步:加入购物车 用户点"加入购物车"时,需要校验库存并做一次**轻量锁定**。 ``` 用户A 加购 SKU_1001 × 2件 │ ▼ GET /cart/add │ ├── 1. 读Redis可用库存,判断是否充足 ├── 2. 充足 → DECRBY 可售库存,INCRBY 购物车锁定库存 └── 3. 返回加购成功 ``` 关键设计: ```lua -- Redis Lua 原子操作 local available = redis.call('GET', 'stock:available:sku_1001') if tonumber(available) < tonumber(ARGV[1]) then return -1 end redis.call('DECRBY', 'stock:available:sku_1001', ARGV[1]) redis.call('INCRBY', 'stock:cart_locked:sku_1001', ARGV[1]) redis.call('SET', 'cart:lock:{userId}:sku_1001', ARGV[1], 'EX', 1800) -- 30分钟过期,超时自动归还 return 1 ``` 核心要点: - 购物车锁定有时效(一般 30 分钟),超时自动释放回可售库存 - 锁定粒度到用户+SKU,用 key 过期机制做自动归还,不依赖定时任务 - 加购环节**不碰数据库**,纯 Redis 操作,保证性能 ### 第二步:提交订单(下单) 用户点"去结算"→"提交订单",把购物车锁定正式转为订单预扣。 ``` 用户A 提交订单,SKU_1001 × 2件 │ ▼ POST /order/create │ ├── 1. 校验购物车锁定记录是否存在(防伪造) ├── 2. 购物车锁定 → 订单预扣(状态流转) ├── 3. 写入订单表(状态:待支付) ├── 4. 发MQ消息,异步落DB库存流水 └── 5. 返回订单号 + 支付倒计时(15分钟) ``` 关键设计: ```lua -- Redis 状态流转(原子) local cartLock = redis.call('GET', 'cart:lock:{userId}:sku_1001') if not cartLock then return -1 -- 购物车锁已过期,需要重新加购 end -- 购物车锁定 → 订单预扣 redis.call('DECRBY', 'stock:cart_locked:sku_1001', ARGV[1]) redis.call('INCRBY', 'stock:order_locked:sku_1001', ARGV[1]) redis.call('DEL', 'cart:lock:{userId}:sku_1001') redis.call('SET', 'order:lock:{orderId}:sku_1001', ARGV[1], 'EX', 900) -- 15分钟支付超时 return 1 ``` 需要处理的异常场景: | 异常 | 处理方式 | | ----------------------- | ---------------------------------------------- | | 购物车锁已过期 | 提示用户"库存变动,请重新加购",引导回到购物车 | | 提交订单途中 Redis 超时 | 订单表记录状态为"创建中",定时任务扫描补偿 | ### 第三步:用户支付 支付回调到达后,订单预扣 → 已付款待发。 ``` 支付回调到达 │ ▼ POST /pay/callback │ ├── 1. 校验支付结果(防伪造回调) ├── 2. 幂等校验(同一笔订单不重复处理) ├── 3. Redis: 订单预扣 → 已付款待发 ├── 4. 更新订单状态 → 已支付 ├── 5. MQ异步落DB库存 + 订单流水 └── 6. 通知仓储系统发货 ``` ```lua -- Redis 状态流转 local orderLock = redis.call('GET', 'order:lock:{orderId}:sku_1001') if not orderLock then -- 可能已超时释放,但用户实际付了款 -- 进入异常流程:尝试从可用库存补扣 local available = redis.call('GET', 'stock:available:sku_1001') if tonumber(available) >= tonumber(ARGV[1]) then redis.call('DECRBY', 'stock:available:sku_1001', ARGV[1]) else return -1 -- 真的没库存了,触发退款 end else redis.call('DECRBY', 'stock:order_locked:sku_1001', ARGV[1]) redis.call('DEL', 'order:lock:{orderId}:sku_1001') end redis.call('INCRBY', 'stock:paid_pending:sku_1001', ARGV[1]) return 1 ``` 关键异常:用户在 15 分钟最后几秒支付,但订单锁刚好过期被归还了。处理策略: - 优先从可售库存补扣(一般归还后还没被别人抢走) - 补扣失败 → 触发自动退款 + 补偿券(用户体验兜底) - **宁可退款不少卖**,这是底线 ### 第四步:发货出库 仓储确认发出后,已付款待发 → 扣减总库存(真实物理库存减少)。 ``` 仓储系统回传发货成功 │ ▼ POST /warehouse/ship/callback │ ├── 1. Redis: 已付款待发 → 总库存扣减 ├── 2. DB: 扣减 total_stock(最终落库) ├── 3. 更新订单状态 → 已发货 └── 4. 记录库存流水明细 ``` 这一步库存变化落地到 DB,是**最终一致性的锚点**。 --- ## 六、异常兜底:定时对账 以上全链路中 Redis 和 DB 存在短暂不一致,必须靠对账兜底: ``` ┌─────────────────────────────────────────────────────┐ │ 定时对账任务(每分钟) │ │ │ │ 1. Redis各状态求和 vs DB total_stock │ │ 不一致 → 以DB为准,修正Redis │ │ │ │ 2. 扫描过期购物车锁(二次保障,不依赖TTL) │ │ 有残留 → 归还available │ │ │ │ 3. 扫描超时未支付订单 │ │ 有残留 → 归还available + 关闭订单 │ │ │ │ 4. 扫描"创建中"状态的僵尸订单 │ │ 超时未确认 → 回滚所有预留库存 │ └─────────────────────────────────────────────────────┘ ``` --- ## 七、全链路库存流转总览 ``` 可售库存(available) │ │ ①加购 ▼ 购物车锁定(cart_locked) ──超时30min──→ 归还available │ │ ②下单 ▼ 订单预扣(order_locked) ──超时15min──→ 归还available │ │ ③支付 ▼ 已付款待发(paid_pending) │ │ ④发货 ▼ 总库存扣减(DB total_stock 真实减少) ``` **面试总结一句话**:库存不是一步扣完的,而是通过**状态拆分 + 逐步流转 + 超时自动归还 + 定时对账**,在保证高并发性能的同时,实现不超卖不少卖的最终一致性。

Prompt调优是什么

也就是 提示词工程的一部分内容 ### 区别对比 Prompt = 你发给 AI 的指令、提问、上下文文本(就是你跟 AI 说的所有话)。 Prompt 调优:反复修改、优化给 AI 的输入指令,让大模型输出精准、稳定、符合预期结果的整套优化方法。 类比: 你让厨师做菜,随口说 “炒个肉”(原始差 Prompt); 调优后:“瘦肉切薄片、少油、微辣、少盐、配青椒,不要勾芡,出锅放蒜末”(优化后 Prompt)。 调优就是不断打磨这句 “做菜要求”,让 AI 每次都交出合格成品。 ## 核心目的 解决大模型常见问题: 答非所问、跑偏主题 输出格式混乱(想要表格却给大段文字) 逻辑不严谨、细节缺失 回答忽好忽坏、结果不稳定 不懂你的角色 / 场景需求 ## 主要注意五个方向 ### 1. 基础结构优化(最常用) 固定万能模板框架,把零散提问补全: 角色 + 任务 + 约束条件 + 输出格式 + 示例 ### 2. 少样本调优 Few-shot 在 Prompt 里塞入1~5 组标准答案示例,AI 会复刻逻辑、格式、文风。 例:你要 AI 做分类,直接放 3 条 “输入→分类结果” 样板,精度大幅提升。 ### 3. 思维链 CoT 调优 在指令里强制 AI 分步思考: 回答前先拆解问题,一步步推导,最后给出结论 适合数学计算、逻辑推理、数据分析类任务。 ### 4. 约束与边界限定调优 增加限制词,杜绝 AI 乱发挥: 字数限制:回答不超过 300 字 立场限制:客观中立,不主观评价 禁止内容:不要解释原理,只给方案,不要废话 ### 5. 负面 Prompt 调优(画图 / 生成类高频) 专门告诉 AI不要输出什么,规避错误、低质量内容。 例绘画:模糊、畸形、水印、多余肢体、低分辨率。 ## 使用场景: 什么时候必须做 Prompt 调优? 批量自动化生成内容(周报、文案、代码、报表) 对接 AI 工具、知识库 RAG、企业 AI 机器人 对输出格式、准确性、稳定性有硬性要求 不想花钱训练专属模型,低成本提升 AI 效果

面试官皱眉:“你懂RAG测评吗?” 我:“何止懂?我直接用Ragas把系统的幻觉率干到了0”,他愣了…

## 1. 什么是Rag评估? RAG 评估,就是用一套可量化的指标体系,持续测量 RAG 系统「回答得好不好」,并且能把「好不好」这个笼统的感受,拆解成具体是哪个环节出了问题。 目前主流的RAG评估体系通常分为以下三个核心维度: - 检索质量 (Retrieval Quality) 主要看检索模块有没有把真正相关的文档找出来。如果这一步做不好,后续大模型再生成得再好也是“垃圾进,垃圾出”。常用的指标有上下文召回率,上下文精准率等。 - 生成质量 (Generation Quality) 主要评估大模型拿到上下文后回答得好不好。这里有两个最关键的指标: _ 忠实度/防幻觉 (Faithfulness):检测模型生成的答案是否严格基于检索到的上下文,有没有自己瞎编乱造(即产生幻觉)。 _ 答案相关性 (Answer Relevance):检测生成的答案有没有正面、准确地回应用户的提问。\* AnswerAccuracy(答案准确性): 根据给定的 user_input,衡量答案与真实答案的准确性。此指标平均两个不同的评判提示来进行评估。 ## 2. 为什么需要对Rag做评测? 1. 防止它一本正经地胡说八道(防幻觉) RAG 最怕的就是瞎编。如果不评测,你根本不知道它是真的查到了资料在回答,还是在凭印象乱编。评测能帮你揪出那些“看着很对,其实是错的”答案。 2. 方便出了问题知道该骂谁(找病灶) RAG 分两步:先“翻书”(检索),再“答题”(生成)。 如果答错了,到底是“书没找对”,还是“脑子不好使”?有了评测,你就能一眼看出来是该去优化找资料的环节,还是该去换个大模型。 3. 别光靠感觉,要有实锤(看疗效) 改完代码后,系统到底变聪明了还是变笨了?不能光凭肉眼看几个例子。评测能给出一堆客观的分数,让你明明白白地知道这次优化有没有效果。 一句话总结:不做评测就是蒙眼狂奔,做了评测才能心里有底。 ## 3. 如何给Rag做评测? Rag核心测评指标如下: ![](https://pic.code-nav.cn/post_picture/1935697882688061442/eZ14qP4CtIaxF9Hu.webp) 根据评估对象的不同,可以分为以下三个核心的评估指标: - 检索资料(Context)和用户问题(Query)——上下文相关性(Context Relevance) - 模型生成的回答 (Response) 与 检索到的资料 (Context)——忠实度 / 依据性(Faithfulness / Groundedness) - 模型生成的回答 (Response) 与 用户提问 (Query)——答案相关性(Answer Relevance) > 现在市面上有很多给Rag做评测的框架:TruLens、 RAGAS(RAG首选)、DeepEval(Python单测集成)、Phoenix 等 注:本文主要使用rags(https://docs.ragas.org.cn/)框架做演示)) ### 3.1 如何构建评测集? **基于LLM生成** 基于LLM生成的核心逻辑是,让大模型阅读你的“知识文档”来自动出题。这种方式可生成: - 原始问题、 - 标准答案(作为评估基准的groud truth)。 **人工构建** 人工构建指的是,从已有的知识中,人工的构建一些Q&A对,一般是由专业的人员基于文档内容、历史用户问题等提炼出来的常见的问题和答案。这种方式构建出来的数据也是包含: - 原始问题 - 真实答案 **线上指标** rag系统上线以后根据用户的追问率,踩率,转人工率,空回答率等真实问题作为评测 > 接下介绍如何基于文档生成测评集,人工构建和线上指标 相对简单,仅操作复杂 ### 3.2 基于文档生成测评集 比如我们提前准备一份文档,markdown格式最好,放到docs目录下 ![](https://pic.code-nav.cn/post_picture/1935697882688061442/gVuEldsumW5xiuIz.png) 运行以下代码: ```python import os from dotenv import load_dotenv from langchain_community.document_loaders import DirectoryLoader from openai import OpenAI from ragas.embeddings.base import embedding_factory from ragas.llms import llm_factory from ragas.testset import TestsetGenerator from ragas.testset.persona import Persona # 1. 加载环境变量 load_dotenv() # 2. 加载文档 path = "docs/" loader = DirectoryLoader(path, glob="**/*.md") docs = loader.load() # 3. 配置 LLM openai_client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", ) llm = llm_factory("qwen3.7-max", client=openai_client) # 4. 配置 Embeddings embeddings = embedding_factory("openai", model="text-embedding-v4", client=openai_client) # 5. 配置角色(因为文档是关于 时刻表及航班日期变更、行李政策相关的,所以角色设定为以下 ) #角色提供了上下文和视角,确保生成的查询自然、针对特定用户且多样化。通过根据不同用户视角定制查询,我们的测试集覆盖了广泛的场景 personas = [ Persona( name="首次乘机旅客", role_description="第一次乘坐飞机,可能会感到焦虑。需要清晰的飞行流程指引、安全协议说明以及整个旅程的预期指导。", ), Persona( name="常旅客", role_description="经常出行,重视效率和舒适度。对忠诚计划、快速通道服务和无缝旅行体验感兴趣。", ), Persona( name="愤怒的商务舱旅客", role_description="要求顶级服务,对任何延误或问题都容易感到恼火。期望立即解决问题,如果标准未达到会迅速表达不满。", ), ] generator = TestsetGenerator( llm=llm, embedding_model=embeddings, persona_list=personas, ) dataset = generator.generate_with_langchain_docs(docs, testset_size=10) df = dataset.to_pandas() print(df) df.to_csv("rag_testset.csv", index=False) print("测试集已成功导出为 rag_testset.csv") ``` 评测集生成如下: ![](https://pic.code-nav.cn/post_picture/1935697882688061442/cPeXNlzvv451noJL.webp) 不管是针对Context Precision、Context Recall两个指标做评测,还是要针对另外几个指标做评测,我们还需要基于这份数据集,生成我们自己的RAG问答系统的答案+参考资料才行。 ### 3.3 构建一个简答的rag系统并做评测 ```python import os import asyncio from dotenv import load_dotenv from langchain_core.documents import Document from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_text_splitters import MarkdownTextSplitter load_dotenv() #一:构建一个简单的rag系统 # 读取知识库文档 with open("docs/知识库.md", "r", encoding="utf-8") as f: knowledge_base_content = f.read() # 使用 LangChain 的 Markdown 文本分割器切割(本例子使用Markdown 切割器仅供参考) markdown_splitter = MarkdownTextSplitter( chunk_size=600, # 每个分块的最大字符数 chunk_overlap=50 # 分块之间的重叠字符数 ) # 分割文档 chunks = markdown_splitter.split_text(knowledge_base_content) # 创建 LangChain 文档对象 langchain_documents = [ Document(page_content=chunk) for chunk in chunks if chunk.strip() ] print(f"文档被分割成 {len(langchain_documents)} 个块") from langchain_core.vectorstores import InMemoryVectorStore import openai # 创建 LangChain 的 OpenAI 客户端 openai_client = openai.OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1" ) # 创建 LangChain 的 LLM 和 Embeddings llm = ChatOpenAI(model="qwen-max", api_key=os.getenv("OPENAI_API_KEY"),base_url="https://dashscope.aliyuncs.com/compatible-mode/v1") embeddings = OpenAIEmbeddings(api_key=os.getenv("OPENAI_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", model="text-embedding-v4" , check_embedding_ctx_length=False) vector_store = InMemoryVectorStore(embeddings) # 分批添加文档到向量存储(每批最多10个,避免API限制) batch_size = 10 for i in range(0, len(langchain_documents), batch_size): batch = langchain_documents[i:i + batch_size] vector_store.add_documents(batch) print(f"已添加批次 {i // batch_size + 1}, 当前共 {min(i + batch_size, len(langchain_documents))} 个文档") # 创建向量存储的检索器 retriever = vector_store.as_retriever(search_kwargs={"k": 5}) #提示词模板 template = """你是一个严格的客服助手。请仅根据以下提供的上下文信息回答问题。 重要规则: 1. 只能使用上下文中明确提到的信息 2. 如果上下文中没有足够信息,请说"根据提供的信息无法回答" 3. 不要添加任何上下文中不存在的内容 4. 不要使用你自己的知识 上下文: {context} 问题:{query} 回答:""" prompt = ChatPromptTemplate.from_template(template) qa_chain = prompt | llm | StrOutputParser() def format_docs(relevant_docs): return "\n".join(doc.page_content for doc in relevant_docs) #测评集问题 #用户问题根据上一步生成的测评集取得的 sample_queries = [ "Ragas航空公司 改 时间 咋办 怕", "作为Ragas航空公司的一名商务舱旅客,我对航班时刻表的更改感到极其不满,如果我拒绝所有替代航班并决定申请全额退款,具体的操作步骤是什么,且退款至我的信用卡需要多长时间?", "作为一个经常到处飞的常旅客我就是想问下如果我之前买那种不可退票的机票然后我现在想要改的话是不是说我可以得到那个旅行积分的啊,这个旅行积分到底是怎么一回事能不能给我说说?", "我头一回坐飞机心里慌得很要是我买Ragas航空公司的机票怀疑时间变了但是没收到通知咋整啊是不是被骗了那个什么管理我的预订怎么弄还有垃圾邮件在哪看客服电话能打吗你快给我说说步骤吧我啥都不懂", "我买是商务舱票,为啥行李限额显示跟经济舱一样只有1个包?你们是不是弄错,快点给我解决这问题!", "首飞好紧张,怎么登路管理我的预订改期和查行李?", '作为一名支付了高昂票价的商务舱旅客,我对任何潜在的延误都感到极度恼火,现在我需要立刻通过"管理我的预订"系统来更改我的航班日期,请详细列出我必须遵循的具体操作步骤,并且明确告诉我,作为尊贵的商务舱旅客,我的托运行李限额究竟是多少,以确保我不会在机场遭遇任何不必要的麻烦或额外费用?', "我那个航班日期想改但不知道票行不行,万一没收到通知,管理我的预订里面能查吗,还有网站报错弄不了咋办啊?", '第一次坐飞机 怎么在"管理我的预订"里改签航班 没收到通知或网页报错怎么办', '如何通过"管理我的预订"功能查看行李限额与航班变更?', '我是一名尚务舱旅客,我的航班好像被你们偷偷改了,而且我根本没收到通之!我现在非常生气,立刻告诉我怎么通过"管理我的预订"来查看我的航班更新,顺便给我确任一下我作为尚务舱乘客到底能带几个多重的托运行里,要是我的行里因为你们的错务被收费,我绝对会投述到底的!' ] #测评集期待答案根据上一步生成的测评集取得的 expected_responses = [ "当Ragas航空公司因运营原因(如天气状况、飞机维护或机场限制)更改起飞时间、到达时间或航班号时,会发生时刻变更。此外,乘客可因个人原因申请更改航班日期,具体情况取决于票价规则和可用性。", '若要申请全额退款,您需要在"管理我的预订"中点击"取消航班",然后选择"申请全额退款"。如果您使用信用卡或借记卡付款,退款处理时间为7个工作日;若为银行转账,则需要20个工作日。', "根据票价规则,对于不可退票的机票,在修改车票时,您可以支付修改费或获得旅行积分。", '如果您怀疑航班已更改但未收到任何通知,请按照以下步骤操作:第一步,登录"管理我的预订",查看是否有任何更新;第二步,检查你的垃圾邮件/垃圾邮件文件夹,看看有没有错过的邮件;第三步,请致电Ragas航空公司客服,询问您的航班状态。', '根据规定,经济舱的托运行李限额是1个包(最大:23公斤),而商务舱的托运行李限额是2个包(最多32公斤)。行李限额根据票价类型和目的地而异,请在预订确认邮件中查看具体的津贴,或登录"管理我的预订"进行核实。', '别紧张,您可以登录"管理我的预订"(输入预订参考号码和姓氏)来完成:1. 改期:查看票价规则,选择"更改航班"及新日期并支付费用;2. 查行李:查看根据您的票价类型和目的地而定的具体行李限额。', '要通过"管理我的预订"更改航班日期,您首先需要登录系统,输入您的预订参考号码和姓氏以查看票价规则;接着选择"更改航班"并选择新的航班日期(视可用性而定),支付任何适用的差价或改乘费用后,点击"确认新航班"即可获得新机票。此外,作为商务舱旅客,您的托运行李限额为2个包,每个包最多可达32公斤。请务必确保您的行李符合航空公司的尺寸、重量和内容限制,以避免安检延误或产生额外的行李费用。', '要确认机票是否允许更改,请登录"管理我的预订",输入预订参考号码和姓氏以查看票价规则。可退票可免费改签,不可退票需支付修改费或获得旅行积分,基础经济舱或促销机票不允许修改。若未收到航班变更通知,您可以登录"管理我的预订"查看更新,检查垃圾邮件文件夹,或致电Ragas航空公司客服。若遇到网站报错等系统问题,请尝试更换浏览器、清除缓存和Cookie,或切换至其他设备;如问题依旧,请致电客服并提供预订参考号及错误信息截图。', '作为首次乘机旅客,您可以按照以下指南操作:\n1. 更改航班日期:登录"管理我的预订",输入预订参考号码和姓氏查看票价规则。接着选择"更改航班",挑选新的航班日期,支付适用的差价或改乘费用,最后点击"确认新航班"。\n2. 未收到变更通知:若未收到通知,请先登录"管理我的预订"查看是否有更新,并检查垃圾邮件文件夹。若仍无信息,请致电Ragas航空公司客服询问航班状态。\n3. 网页报错等系统问题:若遇到网站错误,请尝试更换浏览器、清除缓存和Cookie,或切换到手机、平板等其他设备。若问题依旧,请致电客服并提供您的预订参考号和错误信息截图。', '您可以登录"管理我的预订"查看具体的行李津贴限额;若未收到航班变更通知,也可通过该功能查看航班状态是否有任何更新。', '如果您怀疑航班已更改但未收到任何通知,第一步是登录"管理我的预订"查看是否有任何更新。关于您的行李限额,作为商务舱乘客,您的托运行李限额为2个包(最多32公斤)。请注意,如果行李超过允许的重量或行李数量,将收取行李费用。' ] from ragas import EvaluationDataset # 异步处理单个查询 async def process_query(query, reference): #根据问题去rag进行检索 relevant_docs = await retriever.ainvoke(query) #调用大模型,根据用户问题和检索到的文档生成答案 response = await qa_chain.ainvoke({"context": format_docs(relevant_docs), "query": query}) return { "user_input": query, "retrieved_contexts": [rdoc.page_content for rdoc in relevant_docs], "response": response, "reference": reference, } # 并发执行所有查询 async def process_all_queries(): tasks = [process_query(q, r) for q, r in zip(sample_queries, expected_responses)] results = await asyncio.gather(*tasks) return list(results) # 运行异步任务 dataset = asyncio.run(process_all_queries()) print(f"已完成 {len(dataset)} 个查询的处理") # 创建评估数据集 evaluation_dataset = EvaluationDataset.from_list(dataset) from ragas import evaluate from ragas.llms import LangchainLLMWrapper from ragas.metrics import LLMContextRecall, Faithfulness, FactualCorrectness,AnswerAccuracy,ContextRelevance,ResponseRelevancy # LLMContextRecall(LLM上下文召回): 衡量的是有多少相关文档被成功检索了出来。它的核心关注点在于不要遗漏重要的结果。 # ContextRelevance(llm上下文精确率): 根据用户输入对检索到的上下文的相关性进行评分。 # 输入:data:包含键 user_input, retrieved_contexts 的字典列表 输出:0.0:retrieved_contexts 与 user_input 不相关 0.5:retrieved_contexts 与 user_input 部分相关 1.0:retrieved_contexts 与 user_input 完全相关 # AnswerAccuracy(答案准确性): 根据给定的 user_input,衡量答案与真实答案的准确性。此指标平均两个不同的评判提示来进行评估。 # Faithfulness(忠诚度): 判断AI 生成的 response(回答)在事实层面与 retrieved context(检索到的上下文)的一致性。 # ResponseRelevancy (答案相关性): 根据给定的问题对答案的相关性进行评分。包含不完整、冗余或不必要信息的答案会受到惩罚。分数范围从 0 到 1,1 为最佳。 evaluator_llm = LangchainLLMWrapper(llm) # 评估 result = evaluate( dataset=evaluation_dataset, metrics=[LLMContextRecall(), ContextRelevance(),Faithfulness(), ResponseRelevancy(),AnswerAccuracy()], llm=evaluator_llm, embeddings=embeddings, ) print(result) ``` **测评集结果** {'context_recall': 0.7803, 'nv_context_relevance': 0.9545, 'faithfulness': 0.5597, 'answer_relevancy': 0.7836, 'nv_accuracy': 0.7273} 问题:faithfulness': 0.5597 忠实度得分较低 原因:1. 检索质量不足 2.关键信息分散在多个 chunk 中,但只检索到部分 3 提示词约束不够强 修改:1、提示词修改为现在的。原先的:根据以下参考资料生成答案 2、提高召回的分片 3->5 再次测评: {'context_recall': 0.9333, 'nv_context_relevance': 0.9286, 'faithfulness': 0.8889, 'answer_relevancy': 0.7817, 'nv_accuracy': 0.7222} 忠实度明显提高,本次测评仅提供问题解决思路。具体问题应根据具体业务场景和测评结果

Java 后端面试题(二)

# Java 后端核心面试题 --- ## 一、设计题 ### Q1:从数据库同步导出 1000 万条用户数据成 Excel 文件,请给出完整设计解决方案 **核心挑战**:千万级数据一次性加载会 OOM,Excel 单个 Sheet 最多约 104 万行,需考虑内存、性能、文件大小。 **方案设计**: #### 1. 数据读取:分页流式读取 - 使用 **游标分页**(基于主键 ID)而非 offset 分页,避免深分页性能问题 ```sql SELECT * FROM user WHERE id > #{lastId} ORDER BY id LIMIT 10000 ``` - 每次取 10000 条,分批处理,避免一次性加载全部数据进内存 #### 2. Excel 写入:流式写入 - 使用 **Apache POI SXSSFWorkbook**(Streaming Workbook),内存中只保留固定行数(如 100 行),其余刷到磁盘临时文件 - 或用 **EasyExcel**(阿里开源),更简单,专为大文件设计: ```java EasyExcel.write(fileName, UserVO.class) .sheet("用户数据") .doWrite(dataList); ``` - 数据量大时拆分为多个 Sheet 或多个文件(如每 100 万行一个文件) #### 3. 异步 + 进度通知 - 接口收到请求后立即返回"任务已提交",后台异步执行 - 用消息队列(或线程池 + Future)异步处理导出任务 - 导出完成后通知用户下载(站内信 / 邮件 / WebSocket 推送下载链接) #### 4. 完整流程 ``` 用户请求 → Controller 提交异步任务 → 返回"处理中" → 异步线程分页查询 + 流式写入 Excel → 上传到 OSS/MinIO → 通知用户下载链接 ``` #### 5. 可优化点 - 并行分页查询 + 多 Sheet 并行写入(注意分页有序性) - 如果数据库是从库,注意主从延迟,建议读主库或强制走主库 - 超大文件考虑压缩(zip)后提供下载 --- ### Q2:如何防止重复请求? #### 前端防重 | 方案 | 说明 | |------|------| | **按钮禁用 + loading** | 点击后立即 `disabled = true`,显示 loading 状态,接口返回后恢复 | | **防抖(debounce)** | 搜索类输入,用户停止输入 N 毫秒后再发请求 | | **节流(throttle)** | 滚动加载等高频场景,固定间隔只发一次 | | **requestId 去重** | 每次请求生成唯一 requestId,相同 requestId 的请求拦截 | | **路由切换取消请求** | 组件卸载时 AbortController 取消 pending 请求 | #### 后端防重 | 方案 | 说明 | |------|------| | **幂等设计** | 业务本身支持幂等(如 INSERT 前先查是否存在),重复请求返回相同结果 | | **唯一索引** | 数据库唯一约束,重复数据直接报错 | | **Token 机制** | 提交表单前先获取 token,提交时校验并删除 token(一次性) | | **分布式锁** | Redis `SETNX` 锁住"用户ID + 业务标识",处理完释放 | | **AOP + 注解** | 自定义 `@RepeatSubmit` 注解,切面校验参数签名/时间窗口内是否重复 | | **消息队列** | 请求入队时根据业务 key 去重(RocketMQ 支持消息去重) | **通用推荐**:前端按钮防重点 + 后端 Token 机制(简单有效)+ 业务幂等设计(根本解决)。 --- ### Q3:MySQL 索引的使用原则是什么?索引是不是越多越好?有什么弊端? #### 索引使用原则 1. **最左前缀原则**:联合索引 `(a, b, c)` 只有查询条件从最左列开始才能命中,如 `WHERE a=1` ✅、`WHERE a=1 AND b=2` ✅、`WHERE b=2` ❌ 2. **选择区分度高的列**:如用户 ID、订单号,性别这种只有 2-3 个值的列不适合 3. **WHERE、JOIN、ORDER BY、GROUP BY 涉及的列建索引** 4. **覆盖索引**:查询列都在索引中,不需要回表,性能最优 5. **避免在索引列上使用函数或计算**:`WHERE DATE(create_time) = '2024-01-01'` ❌,应改为范围查询 6. **负向查询不命中索引**:`!=`、`NOT IN`、`NOT EXISTS` 通常不走索引 7. **模糊查询前缀匹配**:`LIKE 'abc%'` ✅ 走索引,`LIKE '%abc'` ❌ 不走 #### 索引不是越多越好,弊端如下: | 弊端 | 说明 | |------|------| | **写入性能下降** | INSERT/UPDATE/DELETE 需要同步维护所有索引,索引越多越慢 | | **占用磁盘空间** | 每个索引都是一个 B+ 树,数据量大时索引本身占用可观的磁盘 | | **优化器选择困难** | 索引太多可能导致优化器选错索引,反而变慢 | | **内存占用** | 索引页加载到 buffer pool,挤占数据页的缓存空间 | --- ### Q4:用 Stream API 对 List 做过滤、计算、排序、打印 ```java List<User> users = Arrays.asList( new User("张三", 25, 8000), new User("李四", 30, 12000), new User("王五", 22, 6000), new User("赵六", 28, 15000) ); // 过滤:年龄 > 24 List<User> filtered = users.stream() .filter(u -> u.getAge() > 24) .collect(Collectors.toList()); // 计算:工资总和 int totalSalary = users.stream() .mapToInt(User::getSalary) .sum(); // 计算:平均工资 double avgSalary = users.stream() .mapToInt(User::getSalary) .average() .orElse(0); // 排序:按年龄升序 List<User> sorted = users.stream() .sorted(Comparator.comparingInt(User::getAge)) .collect(Collectors.toList()); // 排序:按工资降序 List<User> sortedBySalary = users.stream() .sorted(Comparator.comparingInt(User::getSalary).reversed()) .collect(Collectors.toList()); // 分组:按年龄分组 Map<Integer, List<User>> groupByAge = users.stream() .collect(Collectors.groupingBy(User::getAge)); // 最大值 User maxSalaryUser = users.stream() .max(Comparator.comparingInt(User::getSalary)) .orElse(null); // 遍历打印 users.stream() .sorted(Comparator.comparingInt(User::getAge)) .forEach(u -> System.out.println(u.getName() + ": " + u.getSalary())); ``` > **常用 API 速查**: > - 中间操作:`filter`、`map`、`flatMap`、`sorted`、`distinct`、`limit`、`skip`、`peek` > - 终端操作:`collect`、`forEach`、`reduce`、`count`、`anyMatch`、`allMatch`、`findFirst`、`max`/`min` > - Collectors:`toList()`、`toMap()`、`groupingBy()`、`joining()`、`summarizingInt()` --- ## 二、算法题:树结构 > 建议系统学习:二叉树遍历(前中后序递归+迭代)、层序遍历(BFS)、二叉搜索树、平衡二叉树(AVL)、深度/高度计算、路径问题。 ### 常见树题型速查 #### 1. 二叉树遍历 ```java // 前序遍历:根 → 左 → 右 void preOrder(TreeNode root) { if (root == null) return; System.out.println(root.val); preOrder(root.left); preOrder(root.right); } // 中序遍历:左 → 根 → 右(BST 中序遍历结果有序) void inOrder(TreeNode root) { if (root == null) return; inOrder(root.left); System.out.println(root.val); inOrder(root.right); } // 后序遍历:左 → 右 → 根 void postOrder(TreeNode root) { if (root == null) return; postOrder(root.left); postOrder(root.right); System.out.println(root.val); } // 层序遍历(BFS) List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> res = new ArrayList<>(); if (root == null) return res; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); List<Integer> level = new ArrayList<>(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); level.add(node.val); if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } res.add(level); } return res; } ``` #### 2. 二叉树最大深度 ```java int maxDepth(TreeNode root) { if (root == null) return 0; return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; } ``` #### 3. 翻转二叉树 ```java TreeNode invertTree(TreeNode root) { if (root == null) return null; TreeNode left = invertTree(root.left); TreeNode right = invertTree(root.right); root.left = right; root.right = left; return root; } ``` #### 4. 判断对称二叉树 ```java boolean isSymmetric(TreeNode root) { return check(root, root); } boolean check(TreeNode p, TreeNode q) { if (p == null && q == null) return true; if (p == null || q == null) return false; return p.val == q.val && check(p.left, q.right) && check(p.right, q.left); } ``` #### 5. 路径总和 ```java boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) return false; if (root.left == null && root.right == null) { return root.val == targetSum; } return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); } ``` --- ## 三、MySQL 面试题 ### Q5:MySQL 查询没命中索引时是什么锁? - 在 **RC(读已提交)** 隔离级别下:只锁住实际扫描到的行,不匹配的行会立即释放锁 - 在 **RR(可重复读)** 隔离级别下(MySQL 默认): - 如果走了索引,行锁锁住索引记录 + Gap Lock - 如果 **没走索引**,行锁退化为 **表锁**(所有记录加锁 + 全部 Gap),导致大面积锁等待,极易死锁 > **结论**:RR 级别下没命中索引非常危险,update/delete 会锁全表,务必让 SQL 走索引。 --- ### Q6:讲讲事务隔离级别 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 | |----------|------|-----------|------|----------| | **READ UNCOMMITTED** | ❌ 会 | ❌ 会 | ❌ 会 | 不加锁 | | **READ COMMITTED** | ✅ 解决 | ❌ 会 | ❌ 会 | 快照读(每次读最新快照) | | **REPEATABLE READ**(默认) | ✅ 解决 | ✅ 解决 | ⚠️ 部分解决 | 一致性快照 + Gap Lock | | **SERIALIZABLE** | ✅ 解决 | ✅ 解决 | ✅ 解决 | 所有读加共享锁 | **概念解释**: - **脏读**:读到其他事务未提交的修改 - **不可重复读**:同一事务内两次读取同一行,结果不同(其他事务 UPDATE 了) - **幻读**:同一事务内两次范围查询,记录数不同(其他事务 INSERT 了) **RR 如何解决幻读**:使用 Next-Key Lock(行锁 + Gap Lock),锁定索引记录之间的间隙,阻止其他事务插入。 --- ### Q7:SQL 执行很慢,怎么排查? **排查流程**: 1. **`EXPLAIN` 分析执行计划** - `type`:连接类型,`ALL`(全表扫描)最差,`ref`/`eq_ref` 较好 - `key`:实际使用的索引,`NULL` 表示没用索引 - `rows`:预估扫描行数 - `Extra`:`Using filesort`、`Using temporary` 需关注 2. **检查是否命中索引** - 索引列上是否有函数/计算? - 是否满足最左前缀? - 类型是否隐式转换?(如 `varchar` 字段用 `int` 类型查) 3. **慢查询日志** ```sql SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; ``` 分析慢查询日志找出问题 SQL 4. **锁等待** ```sql SHOW PROCESSLIST; -- 查看当前连接 SELECT * FROM information_schema.INNODB_TRX; -- 当前事务 SELECT * FROM information_schema.INNODB_LOCKS;-- 锁信息 SHOW ENGINE INNODB STATUS; -- InnoDB 状态 ``` 5. **数据量大但索引没问题** - 考虑分页是否深分页(`LIMIT 1000000, 10`),改用游标分页 - 检查返回数据量是否过大,考虑分批 - 考虑读写分离,把慢查询分流到从库 6. **其他方向** - 检查磁盘 IO、CPU 是否瓶颈 - 大字段(TEXT/BLOB)是否影响 - 表设计是否合理(是否需分库分表) --- ## 四、Redis 面试题 ### Q8:Redis 淘汰策略 | 策略 | 说明 | |------|------| | **noeviction** | 不淘汰,内存满时写入报错(默认) | | **allkeys-lru** | 所有 key 中 LRU(最近最少使用)淘汰 | | **volatile-lru** | 设了过期时间的 key 中 LRU 淘汰 | | **allkeys-random** | 所有 key 中随机淘汰 | | **volatile-random** | 设了过期时间的 key 中随机淘汰 | | **volatile-ttl** | 设了过期时间的 key 中,TTL 越短越先淘汰 | | **allkeys-lfu** | 所有 key 中 LFU(最不经常使用)淘汰(Redis 4.0+) | | **volatile-lfu** | 设了过期时间的 key 中 LFU 淘汰(Redis 4.0+) | **LRU vs LFU**: - LRU:最近被访问过,说明热,保留 - LFU:被访问频率高,说明热,保留(避免偶发性访问误判为热数据) **生产建议**:通常用 `allkeys-lru`,缓存场景下最通用。 --- ### Q9:Redis 默认设置下,服务器突然断电会不会丢数据? **会丢数据**。 原因分析: - Redis 默认 RDB 和 AOF 都不开启(或 RDB 按固定间隔触发),两次持久化之间写入的数据全在内存中 - 即使开启 RDB,默认 `save 900 1`(900 秒内至少 1 次修改才触发),断电可能丢失最近 15 分钟的数据 - AOF 默认 `appendfsync everysec`(每秒刷盘),最多丢 1 秒数据,但也不是完全实时 **解决方案**: - 开启 AOF `appendfsync always`(每条命令刷盘)—— 最安全但性能最差 - 开启 AOF `appendfsync everysec`(每秒刷盘)—— 平衡方案,最多丢 1 秒 - 主从 + Sentinel/Cluster,如果主挂了从库可以补上(异步复制仍有少量丢失) --- ### Q10:Redis 结合 Lua 脚本的经验 **使用场景**: - **原子性操作**:多个 Redis 命令需要原子执行,如"先判断库存再扣减" - **减少网络往返**:多条命令打包一次执行 **示例(库存扣减)**: ```lua local key = KEYS[1] local qty = tonumber(ARGV[1]) local stock = tonumber(redis.call('get', key)) if stock == nil or stock < qty then return -1 -- 库存不足 end redis.call('decrby', key, qty) return stock - qty -- 返回剩余库存 ``` **注意事项**: - Lua 脚本执行时是阻塞的,不能执行过于耗时的操作 - 脚本需要提前在开发环境充分测试,避免线上脚本 bug - Redis 6.2+ 支持 `FUNCTION`,可以更好地管理脚本 --- ### Q11:怎么解决缓存击穿和缓存雪崩? #### 缓存雪崩 **现象**:大量缓存 key 同时过期,请求全部打到数据库,数据库压力暴增可能宕机。 **解决方案**: | 方案 | 说明 | |------|------| | **随机过期时间** | 在基础 TTL 上加随机值,如 `TTL = 3600 + random(0, 600)` | | **缓存预热** | 提前将热点数据加载到缓存,避免冷启动打崩 | | **Redis 集群/哨兵** | 避免单点 Redis 挂掉导致缓存全不可用 | | **多级缓存** | 本地缓存(Caffeine)+ Redis 多级,Redis 挂了本地还能顶 | | **熔断降级** | 数据库压力过大时限流,返回兜底数据 | #### 缓存击穿 **现象**:单个热点 key 过期瞬间,大量并发请求穿透到数据库。 **解决方案**: | 方案 | 说明 | |------|------| | **互斥锁** | 第一个线程获取锁去查数据库,其他线程等待锁释放后直接读缓存 | | **逻辑过期** | key 本身不过期,value 中存一个"逻辑过期时间",发现过期后异步更新,返回旧值 | | **永不过期** | 热点 key 不设过期时间,后台线程定时刷新 | #### 追问:如果并发量很大,同时查多个 key 怎么办? 1. **分布式锁 + 细粒度锁**:每个 key 一把锁,互不影响 ```java String lockKey = "lock:cache:" + key; boolean locked = redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); ``` 2. **逻辑过期 + 异步刷新**:key 不真的过期,value 内含过期时间戳,发现逻辑过期后仍返回旧值,**异步起一个线程去更新**,所有并发请求都能立刻拿到旧值返回 3. **限流 + 降级**:数据库查询层加限流,超过阈值的请求直接返回降级数据 4. **缓存预热任务**:定时扫描即将过期的热点 key,提前刷新 --- ## 五、其他面试题 ### Q12:了解 Dubbo 的序列化机制吗? Dubbo 支持的序列化协议: | 协议 | 特点 | |------|------| | **Hessian2**(默认) | 跨语言、二进制、字段顺序敏感、性能较好 | | **Fastjson2** | JSON 格式,可读性好但性能不如二进制 | | **Kryo** | 高性能二进制,需提前注册类 | | **Protobuf** | Google 出品,跨语言、性能最优、需 `.proto` 定义 | | **Java 原生** | 兼容性最好但性能最差 | **Hessian2 注意事项**: - 序列化和反序列化依赖字段顺序,增加字段时建议加在末尾否则需兼容处理 - 父类字段会优先序列化 **生产建议**:对性能有要求用 Kryo 或 Protobuf,一般场景 Hessian2 够用。 --- ### Q13:AI 对话超出上下文限制怎么办? **现象**:AI 对话轮次多了以后,超出上下文窗口,AI "忘记"之前的内容。 **解决方案**: | 方案 | 说明 | |------|------| | **拆分任务** | 大需求拆为小任务,一个对话只聚焦一个模块,减少单次对话轮次 | | **分阶段开发** | 先设计(一个会话)→ 讨论通过后写代码(另一个会话) | | **关键信息总结** | 阶段完成后让 AI 总结当前状态,新建对话时把总结贴回去 | | **外部文档沉淀** | 把架构设计、接口约定、数据库表结构写到项目 md 文件中,新会话直接引用 | | **Spec-Driven 开发** | 先写好详细需求 spec 文档,然后让 AI 严格按 spec 实现 | | **分层沟通** | 不在一轮中既讨论架构又写代码,架构定了后再开新对话写代码 | | **上下文压缩工具** | 使用支持上下文摘要/压缩的工具(部分 IDE 插件支持) | **核心思路**:不要把 AI 当"一个大项目长对话搞定",而是把它当"按 spec 执行的小粒度任务工具",每次聚焦一个目标。 ---

Java 后端面试题(一)

# Java 后端核心面试题 --- ## 一、如何保证方法线程安全? **问题:** 写了一个方法,如何保证该方法是线程安全的? **答案:** | 方式 | 说明 | |------|------| | 无共享变量 | 方法内部仅使用局部变量,不共享成员变量,天然线程安全 | | 使用锁机制 | `synchronized`(隐式锁)、`Lock`(显式锁),保证同一时刻只有一个线程执行临界区代码 | | 使用线程安全类 | 如 `ConcurrentHashMap`、`CopyOnWriteArrayList`、`Atomic` 原子类等 | | 变量不可变 | 使用 `final` 修饰共享变量,禁止修改 | | 线程私有 | 通过 `ThreadLocal` 让每个线程拥有独立变量副本,避免共享 | --- ## 二、synchronized 锁的级别及升级机制 **问题:** `synchronized` 是重量级锁还是轻量级锁?是一开始就是轻量级,还是一直不变? **答案:** `synchronized` 并非固定锁级别,JDK1.6 后做了锁优化,存在**锁升级**机制: ``` 偏向锁 → 轻量级锁 → 重量级锁(只能升级,不能降级) ``` | 锁级别 | 触发条件 | 实现方式 | 特点 | |--------|----------|----------|------| | 偏向锁 | 无竞争时默认开启 | CAS 记录线程 ID | 几乎无开销 | | 轻量级锁 | 出现轻微竞争(交替执行) | 自旋实现 | 无内核态切换 | | 重量级锁 | 竞争激烈、自旋失败 | 依赖操作系统内核 | 阻塞线程,开销大 | > **总结:** 传统 `synchronized` 是纯重量级锁,现在是自适应升级的混合锁。 --- ## 三、@Transactional 异常未回滚问题分析 **问题:** 方法加 `@Transactional`,抛出异常但数据库未回滚,如何排查分析? **答案:** 常见原因及排查方向: | 序号 | 原因 | 说明 | |------|------|------| | 1 | 异常类型不对 | 默认仅捕获运行时异常(`RuntimeException`)和 `Error`,普通受检异常 `Exception` 不会回滚,需手动指定 `rollbackFor = Exception.class` | | 2 | 异常被内部捕获 | 代码中 `try-catch` 吃掉异常,事务切面感知不到异常,不会触发回滚 | | 3 | 事务失效(同类内部调用) | 同类方法内部调用加事务的方法,AOP 无法增强,事务失效 | | 4 | 方法权限问题 | 事务注解加在 `private` / `protected` 方法上,Spring AOP 无法拦截 | | 5 | 数据库引擎不支持事务 | 表引擎为 MyISAM,改为 InnoDB | | 6 | 传播属性配置错误 | 传播行为设置为 `NOT_SUPPORTED` 等,强制非事务运行 | | 7 | 多数据源 / 事务管理器配置异常 | 注解指定的事务管理器与数据源不匹配 | --- ## 四、同类调用、事务嵌套导致事务失效原因 **问题:** 调用同类方法为什么事务失效?事务嵌套为何也会失效? ### 4.1 同类内部调用失效 Spring 事务基于**动态 AOP 代理**实现,只有外部通过代理对象调用方法,才会被切面拦截、开启事务。同类中 `this.方法()` 是直接调用原对象,不走代理,事务注解无效。 **解决方案:** - 自身注入自己 - 使用 `AopContext` 获取代理对象 - 拆分方法到不同类 ### 4.2 事务嵌套失效 主要由事务传播机制导致: | 场景 | 行为 | |------|------| | 外层无事务、内层 `REQUIRED` | 内层独立事务,互不影响 | | 外层有事务、内层 `REQUIRED` | 并入外层事务,内层异常会导致整体回滚 | | 内层 `REQUIRES_NEW` | 新建独立事务,但部分场景因连接、代理问题出现异常 | | 内层吞掉异常 | 外层无法感知,造成回滚异常 | --- ## 五、事务传播机制、四大特性、原子性理解 **问题:** 事务传播机制有几种?事务特性有哪些?原子性如何理解? ### 5.1 事务传播机制(共 7 种) | 传播行为 | 说明 | |----------|------| | `REQUIRED` | 支持当前事务,不存在则新建(默认) | | `SUPPORTS` | 支持当前事务,不存在则以非事务运行 | | `MANDATORY` | 必须在事务中运行,不存在则抛异常 | | `REQUIRES_NEW` | 新建事务,挂起当前事务 | | `NOT_SUPPORTED` | 以非事务运行,挂起当前事务 | | `NEVER` | 以非事务运行,存在事务则抛异常 | | `NESTED` | 嵌套事务,外层回滚则内层也回滚,内层回滚不影响外层 | ### 5.2 事务四大特性(ACID) | 特性 | 英文 | 说明 | |------|------|------| | 原子性 | Atomicity | 事务内所有操作要么全部成功,要么全部失败回滚 | | 一致性 | Consistency | 事务执行前后,数据必须保持一致状态 | | 隔离性 | Isolation | 并发事务之间互不干扰 | | 持久性 | Durability | 事务提交后,数据永久保存 | ### 5.3 原子性理解 一个事务内的所有数据库操作要么全部成功提交,要么全部失败回滚,不可部分执行。比如下单扣库存、生成订单,两个操作必须同时生效或同时撤销。 --- ## 六、Spring AOP 执行阶段 & 循环依赖解决方案 **问题:** Spring AOP 发生在 Bean 生命周期哪个阶段?Spring 循环依赖如何解决? ### 6.1 AOP 执行阶段 AOP 动态代理在 **Bean 初始化完成后、放入单例池之前** 生成代理对象。 ``` Bean 核心流程: 实例化 → 属性填充 → 初始化(@PostConstruct / init-method)→ AOP 代理 → 存入单例池 ``` ### 6.2 循环依赖解决 Spring 仅能解决**单例 Bean 基于 setter 注入**的循环依赖,依靠三级缓存: | 缓存级别 | 存储内容 | |----------|----------| | 一级缓存 | 完整可用的单例 Bean | | 二级缓存 | 已实例化、未完成属性填充的半成品 Bean | | 三级缓存 | Bean 的工厂对象(提前暴露实例) | **原理:** 实例化后提前暴露半成品,打破循环引用;构造器注入、多例 Bean 无法解决循环依赖。 --- ## 七、项目中线程池使用、作用与选型原因 **问题:** 项目中如何使用线程池?为什么使用线程池?解决了什么问题? ### 7.1 使用方式 项目一般使用 `ThreadPoolExecutor` 手动创建线程池(**禁止 `Executors` 默认工厂类,避免 OOM**),根据业务设置核心线程、最大线程、队列、拒绝策略;用于异步处理、批量任务、文件导出、消息消费等场景。 ### 7.2 使用原因 & 解决的问题 | 优势 | 说明 | |------|------| | 降低性能开销 | 避免频繁创建 / 销毁线程 | | 统一管理线程 | 控制并发数量,防止无限创建线程导致 OOM | | 提升吞吐量 | 复用线程、任务排队 | | 提升稳定性 | 提供拒绝策略、监控、超时机制 | --- ## 八、大数据量导出 + 实时进度展示实现 **问题:** 大数据导出场景,如何实现展示导出进度(1%、5%…)? **答案:** 整体采用**异步导出 + 进度存储 + 前端轮询**方案: ``` 1. 前端发起导出请求 → 后端开启异步线程执行导出任务 → 立即返回任务 ID 2. 每处理一批数据 → 将当前进度、任务状态存入 Redis / 内存 / 数据库(key = 任务 ID) 3. 前端根据任务 ID 轮询接口 → 查询 Redis 中的进度并展示 4. 导出完成 → 将文件地址写入缓存,前端拉取文件下载;异常则标记失败状态 ``` **优化:** 数据分批查询、分批写入文件,避免一次性加载全量数据内存溢出。 --- ## 九、死锁产生条件、排查、规避方案 **问题:** 什么情况会发生死锁?如何处理死锁?开发中如何避免? ### 9.1 死锁四大必要条件(同时满足才会死锁) | 条件 | 说明 | |------|------| | 互斥条件 | 资源同一时刻只能被一个线程占用 | | 请求与保持条件 | 持有资源的同时请求其他资源 | | 不可剥夺条件 | 已获得的资源不能被强行释放 | | 循环等待条件 | 多个线程形成循环等待资源的关系 | ### 9.2 线上处理死锁 | 场景 | 排查方式 | |------|----------| | MySQL 死锁 | `SHOW ENGINE INNODB STATUS` 定位死锁 SQL | | Java 线程死锁 | 使用 `jstack`、`Arthas` 排查阻塞线程与锁持有关系 | | 临时恢复 | 重启服务、终止卡死请求 | ### 9.3 代码规避死锁(破坏四大条件) - **统一锁顺序:** 所有线程按固定顺序获取多把锁 - **加锁设置超时时间:** `Lock.tryLock(time)`,超时自动释放 - **减少锁粒度:** 尽量不嵌套锁 - **避免持有锁时再请求其他锁** --- ## 十、MySQL 排他锁(FOR UPDATE)并发访问效果 **问题:** SQL 加排他锁 `FOR UPDATE`,第一个请求未执行完,第二个请求进来会继续执行还是阻塞? **答案:** | 锁类型 | 场景 | 第二个请求效果 | |--------|------|----------------| | 行级排他锁 | 查询命中索引,锁对应数据行 | 访问同一行 → 阻塞等待;访问其他行 → 正常执行 | | 表级排他锁 | 查询未命中索引 / 索引失效,行锁升级为表锁 | 所有后续请求都会阻塞 | > **总结:** 访问同一条数据必然阻塞;不同行、行锁生效则不阻塞。 --- ## 十一、读锁、写锁(共享锁 & 排他锁)区别 **问题:** MySQL 读锁和写锁有什么区别? ### 读锁(共享锁 / LOCK IN SHARE MODE) - **共享:** 多个线程可同时加读锁,互不阻塞 - **互斥:** 有加读锁时,所有写锁都会被阻塞 - **用途:** 保证查询数据期间不被修改 ### 写锁(排他锁 / FOR UPDATE) - **独占:** 只能有一个线程持有写锁 - **互斥:** 持有写锁后,其他线程的读锁、写锁全部阻塞 - **用途:** 更新、删除、强一致性查询场景 > **口诀:** 读读共享、读写互斥、写写互斥 --- ## 十二、MySQL 调优 & 索引数据结构 **问题:** MySQL 日常如何调优?索引使用什么数据结构? ### 12.1 MySQL 调优方向 | 优化方向 | 具体措施 | |----------|----------| | SQL 优化 | 避免 `SELECT *`、大事务、全表扫描、隐式类型转换;用 `EXPLAIN` 分析执行计划 | | 索引优化 | 合理创建联合索引、遵循最左匹配,删除冗余 / 失效索引 | | 架构优化 | 主从复制、读写分离、分库分表 | | 配置调优 | 调整 Buffer Pool、连接数、超时时间等参数 | | 存储引擎 | 业务表统一使用 InnoDB | ### 12.2 索引数据结构 InnoDB 主键索引、二级索引默认使用 **B+ 树**。 --- ## 十三、B+ 树特点 & 和 B 树的区别 **问题:** B+ 树有哪些特点?对比 B 树有什么不同? ### B+ 树特点 - 所有数据行 / 数据页都存在**叶子节点**,非叶子节点只存索引键,不存完整数据 - 叶子节点通过**双向链表**串联,范围查询、排序效率极高 - 树高度更低,磁盘 IO 次数更少,查询性能稳定 ### 与 B 树核心区别 | 对比维度 | B 树 | B+ 树 | |----------|------|-------| | 数据存储 | 非叶子、叶子节点都存完整数据 | 仅叶子存数据 | | 范围查询 | 需要中序遍历,效率低 | 叶子链表遍历,远快于 B 树 | | IO 次数 | 节点存储数据多,索引少,树更高 | 节点存储索引更多,树更矮,磁盘 IO 更少 | | 查询效率 | 单条查询速度不稳定 | 所有查询都从根走到叶子,性能均衡 | --- ## 十四、设计模式 + 实际业务场景 **问题:** 开发中用过哪些设计模式?举一个场景、模式、解决的问题。 ### 示例 1:策略模式 - **场景:** 订单支付(微信、支付宝、银行卡、余额多种支付方式) - **使用模式:** 策略模式 - **解决问题:** 消除大量 `if-else` / `switch` 判断,新增支付方式只需新增策略类,符合开闭原则,代码易维护扩展 ### 示例 2:单例模式 - **场景:** 全局配置类、工具类、连接池管理 - **使用模式:** 饿汉 / 懒汉单例 - **解决问题:** 保证全局只有一个实例,节省资源,统一全局配置 ### 示例 3:工厂模式 - **场景:** 根据类型创建不同消息推送(短信、APP 推送、站内信) - **使用模式:** 简单工厂 / 抽象工厂 - **解决问题:** 对象创建与业务逻辑解耦,统一创建入口 --- ## 十五、千万级宽表多条件分页查询优化 **问题:** 千万级订单主表、上百字段、多查询条件 + 关联表,分页十几秒,如何优化? ### 分维度整体优化 | 优化维度 | 具体措施 | |----------|----------| | 字段优化 | 宽表拆分为主表 + 扩展表,分页查询只查必要字段,避免查询上百字段 | | 索引优化 | 根据高频查询条件建立联合覆盖索引,避免回表;优化关联表索引 | | 分页优化 | 深分页使用主键偏移分页(`WHERE id > ? LIMIT 10`)替代传统 `LIMIT offset, size` | | SQL 优化 | 简化多表关联,优先小表驱动大表;禁止不必要查询、排序 | ### 架构层面 - **冷热数据分离:** 历史订单归档到历史表 - **引入 ES:** 复杂多条件、模糊查询、分页全部走 ES,MySQL 仅做基础事务 - **读写分离:** 查询走从库,减轻主库压力 - **缓存:** 对高频固定条件的分页结果做缓存 --- ## 十六、分页 COUNT 总数查询慢(全表扫描)优化 **问题:** 分页需要查 COUNT 统计总数,条件多变、全表检索很慢,索引已加,如何优化? ### 区分业务场景,非精准统计 | 方案 | 说明 | |------|------| | 允许近似值 | 使用 MySQL 信息统计表 `information_schema.TABLES` 获取粗略总数,速度极快 | | 允许延迟更新 | 定时任务把总条数、条件统计结果预计算存入 Redis / 统计表,前端直接读取 | ### 精准 COUNT 优化 | 方案 | 说明 | |------|------| | 优先使用主键 / 非空唯一索引 | `COUNT(主键)` 优于 `COUNT(*)`、`COUNT(字段)` | | 复杂多条件 | 将统计逻辑下沉到 ES / OLAP 数据库,专门做聚合统计 | | 分页兜底 | 前端取消实时总数,使用「上拉加载更多」,不再查询 COUNT | | 分区表 | 按时间分区,统计时只扫描对应分区,缩小扫描范围 | --- ## 十七、分库、分表的作用与解决的问题 **问题:** 为什么要分库?为什么要分表?分别解决什么问题? ### 分表(水平分表 / 垂直分表) 解决单表数据量过大问题(千万 / 亿级):单表数据越多,索引、查询、写入越慢,分表把数据拆分到多张结构相同的表,降低单表数据量,提升查询和写入性能。 - **垂直分表:** 将不常用的大字段拆分到扩展表,主表保留高频字段 - **水平分表:** 按规则(如 ID 取模、时间范围)将数据分散到多张结构相同的表 ### 分库 解决单库连接数、IO、CPU 瓶颈问题:单库承载的并发连接数、磁盘 IO、CPU 计算都有上限,分库将数据分散到不同数据库实例,提升整体并发处理能力。 - **垂直分库:** 按业务模块拆分(如订单库、用户库、商品库) - **水平分库:** 同一业务数据按规则分散到多个数据库实例

今年3月面了20个前端……

## 我面了二十个前端 3月,我参与了前端岗位的招聘工作。HR初筛了上百份简历后,递到我手中的还有30余份,前前后后面试了近二十场,最终才招到 2 位合适的伙伴入职。今天就和大家好好聊聊,也给正在求职的同行、负责招聘的伙伴,分享一点真实的观察。 ## 一、面试者画像 面试者里,5到10年工作经验的伙伴占了大头,不再是以往刚入行一两年的新人。学历覆盖很广,其中一本、二本院校的伙伴居多,大家来自不同的行业,互联网、传统软件、研究所项目都有涉及,甚至有伙伴长期深耕地图、电子表格这类垂直领域,各自都有自己的积累。 还有一个很明显的现象,就是大家的简历上,大多都标注了 AI相关的能力——基础一点的,会写掌握各类AI编程辅助工具;层次标注得高一些的,会写具备Agent开发能力。但实际深入追问后发现,这些AI相关的经历,大多是做过简单的文本问答类应用,本身技术含量并不高;还有更多伙伴,其实只负责了前端打字渲染的部分,对于Agent的核心流程、底层逻辑,根本没有清晰的认知,相当于只是“沾了AI的边”,并没有真正掌握相关技术。 在基础层面,很多伙伴其实都掌握了核心概念,只是欠缺一点深入的思考和实操打磨。比如 this指向、事件循环、Promise 这些高频考点,大家都能流利说出概念,但如果给一段具体代码让分析输出,或是手写一个改变this指向的实现方法,就容易卡顿。还有Vue组件挂载顺序、watch 和 watchEffect 的区别,不少工作七年以上、简历标注“精通Vue”的伙伴,也没能说清楚。 项目经历上,不少伙伴简历上写着“负责项目性能优化”,但细问优化前的指标、优化后的效果、采用的核心方案,就有些支支吾吾;有的伙伴说自己二次封装过表格库,可问到canvas渲染流程、协同编辑的冲突解决,就显得有些茫然;还有长期做地图业务的伙伴,聊起自己最核心的项目,只能简单复述业务场景,说不出技术亮点,也没有对项目迭代的思考——其实这些沉淀,恰恰是咱们拉开差距的关键。 另外,很多伙伴在 Node.js和后端认知 上,还有很大的提升空间。大部分伙伴对Node.js的了解,只停留在“知道这个工具”的层面,没有实际项目使用经验,基础API用法不清楚,也缺乏基本的后端思维。长期专注于“写页面、调接口”,技术栈相对单一,对前端工程化、服务端部署等相关领域接触较少,这其实也限制了咱们的成长上限。 ## 二、面试现场 二十场面试下来,能感受到每位伙伴的求职诚意,大家的临场状态差异很大,也暴露了一些可以改进的小问题,在这里和大家一一说说,希望能帮到后续求职的伙伴。 有一部分伙伴,会过度紧张,导致发挥失常。能理解大家求职的急切,也知道面试时的压力很大,所以会出现表达卡顿、逻辑混乱的情况,即便平时掌握的基础知识点,也难以清晰作答,少数伙伴甚至会因压力过大,中途终止面试。比如有一场面试,那位伙伴回答问题时声音都在颤抖,this指向、Vue组件挂载顺序这些基础题,都没能顺利答上来;还有一场面试,有位伙伴面试到一半退出了会议,能感受到他的情绪很急躁。 还有一些伙伴,背题痕迹比较明显,容易答非所问。比如有一位伙伴,回答问题时更像是在念面试题库,我问的是原理层面的内容,他却只说标准答案,甚至答非所问;还有一位伙伴,也有类似的情况,项目技术积累很扎实,但因为过度依赖背题,没能结合自己的实际经验作答,问东答西,没能形成完整的逻辑。其实面试没有标准答案,真诚分享自己的理解和经验,反而更能打动面试官。 不少伙伴理论掌握得很扎实,但缺乏实操落地能力。大家能清晰说出性能优化的几个方向,却讲不清楚具体的优化点和量化数据;知道虚拟滚动、内存泄露排查的概念,却从来没有在实际项目中应用过,无法结合具体场景给出解决方案。其实前端是一门注重实操的学科,平时多动手、多尝试,把理论落地到项目中,才能真正提升自己的能力。 还有少数伙伴,面试态度有些随意,甚至存在简历美化过度的情况。有一位有五年经验的伙伴,态度比较随意,基础能力还有提升空间,简历上的项目经历和实际表述有不小差距;还有一场面试,一位985院校毕业的伙伴,JS基础知识不够扎实,Vue使用也不够深入,对后端知识的了解也比较浅——其实简历美化是可以理解的,但过分夸大,反而会影响面试效果,真诚对待每一场面试,才是最稳妥的方式。 ## 三、市场环境:供需两端的变化,需要我们主动适应 这段时间的招聘,也让我感受到了前端人才市场的一些变化,不管是求职的伙伴,还是我们招聘方,都需要慢慢适应这些变化,在这里和大家好好聊聊。 从求职伙伴的角度来说,现在活跃在市场上的前端从业者,大多是因为公司裁员、业务收缩等客观原因被迫离职,这也导致市场上候选人的整体水平,出现了一些波动。再加上AI工具的普及,简历包装的门槛降低了很多,一键生成简历、优化项目描述变得很容易,但只要深入追问两句,就能看出大家的真实能力。其实很多时候,简历不用写得过于华丽,朴实、真实地展现自己的能力和经验,反而更容易获得认可;那些简历写得朴实、不刻意包装的伙伴,往往能力更真实、更稳定。 同时,也能感受到大家的求职心态,因为市场上的工作机会变少,大家都比较急切,这种急切很容易导致临场表现变形。还有一部分伙伴,可能对自己的能力认知不够清晰,容易陷入自我误区,觉得面试官“不懂行”,其实我们只是想找到真正适配的伙伴,没有任何轻视的意思。如果面试后没有通过,大多是因为技术栈不匹配,大家不用过于否定自己,继续提升自己,总会遇到合适的机会。 从我们招聘方的角度来说,以往带新人的过程中,也发现了一些共性问题,这些问题或许也能帮大家更好地认识自己的成长方向。 动手能力上:不少新人自主解决问题的能力有待提升,对业务的理解速度较慢,写代码不够规范、实现方式不够优雅,缺乏主动思考和总结的意识,遇到问题习惯依赖他人。其实平时工作中,多主动尝试解决问题,多总结经验,慢慢就能提升自己的动手能力。沟通协作上:有些伙伴喜欢埋头苦干,不懂得主动提问、及时求助,遇到问题不反馈;工作态度有些松散,到点就走,即便有紧急工作也会推脱,甚至有少数伙伴自由散漫,未经报备就擅自离岗。其实良好的沟通协作能力,和技术能力同样重要,学会主动沟通、认真负责,才能走得更远。学习能力上:现在很多伙伴会依赖AI工具,但缺乏对AI生成内容的掌控力,不会辨别对错;接受新事物、新技术的速度较慢,学习时需要他人一步步讲解,缺乏自主学习的主动性。其实AI是很好的辅助工具,但不能过度依赖,主动学习、主动探索,才能跟上技术迭代的步伐。 (顺手推几个技术大厂的机会,前、后端or测试,感兴趣就[试试](https://jsj.top/f/o38ijj)  ) ## 四、聊聊根源 面试完这二十场,我一直在思考一个问题:为什么很多工作五年以上的前端伙伴,基础依然不够扎实?仔细梳理后发现,这从来不是大家不够努力,而是多种因素长期共同作用的结果,很多时候,是我们的成长环境,限制了对底层的探索。 一方面,业务场景单一,让很多伙伴长期陷入“CRUD陷阱”。这批面试者中,很多伙伴长期做的都是列表、表单、文件上传等基础功能,不是大家不想学习Node.js、不想接触工程化,而是工作内容本身就是“把数据展示出来”,根本没有机会接触服务端开发、性能优化、部署运维等更有深度的场景。技术栈越用越窄,能力也慢慢固化,等到市场要求前端具备“大前端”思维时,才发现自己除了写页面,还有很多需要学习的地方。 另一方面,前端技术迭代太快,很多伙伴的精力,被分散在了“追热点”上。每年都有新的概念、新的工具出现,大家被推着去学习:刚学会Vue3的Composition API,马上又要学Vite、Rspack;刚掌握React Hooks,又要了解Server Components。精力被大量分散在各种“新东西”上,反而没有时间沉下心来,夯实基础。面试时最明显的就是,新词说得头头是道,老基础却答不上来——其实慢一点、稳一点,把基础打牢,再去追热点,反而会更高效。 除此之外,还有一个现实因素,当前市场上活跃的求职者,很多是被裁或因公司问题被迫离职的,整体经验质量有所下滑,再加上AI降低了简历包装的门槛,使得大家的真实能力,需要更深入的沟通才能看清。 ## 五、一点心得 面完这二十场,我对“靠谱的前端”,有了更清晰的认知,也想把这些心得分享给大家,希望能帮到正在成长的每一位前端伙伴。 不用过分纠结工作年限,重点打磨基础密度。以前招聘,我也会觉得工作年限越长,能力越强,但实际面试下来发现,五年经验和十年经验,在现场表现上可能拉不开差距。但基础扎实的伙伴,不管工作几年,回答问题时都有底气,能经得起连环追问,也能快速适应新的业务和技术。这次最终入职的那位伙伴,未必是年限最长的,但他的基础很扎实,问到底层原理也不怵——基础,才是我们最核心的竞争力。不用追求项目数量,重点沉淀项目深度。很多伙伴的简历上,列着四五个甚至更多项目,但每个项目的描述都大同小异,无非是“做了列表页、做了表单、做了上传”。其实真正有价值的项目经历,不是数量多,而是深度足够:能讲清楚当时的业务约束是什么、为什么选择这个技术方案、最终的量化结果如何(比如性能提升了多少、用户体验改善了多少)。能把一个项目做深、做透,总结出自己的经验和思考,比堆砌多个重复项目,更能体现自己的能力。不用刻意追求技术栈广度,重点突破自己的舒适区。技术栈单一本身不是问题,问题是长期停留在单一的技术栈里,从来没有被逼着去解决过边界问题、复杂问题。比如长期做电子表格的伙伴,不妨主动研究一下canvas渲染性能的优化方法;长期做地图业务的伙伴,不妨总结一下海量数据点的加载优化方案。有过突破舒适区的经历,即便技术栈不宽,也能看出你的学习能力和解决问题的潜力,这比单纯的“技术栈多而杂”,更有价值。 接受简历与能力的小落差,多提升实操能力。现在简历包装已经很普遍,光靠简历,很难判断一个人的真实能力。对于求职的伙伴来说,与其花大量时间包装简历,不如多花时间提升自己的实操能力,平时多动手写代码、多做项目、多总结经验;对于我们招聘方来说,以后也会在初筛阶段增加实操题,帮大家更好地展现自己的真实能力。 最后想和大家说一句:前端这个行业,正在慢慢告别“会写页面就能找工作”的时代,转向“能解决复杂问题、有深度沉淀才能留下”的新阶段。真正优秀的前端,不是会的框架多、懂的新词多,而是基础牢、能深入、有沉淀,能在业务场景中解决实际问题。 以上,就是我3月面了二十个前端后的真实心得与思考,愿与所有前端同行、招聘伙伴共勉,也祝愿每一位努力的前端伙伴,都能在成长路上稳步沉淀、持续提升,找到自己心仪的工作,在前端这个行业里,踏实前行、不负热爱。 ——转载自:三只萌新

分享社招面经——懂车帝1面

懂车帝 - 后端开发 面试复盘 一、基本信息 - 公司:懂车帝(字节跳动) - 岗位:后端开发 - 轮次:一面 - 面试形式:远程视频 - 候选人背景:小米数仓+服务端开发,2024届华中农业大学本科 - 已挂 二、面试问题与回答 1. 自我介绍 - 问题:简单自我介绍 - 回答:华中农业大学20届应届生,小米工作近两年,主要做数仓开发和服务端开发 2. Redis基础数据结构 - 问题:说一下Redis的几个基础结构 - 回答: - 五个基础数据结构:string、list、hash、set、zset - 其他数据结构:bitmap、geo、hyperlog等 - string用于token、key-value存储 - hash用于可重入锁设计,存储对象并计数 - list用于队列,SDK全链路采集中用list作缓存队列,按批次写入PG数据库 3. 用户分数排行榜设计 - 问题:实现用户分数排行榜用什么数据结构 - 回答:用zset,它自带排序功能,可直接展示排序后的数据 - 追问:千万级别用户分数排行榜怎么实现 - 回答: - Redis集群部署,提高并发能力 - 限流:使用Redis漏桶算法 - 但对于千万级数据直接放在一个zset中的大key问题回答不清楚 4. 大key问题 - 追问:大key会造成什么问题 - 回答: - Redis是单线程,大key查询响应慢 - 虽然有IO多路复用,但大key返回慢会影响整体性能 - 对大key的具体处理方案和上限值回答不清楚 5. 热key问题 - 问题:如何防止热key失效导致的问题 - 回答: - 热key预热 - 谨慎设置过期时间 - 保证数据库中的key可以命中 6. 缓存一致性 - 问题:数据库写、缓存读,如何处理缓存一致性 - 回答: - 采用先更新数据库,再删除缓存的策略 - 这样可以保证在高并发多线程情况下不会因为Redis和数据库更新顺序导致数据不一致 7. 布隆过滤器 - 问题:对布隆过滤器有了解吗 - 回答: - 用于解决缓存穿透问题 - 当数据库和Redis都没有查询的数据时,请求会一直击穿 - 布隆过滤器可以在访问Redis前返回结果,避免打到数据库 8. 点赞系统设计 - 问题:设计一个点赞系统,要求: 1. 用户可以给文章点赞 2. 进入文章详情知道有多少人点赞及我是否点过赞 3. 作者知道有多少人给我点过赞 设计存储方案和表结构 - 回答: - 存储:MySQL持久化,Redis缓存 - Redis用hash结构,key为"user_id|article_id",value为0/1表示是否点赞 - MySQL表设计: - 用户表 - 文章表 - 点赞关联表(user_id、article_id、是否点赞) - 第三张表与Redis保持一致,每10分钟同步一次 - 如果Redis挂掉,通过本地日志补偿机制恢复 - 追问:如何实现作者本人知道有多少人点赞 - 回答:文章表中有user_id字段,可以通过article_id关联找到作者 9. 日志文件最大同时在线人数 - 问题:日志文件记录用户登录登出时间(精确到秒),求: 1. 全天最大同时在线人数 2. 最大同时在线人数对应的持续时间段 - 回答: - 思路:行转列,把开始时间到结束时间拆分成每一秒 - 统计每一秒有多少用户在线 - 找到重复次数最多的时间点 - 时间复杂度:O(logN) + O(N) - 面试官指出这个方案不够优化 10. 懂车帝业务介绍 - 问题:懂车帝主要做什么业务 - 回答: - 懂车帝有三个属性:选车(车型库)、买车(电商)、看车(内容) - 车型库和内容属于媒体阶段 - 买车涉及经销商和电商 - 该岗位偏向内容:内容发布、审核、模型特征处理、推荐算法、互动行为、作者生态 - 懂车帝已独立,但基建还用字节的,公司独立但字节是大股东 - 技术栈:Go语言 三、表现亮点 - 对Redis基础数据结构掌握较好,能结合实际项目说明使用场景 - 点赞系统设计思路清晰,考虑了缓存、持久化、补偿机制 - 对缓存一致性问题有正确的理解和方案 - 主动询问业务背景,展现对岗位的兴趣 四、不足与改进 - 对Redis大key问题理解不深,无法给出具体处理方案 - 千万级排行榜设计方案不完整,未考虑大key拆分 - 算法题思路不够优化,时间复杂度未达到最优 - 对布隆过滤器只有概念性了解,缺乏深入理解 - 点赞系统设计中,Redis挂掉的补偿机制描述不够清晰

分享社招面经——百度一面

一、基本信息 - 公司:百度 - 岗位:后端开发 - 轮次:一面 - 面试形式:远程视频 - 候选人背景:小米数仓+服务端开发,2024届华中农业大学本科 二、面试问题与回答 自我介绍与背景 - 问题:请做一下自我介绍 - 回答:华中农业大学2024届本科生,有金山云和浙江时空智子两段Java实习经历。2024年4月入职小米实习,7月正式入职。前一年做数仓开发(泊车业务),2025年10月转岗到众包项目组做服务端开发,主要战果是SDK全链路采集平台。技术栈包括Python、Java、Spark、Spring Boot、Redis等。 数仓重构项目 - 问题:数仓重构是几个人做的?花了多久? - 回答:主要由我一个人主导负责,耗时约两个月(包含双跑验证两周)。 - 问题:合并四张表为一张表的依据是什么? - 回答:四张DWM层主题表存在大量重复计算逻辑,都需要从ODS层track打点信号读取数据。将重复逻辑抽离成base表,后续表只需依赖base表,避免重复计算。同时在DWD层过滤掉80%无用数据(只保留车速<5km/h的最后一段泊车数据),大幅降低计算耗时。 - 问题:分区策略和索引是怎么设置的? - 回答:主要分区是泊车功能类型和日期,使用Iceberg存储。索引主要在Doris层设置,针对省市区、功能类型等常规维度。数据经过聚合计算后量级在百万级,不需要复杂索引设计。 数仓全链路监控 - 问题:数仓全链路监控的核心指标和告警机制是怎么设计的? - 回答:数仓链路监控分两块:一是作业链路监控(超时、失败情况),通过公司内部工厂平台配置;二是Doris指标监控(异常分析、空值判断)。报警阈值按业务设定,如核心泊车次数指标日波动超过50%就触发告警。 SDK全链路采集查询性能优化 - 问题:查询耗时从18秒降到7秒,除了并表还做了哪些优化? - 回答:之前数据来自多个DWM主题表,存在数据口径不统一、需要在Doris层再次聚合计算等问题。通过构建一张DWS层应用层大宽表,把指标在DWM层就预先计算好,Doris直接查询大宽表,避免重复预计算,查询耗时从18秒降到7秒,维护成本(人效)也降低75%。 高并发场景与Redis分布式锁 - 问题:第二个项目(SDK调用全链路统计平台)涉及高并发吗? - 回答:并发不高,主要是to B场景且单趟数量固定。高并发实践主要在浙江时空道宇实习期间的日报提交防重复场景,使用Redisson分布式锁,锁对象是员工id+当天日期。 - 问题:五层架构是怎么设计的? - 回答:采集层(AOP/装饰器异步采集SDK调用)→ 缓存层(Redis list队列,每1000条批量写入)→ 持久层(PostgreSQL)→ 计算层(Spark同步到Iceberg)→ 可视化层(Doris看板)。重写线程池拒绝策略,把溢出请求写入本地日志做兜底,保证最终一致性。 Redis相关 - 问题:Redis分布式锁的实现? - 回答:Redisson底层使用Lua脚本保证setnx和expire的原子性,通过看门狗守护线程在业务时间达到锁过期1/3时自动续期。除Redis外,Zookeeper临时节点也可实现分布式锁,通过监听上一节点判断锁释放。 - 问题:Redis过期键删除策略? - 回答:惰性删除(访问时检查过期)+定期删除(随机抽样部分key)。这两种策略可能导致缓存雪崩(大量key同时过期)、缓存击穿(热点key失效)、缓存穿透(数据库与Redis都没有)。 - 问题:Redis持久化方式? - 回答:这块不太熟悉,记得有RDB相关的机制把数据刷盘,具体细节模糊。 - 问题:Redis常用数据结构? - 回答:string、list、hash、set、zset,以及bitmap、HyperLogLog等。 - 问题:Hash底层结构和冲突解决? - 回答:底层用ziplist和hashtable(字典),冲突用链表法,链表过长时触发rehash(具体阈值不太记得,知道是因为链表过长影响性能时触发)。 - 问题:String底层结构? - 回答:用SDS(简单动态字符串),不是C的char数组,通过结构体记录len等字段实现O(1)取长度。 其他基础知识 - 问题:OAuth 2.0核心流程? - 回答:不太了解,模糊记得是先向OAuth服务器请求拿token,后续请求带token到header里去做认证,类似JWT机制。 - 问题:服务注册与发现的核心原理? - 回答:以Zookeeper为例,从节点初始时向主节点发送注册请求,后续通过心跳机制保持存活,主节点也会定期询问各节点状态。 - 问题:MySQL事务隔离级别? - 回答:读未提交、读已提交、可重复读、串行化。但项目主要用PG和Iceberg,MySQL业务知识不太熟悉。 - 问题:RocketMQ和Kafka区别? - 回答:Kafka基于分区,通过offset推进数据;RocketMQ通过exchange等机制(回答不准确)。两者都支持10万级吞吐。公司用基于Kafka封装的Talos。 - 问题:消息队列如何保证消息不丢失/不重复? - 回答:不丢失通过死信队列;消费端通过ACK确认避免重复(精确一次)。重复消费这块不太了解。 算法题 - 问题:数组中第K大的元素 - 回答:先用Arrays.sort排序,再取下标为n-k的元素。提到更优解可以用堆/快速排序的partition方式,但快排代码不太记得了。 反问 - 问题:候选人反问公司业务、团队方向 - 回答:面试官介绍是百度网盘相关,大团队做AI工程,智能化搜索、AIGC方向。 三、表现亮点 1. 数仓重构项目讲解清晰:从问题(数据量增长导致作业超时)到方案(抽离base表+DWD层过滤80%数据+加盐打散解决数据倾斜)到结果(从2小时降到1小时内),逻辑链路完整。 2. 主动结合业务场景做架构选型说明,如选Redis而非MQ的轻量化考量。 3. 主动谈及对AI/RAG/Agent的探索(自动化制图方案),展现学习能力和技术热情。 4. AI coding实践能力突出,熟悉多Agent协作开发模式(架构师/挑刺角色等)。 5. 沟通节奏好,能识别面试官追问意图并补充关键细节。 四、不足与改进 1. Redis持久化机制(RDB/AOF)知识盲区,只能模糊回答有"刷盘机制"。 2. OAuth 2.0核心流程不熟悉,只能用JWT类比,无法说清authorization_code等具体流程。 3. MySQL事务隔离级别只能列名词,无法详细解释每个级别解决的问题(脏读、不可重复读、幻读)。 4. RocketMQ vs Kafka的核心区别表达不清,把Exchange概念错配给了RocketMQ(实际是RabbitMQ的概念)。 5. 快速排序代码细节遗忘,算法题只能给出O(nlogn)的兜底解,没有写出O(n)的快速选择/堆解法。 6. Redis哈希的rehash触发阈值(负载因子1/5)记不清。

下载 APP