ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从MySQL到Redis:一条知识链串起面试高频考点与工程实践

从MySQL到Redis:一条知识链串起面试高频考点与工程实践

简介:这是一份面向IT岗位求职者的高频面试题合集(PDF),定位为互联网大厂技术面试的“八股文”速查手册,适合正在准备Java后端、开发工程师等岗位的候选人。资料覆盖Redis、MySQL、算法、线程、数据结构及Java核心基础等常见考点,不仅罗列问题,还给出要点式解析与代码片段,帮助读者在有限时间内快速回顾底层原理、梳理面试话术。虽然这些理论知识在实际编码中未必每天直接使用,但作为判断基础功底的标尺,在面试环节非常关键。整个资源包共1个PDF文件,大小约25.8MB,内容按模块分节,包含B/S与C/S架构、Java开发平台、JDK/JRE、面向对象特征、数据类型、拆装箱、instanceof用法、类与对象关系等高频题目,并附有典型示例和易错提醒,适合通勤或碎片时间翻阅。目前已有2484人学习下载,是冲刺面试、查漏补缺的实用参考。

1. 面试八股文不是用来背的:把 Redis、MySQL、线程、算法串成一条知识链

八股文这个说法在编程面试里带着点自嘲,但真到了投简历的关头,它又是绕不过去的硬通货。我见过好几个准备跳槽的开发者,把 MySQL、Redis、线程池、算法、数据结构的高频题背了三个月,打开自己的笔记全会,一上考场全废。问题多半不在记忆力,而在把面试题当成了孤立的问答,没有把考点连成一条可以推导的知识链。这篇博客要做的,就是给你一条主线,把 MySQL 索引、Redis 缓存、线程池参数、算法模板、数据结构串在一起,并给出一套能直接照做的复习和自检方法。它适合正在准备跳槽的中间层开发,也适合秋招春招前复习得心里没底的应届生——按这条线整理一遍,比盲目刷题库要稳得多。

2. MySQL 和 Redis 一起复习:一张电商链路图串起两座题库

2.1 为什么这两块要放一起看:很多题其实是一道题的两面

MySQL 和 Redis 在多数面经里被分成两个文件夹,但真实面试官喜欢把它们揉在一起问。拿缓存穿透来说,题干在 Redis,标准答案是“查不到就回源数据库”,可追问一定会落回 MySQL:数据库扛不住怎么办?这时要答索引、连接数、限流熔断。再比如说双写一致性,面试官问“先更新数据库还是先删缓存”,表面是 Redis 的场景题,底层却牵涉 MySQL 事务提交时机和 binlog 订阅。我一般建议先在脑子里钉一张电商下单链路图:请求先查 Redis,命中直接返回,没命中回源 MySQL,写操作走 MySQL 事务,提交成功后通过 binlog 异步刷新 Redis 缓存。一条链路下来,两个领域的题有了同一个抓手,背起来就不再是两本互不相干的册子。

这张图还能帮你判断真正该背哪一类题。比如链路里“缓存没命中回源”这一个环节,就同时引出 Redis 的穿透、击穿方案和 MySQL 的索引优化。复习时按链路逐个环节过,比按“Redis 题库第 12 题、MySQL 题库第 7 题”这种顺序背有效得多。我自己的习惯是先在纸上画这条链路,然后把每道高频题贴到对应的环节下面。题和题之间的关系一旦画出来,记忆量其实会下降一大截,因为很多答案本来就是从上一个环节推导出来的。

2.2 MySQL 考点地图:先看执行计划,再背锁和日志

MySQL 高频考点可以拆成四类:索引与执行计划、事务与隔离级别、锁与死锁、日志机制(binlog、redo log、undo log)。最容易翻车的是第一类:能背出“联合索引遵循最左前缀”,但给一条慢 SQL,不知道打开 EXPLAIN 验证。我建议准备时用表格列一张考点地图,只给每类留三个常问方向:

  • 索引:回表、覆盖索引、最左前缀、索引下推
  • 事务:ACID、隔离级别、MVCC 快照
  • 锁:行锁、间隙锁、死锁检测
  • 日志:redo log 崩溃恢复、binlog 主从

然后一定要落到真实表上验证。下面是我平时用来练手的最小表结构:

CREATE TABLE `order_t` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB; EXPLAIN SELECT * FROM order_t WHERE user_id = 123 AND status = 1;

这里idx_user_status是联合索引,列顺序刻意把user_id放前面。EXPLAIN 结果里重点看四列:type 是不是 ref 或 range(出现 ALL 就要警惕全表扫描)、key 是不是真用了这个联合索引、rows 估算扫了多少行、Extra 里有没有 Using filesort。这套命令跑一遍,比你背二十条索引结论都管用。

