编程导航面试题话题讨论

面试题

2.4k 参与
分享

快来分享你的内容吧~

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

杭智智谷实习生技术面+小hr面

一、开场与个人情况 1. 请简单介绍一下自己。 2. 实习经历没有在简历里面体现吧? 3. 医疗那一块是在哪里做的? 4. 你已经实习过一小段(一个月),为什么离职了? 5. 那你入职前就应该没了解过吗? 二、技术经历确认 6. 医疗的二开,针对 HIS 平台是吧? 7. 我看你学过 Java,前端也自己做,是吧? 8. Linux 部署,有部署过吗? 9. Python 也做过,是吧? 三、Java 基础 10. 简单说说看 HashMap 的底层原理和扩容机制。(刚开始第一个问题有点紧张,没答好) 11. 为什么(加载因子)取 0.75 这个值? 12. 说说看 String、StringBuilder 和 StringBuffer 的区别。(这个原原本本看过的问题,过) 13. 简单说一下 JVM 的内存结构。(当时这个我就没有复习,本身学得就少简历也就写了解,答出一点就说不会,后面意识到这是对着我简历顺序问的,答不出真的很掉分) 四、并发编程 14. 并发编程里面,线程池的七个核心参数是什么? 15. 常见的拒绝策略有哪些?(这个也没答好忘了,就记得一个抛出异常的) 五、MySQL 16. MySQL 里面什么情况下索引会失效? 六、Spring Boot 17. Spring Boot 里面,整个自动配置的原理。(答得模糊) 七、前端(Vue) 18. 前端你用过哪些框架? 19. Vue 2 和 3 的响应式原理有什么区别?(我看过,但是没想起) 20. Vue 3 的生命周期加载顺序知道吗? 八、Linux 21. Linux 里面查找 8080 端口的进程。(答成名为8080 进程了...) 九、AI 工具使用 22. 你现在主要用的 AI 工具是什么? 23. 有对比过 Cursor、Windsurf、Copilot 这些的区别吗? 24. 包括 Claude 和 Codex 的区别(了解过吗)? 25. 你主要用命令行(Claude Code CLI)? 26. 你这就是 B 站上学的吧?Claude Code 加 DeepSeek 这一套。(追问:是不是同一套教程?) 27. 老师为什么推荐 Claude Code 呀?好在哪里? 十、产品与研发思维 28. 你觉得产品经理去开发和研发去开发,它的优劣是什么? 29. 那你觉得对于企业来说哪个更好呢? 十一、个人素质与岗位匹配 30. 你个人的优势和劣势大概说一下。 31. 我们这个岗位要求懂产品,会进行产品演示、产品培训,你觉得你能胜任得了吗? 十二、反问与收尾 32. 你这边有什么要了解的?(候选人反问环节) 33. 你自己的职业规划是怎样的? # 后言 刚从一个做医疗行业的小公司(活了11年,20多人的,但是公司氛围真的很好,领导没有架子,可惜需要常驻出差到某个医院,再加上还是对医疗行业不感兴趣,不然这家公司将是我的归宿)出来,决定投一家面面找找感觉,还是觉得自己八股文太薄弱了,决定要每天常态化背八股,写在简历上的一定要背下来,被拷打答不出真觉得很丢分;另外个人bg差双非本,投了三四个月,还是觉得光两个后端不够,得至少多加一个agent 然后这家公司竟然给我过了,可能看我综合表达能力还行(,也可能真的缺人,这家依旧是20多人的小公司,在拱墅区离我这边通勤也是有一个钟,原来医疗那家大概半个多钟真的很快而且双休且早九晚六; **后续安排:** 专心把agent剩余内容完成,然后部署上线改简历、每天背八股日常总结、完成后可投agent和传统后端

中科软 一面

1. 最近做的这些项目里面,有遇到哪些印象深刻的点和。然后或者是遇到哪些难点、困难点,然后怎么解决的? 2. Redis缓存有用过吗?它里面分布式锁有用过吗?就是它实现分布式锁的原理,你了解过吗?集群模式有了解过吗? 3. Redis它速度快的原理是什么?为什么要用它作缓存。 4. MySQL里面索引,索引的数据结构是什么?B+树,B树、红黑树这些有什么区别?然后索引的创建原理是什么?我们什么情况下需要创建索引?什么情况件会导致索引失效呢? 5. 事物有了解过吗?事物的特性是什么?并发事物有一些问题,就是它脏读,还有不可重复读,还有幻读,这几个各的是什么情况? 6. spring Bean中的生命周期分哪几步?创建时存在循环依赖。循环依赖问题怎么解决?说一下Spring Mvc的执行流程?它一个请求打到控制层,包括后面是怎么处理的?

杭州靖安科技 一面

1.自我介绍 2.Arraylist和linkedlist区别,遍历复杂度多少 3.什么是栈?什么是二叉树? 4.二叉树用在哪种数据结构中?HashMap;那么HashMap线程安全吗?数据量小的时候会是二叉树吗 5.数据库三范式 没答上来 1NF原子性 2NF唯一性 3NF独立性 6.操作系统分哪几种?没答上来 批处理、分时、实时、网络、分布式、个人 7.冯诺依曼机包含哪几部分? 包括显卡吗? 为什么显卡现在用了这么多 8.TCP3次握手 9.Java中怎么创建线程? 如果想这个线程执行完有返回值呢? 10.线程池的类型 没答上来 11.JDK的双亲委派机制 12.垃圾回收器有哪几种,详细解释一下G1 没答上来 13.为什么Java程序有时候会卡一下 14.什么是跨域,引起的原因有哪些 15.前端交互的时候为什么要做防抖 16.介绍一下实习项目 17.Skill和mcp有什么区别 18.实习中有什么难点或亮点 19.agnet开发和前后端开发有什么区别? 20.什么是切面编程,怎么实现的? 21.什么是单例?创建单例的几种方法

面试官问我"设计一个秒杀系统",我聊了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 计算都有上限,分库将数据分散到不同数据库实例,提升整体并发处理能力。 - **垂直分库:** 按业务模块拆分(如订单库、用户库、商品库) - **水平分库:** 同一业务数据按规则分散到多个数据库实例

下载 APP