参数说明:user_id用 BIGINT 是给未来数据量留余量,status用 TINYINT 而不是 INT,是因为状态值就那么几个。索引体积越小,一个数据页能装下的索引条目就越多,扫描成本就越低。联合索引字段顺序按“等值条件放前、范围条件放后”来排,所以这里user_id在前、status在后;反过来建索引,WHERE status = 1单独查就走不上。

提示:EXPLAIN 的 type 列出现 ALL 时,先看有没有合适的索引可用,再考虑改 SQL。很多时候不是 SQL 写错,而是索引没建对。

再补一个常见做法:面试官问索引失效场景,不要背那五句口诀。打开 EXPLAIN,把WHERE create_time BETWEEN ...、WHERE user_id = 123 OR status = 1各跑一遍,自己看 type 怎么从 ref 掉到 index 再到 ALL。记住“数据是怎么被扫的”,比记“失效口诀”牢靠。能说出“这个查询扫描行数为何从 3 涨到 3000,因为索引无法下推到 status 范围”这种话,面试官就知道你不是背的。

2.3 Redis 三大缓存问题与双写一致性:辨析题加最小防御 Demo

Redis 面试题里出现频率最高的是穿透、击穿、雪崩三兄弟。穿透是查一个根本不存在的数据,缓存和数据库都没有;击穿是某个热点 key 过期的一瞬间,大量请求直接砸到数据库;雪崩是大量 key 在同一时段过期,数据库瞬间被打满。三种现象的后果都是数据库压力陡增,但原因和防御方案完全不同,背的时候千万别合并成一条。

穿透的常见防御有两层:布隆过滤器挡在 Redis 前面,或者把查不到的空值也缓存 30 到 60 秒。布隆过滤器在 Java 里用现成的封装最快,比如下面这段:

// 布隆过滤器:预估元素 10 万,误判率 1% RBloomFilter<String> bloom = redisson.getBloomFilter("userIdFilter"); bloom.tryInit(100000L, 0.01); if (bloom.contains(userId)) { // 可能在库中,继续查缓存,没命中再回源数据库 String value = redisTemplate.opsForValue().get(key); if (value == null) { value = loadFromDb(userId); } } else { // 一定不存在,直接返回,不打数据库 return null; }

参数说明:tryInit的两个参数是预估元素数量和误判率。误判率从 1% 降到 0.1%,位数组大小和哈希次数都要上涨,内存占用可能翻倍,所以线上通常把误判率定在 1% 到 0.5% 之间。要注意布隆过滤器不支持删除,如果业务里经常有“删掉用户再重建”的场景,它没法精确感知,用空值缓存兜底更直接。

击穿的防御要点是“热点 key 重建要串行化”。常见做法是互斥锁:缓存过期后,只让一个线程回源数据库重建,其余线程先等锁,或者直接拿旧值返回。雪崩的防御要点是“错峰过期”,我一般会给缓存过期时间加一个 30 到 300 秒的随机数,再配合多级缓存,让 key 不要同时失效。这三道题各对一种方案,一定要说清“为什么这种方案只治这一种病”,这是面试官区分背答案和真理解的关键提问。

双写一致性是这几道题的延伸版。常见做法是“更新数据库后删缓存”,删除失败要有补偿,常见补偿是消息队列重试,或者订阅 binlog 异步删除。这里有个老坑:为什么不先删缓存再更新数据库?因为删完缓存、还没更新数据库的间隙里,其他请求会把旧值重新写回缓存,缓存里存的还是脏数据。用“先更库后删缓存 + 重试”可以把脏读窗口压到很小,但做不到绝对零。面试时主动说出“这个方案有短暂的脏读窗口,用过期时间兜底”,比只会背方案强很多,因为这已经是在讲系统设计的权衡了。

3. 线程与并发:从 JMM 到线程池,一条追问链打穿高频题

3.1 volatile 到底能保证什么:一道题能引出四道题

“volatile 能保证原子性吗”是并发题里的送命题。完整答法是:volatile 保证可见性和有序性,禁止指令重排,但不保证原子性。写操作i++本质是读-改-写三步,volatile 管不住中间被其他线程插一脚,要原子性得用AtomicInteger或者加锁。面试官听到这里,大概率会接着问“为什么 i++ 不是原子的”,这就引出 JMM 的主内存和工作内存模型,然后再问“那 synchronized 和 volatile 的区别是什么”。一道题开场,最后变成三四道题,复习密度很高。

推荐用双重检查锁单例来验证自己是不是真懂,因为它是 volatile、synchronized、可见性三者的综合体:

class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一层检查,避免每次都抢锁 synchronized (Singleton.class) { if (instance == null) { // 第二层检查,防止重复创建 instance = new Singleton(); } } } return instance; } }

这段代码里 volatile 的经典作用不是修饰锁,而是防止new Singleton()这一步指令重排。正常逻辑是分配内存、初始化对象、把引用赋值给 instance;重排后可能变成分配内存、先赋值引用、再初始化。另一个线程在赋值后、初始化前读到 instance 不为 null,就会拿到一个半初始化对象。加了 volatile,赋值操作前的初始化步骤不允许被重排到赋值之后。

参数说明:这里没有参数,考察的是两个 synchronized 块之间为什么还要再判一次空。第一次判空是性能优化,避免每次都抢锁;第二次判空才是正确性的保证,防止两个线程同时进入第一层检查后各自创建一个实例。我把这个例子理解透之后,volatile、synchronized、JMM、指令重排这四块题基本是一串答案讲下来,不用再分开背。

3.2 线程池七个参数:别背数字,要会推导

线程池是“背了参数名但讲不出为什么要这样设”的重灾区。ThreadPoolExecutor 的七个参数里,真正需要推导的是核心线程数、最大线程数和队列长度。先看一段常见配置:

ThreadPoolExecutor pool = new ThreadPoolExecutor( 2, // 核心线程数:常驻 4, // 最大线程数:忙时可扩 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue<>(100), // 有界队列:防止任务无限堆积 new NamedThreadFactory("order-"), // 自定义线程名,排查日志用 new ThreadPoolExecutor.AbortPolicy() // 满了直接抛异常 );

参数说明:核心线程数按任务类型推导。CPU 密集型任务,常见经验值是 CPU 核数加 1,多个线程同时跑计算,线程数超过核数多了反而在上下文切换上浪费 CPU;IO 密集型任务,线程大部分时间在等待输入输出,可以按 2 倍 CPU 核数起步,更精确的估算公式是N / (1 - 阻塞因子),阻塞因子越高线程数应该越多。队列长度要结合任务峰值和消费速度算,100 只是演示值,线上需要压测后确定。核心原则是“宁可触发拒绝策略,也不要让队列无限堆积把内存打爆”。

拒绝策略也常被追问。AbortPolicy 直接抛异常,适合不能丢任务的场景;CallerRunsPolicy 让提交任务的线程自己跑,天然起到背压作用,适合希望任务降速而不是丢弃的场景;DiscardPolicy 和 DiscardOldestPolicy 会静默丢任务,用之前一定要想清楚业务能不能接受。面试官问“你线上怎么选的”,把这两个场景答出来就足够,不用编造复杂背景。

3.3 手写一个简化版线程池:检验自己是不是真懂了

参数背得再熟,不如手写一次。我面试前会花十分钟写一个只有两个组件的简化版线程池,用来检验自己是不是真的理解了“提交任务、队列、worker 拉取”这三件事:

class SimplePool { private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(16); private final List<Thread> workers = new ArrayList<>(); SimplePool(int n) { for (int i = 0; i < n; i++) { Thread t = new Thread(() -> { while (true) { try { Runnable task = queue.poll(5, TimeUnit.SECONDS); if (task != null) { task.run(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); t.start(); workers.add(t); } } void execute(Runnable task) { queue.offer(task); } }

逻辑说明:线程池的本质就是“一个任务队列加上一组固定 worker 线程”。worker 循环从队列里取任务执行,取不到就阻塞等待或超时退出。这段代码只有三十多行,但已经覆盖了核心线程、阻塞队列、任务提交三个核心环节。队列用LinkedBlockingQueue,offer和poll都是带阻塞语义的,不会出现忙轮询把 CPU 打满的问题。

面试时千万不要只说“我写了个简化版”,要把残缺的部分主动补齐:它没有拒绝策略,队列满了offer会失败;它没有线程状态管理,worker 线程不能优雅关闭;它没有核心线程和最大线程的扩缩容逻辑。能把“这个版本缺什么、JDK 版本是怎么补上的”讲出来,才是手写代码的真正加分点。

提示:手写线程池代码在面试里不需要跑通,重点是能指出它缺了什么。主动讲缺陷比把代码写得完整更抓分。

4. 算法与数据结构:用 30 道母题覆盖 80% 考点,背模板不如写模板

4.1 先把题海切块:按题型选母题,不要按难度刷

算法题是很多人最没底的一块,因为题库看起来无穷无尽,但面试真正高频的题型其实是有限的。把题切成块,每块只留三到五道母题:链表(反转链表、环形链表、合并有序链表)、二叉树(层序遍历、最近公共祖先、路径和)、滑动窗口(无重复最长子串、最小覆盖子串)、双指针(两数之和、有序数组去重)、DFS 与 BFS(岛屿数量、二叉树层序)、动态规划(斐波那契、背包、最长递增子序列)、栈与单调栈(有效括号、每日温度)。全部加起来控制在 30 到 40 道,覆盖数据结构里的链表、树、栈、队列,也覆盖算法里的搜索和动态规划。

选母题的标准是“能不能改装”。比如反转链表是链表题的地基,几乎一半链表题都是反转的变体;滑动窗口那两道题改改窗口收缩条件,就能覆盖最长、最短、定长一大类问题。刷题时每道母题都要做到“闭上眼能默写核心逻辑”,而不是“见过、有印象”。我自己刷题的习惯是每块题型建一个笔记,记母题的模板代码和两道变体,变体只记差异点,不重复贴整段代码,复习时一眼就能看到演化关系。

4.2 两个高频模板:滑动窗口改一版,二叉树层序背一版

以“无重复字符的最长子串”为例,滑动窗口的实现可以固定成三步模板:

def length_of_longest_substring(s: str) -> int: last_seen = {} # 字符上一次出现的下标 left = 0 # 窗口左边界 ans = 0 # 记录最长长度 for right, ch in enumerate(s): # 遇到重复字符,把左边界跳到重复位置之后 if ch in last_seen and last_seen[ch] >= left: left = last_seen[ch] + 1 last_seen[ch] = right ans = max(ans, right - left + 1) return ans

参数说明:last_seen记录每个字符最后一次出现的下标,left是窗口左边界,right - left + 1是当前窗口长度。关键逻辑是,当发现当前字符之前出现过并且出现位置还在窗口内,就把左边界直接跳到那个位置之后,保证窗口内没有重复字符。这个模板用下标跳跃而不是集合去重,时间复杂度是 O(n),每个字符最多被左边界和右边界各扫一次。把这道题改成“最多有两个不同字符的最长子串”,只需要把last_seen换成统计字符次数,收缩条件改成“字符种类超过 2 时左边界右移”。一个模板配上收缩条件的改动,就能覆盖滑动窗口这一大类的多数变体。

二叉树层序遍历是数据结构“队列”的典型应用,也是树这一块必考的高频题:

from collections import deque def level_order(root): if not root: return [] queue = deque([root]) result = [] while queue: level = [] for _ in range(len(queue)): # 固定当前层大小 node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) return result

逻辑说明:核心是进入循环后用len(queue)固定当前层的节点数,再逐一出队。如果不先记录这个长度,for循环里一边出队一边入队,队列会混入下一层节点,边界就分不清了。deque的append和popleft都是 O(1),比用 Python 列表头插和pop(0)高效得多。面试问“层序遍历和 BFS 什么关系”,直接说 BFS 用队列实现,层序是 BFS 按层输出的版本,这两道题的知识点就打通了。

4.3 复杂度自查:写完题先写复杂度和最坏情况

很多人算法题写对了,最后挂在复杂度分析上。面试官的基本问法是“时间复杂度和空间复杂度是多少”“最坏情况是什么”。我要求自己任何一道题写完,先随手写一行注释标注复杂度,再准备一句最坏情况的说明。比如二分是 O(log n) 但前提是数据有序;DFS 遍历图的节点和边,时间和空间都是 O(V+E);动态规划题经常可以把空间从 O(n) 优化成 O(1),这些都是开口就能加分的点。

关键教训是别在复杂度上凭感觉。常见做法是刷题时把复杂度写在代码顶部注释里,复盘时专门看“最坏情况有没有覆盖”。比如滑动窗口时间 O(n) 是因为每个元素最多进出窗口一次,这一句一定要能讲出来;讲不出来就说明对算法过程的理解还有漏洞。我把复杂度分析当成和写代码同等重要的练习,几次面试下来,这一步的收益比多刷三十道题都大。

5. 背八股最容易翻车的 5 个坑:现象、原因、解决

5.1 坑一:按题背不按原理背,换一个问法就挂

现象:把“Redis 为什么快”背得滚瓜烂熟,面试官改成“Redis 单线程为什么还能抗住高并发”,当场卡壳。原因:背的是结论列表,没理解背后的因果链。解决:背完一道题,用“为什么”反问自己三次。比如 Redis 快不是因为单线程本身,而是纯内存访问、IO 多路复用避免了大量线程切换、数据结构设计精炼,单线程反而省掉了锁竞争。能顺着因果链讲下来,问法怎么变都能接住。

5.2 坑二:只看结论不看数据,一问到具体数值就崩

现象:知道 RDB 和 AOF 的区别,面试官追问“AOF 的 everysec 刷盘策略崩溃时最多丢多少数据”,答不上来。原因:背机制的时候没有把数据指标一起背。解决:给每个机制配一组数字。AOF everysec 最多丢 1 秒数据;缓存过期时间的随机范围按分钟级设计;线程池队列长度按峰值任务量估算;索引列长度看区分度。面试官问数字,答得具体才说明你真实配置过,而不是只看了概念图。

5.3 坑三:算法只刷不写复杂度,一追问就露馅

现象:写对了反转链表,面试官问“递归写法的空间复杂度”,回答“应该是 O(1)”,实际递归栈是 O(n)。原因:只关注代码能不能跑,没分析调用栈深度。解决:每次写完题,第一时间写时间和空间复杂度,特别是递归深度和额外数据结构。如果拿不准,就在本子上画一遍调用栈,从递推到回溯数一遍栈帧,比靠感觉猜可靠得多。

5.4 坑四:MySQL 只记概念,不会看执行计划

现象:把“最左前缀、回表、覆盖索引”背得滚瓜烂熟,给一条真实的慢 SQL,不知道从哪下手排查。原因:没有把概念落到工具上。解决:从自己项目的慢日志里找一条慢查询,建一张测试表,跑一次 EXPLAIN,把 type、key、rows、Extra 四列抄下来逐个解释。这个动作做三次,索引题基本不会再翻车。这也是我把 EXPLAIN 放在 MySQL 复习第一步的原因——概念是为解读执行计划服务的。

5.5 坑五:从不模拟面试,以为会了其实一开口就乱

现象:笔记翻得烂熟,一到真实面试,回答前言不搭后语,说完东忘了西。原因:输出比输入难,背和讲是两种能力。解决:自己录音讲题,或者找朋友模拟。我常用的自检方式是把手机录音打开,限时三分钟讲清楚“这个方案解决什么问题、怎么做、有什么代价”,回放时数一数哪里卡壳。卡壳的地方,就是知识链上还没接住的那个结点。这个练习一次不需要太久,但比默背十遍管用,因为它逼着你把零散的句子组织成有逻辑的回答。

6. 进阶技巧:用“三段式讲法”把八股文变成面试里的加分项

八股文背到最后一个阶段,目标不再是把答案复述出来,而是让面试官感觉你在讲一个做过的方案。我把这个技巧总结成三段式:场景、方案、代价。任何一道场景题,都用这个框架组织答案,答完之后自然显得有工程味道。

以双写一致性为例。场景:下单接口并发高,缓存和数据库之间的数据一致性要保证。方案:更新数据库后删缓存,删除失败走消息队列重试;数据量再大一点,可以订阅 binlog 异步删除。代价:删缓存和更新成功之间存在一个极短的脏读窗口,无法做到绝对一致,方案里必须加一个兜底过期时间。这三段讲完,答案就不是背出来的,而是一个有取舍的工程设计,面试官很难把你当成只会背题的人。

这个框架同样适用于纯概念题。比如问“synchronized 原理”,可以这样组织:场景是多个线程写同一个变量;方案是用对象的 Monitor 锁实现互斥;代价是锁竞争带来上下文切换,所以高并发场景要优先选无锁方案或减少锁粒度。一个成熟的回答,永远不是评价“它好”,而是讲清楚它在什么条件下解决了什么问题。这样答,即使面试官追问“那 ReentrantLock 呢”,你也可以沿用同一个框架,把它当作对 synchronized 的一个补充方案来讲。

我最后用来验收自己的方法是限时讲题练习:从每个复习板块里随机抽三道题,每道题只给自己五分钟,手机录音,用三段式讲完。讲不顺的题目标记出来,当天补齐。整个过程大约二十分钟,但比闷头翻笔记高效得多。坚持两轮之后,我明显感觉到面试时不是在想答案,而是在组织语言,那种状态差别非常大。这也是我走过弯路之后最想分享的一条经验:八股文不是黑匣子,它需要过三遍——第一遍整理成知识链,第二遍把每个结论补上原理和数据,第三遍开口讲出来。前两遍决定你能不能过简历关,第三遍决定你能不能过面试关。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进