文章目录
-
- 前言
- [第一章 网络篇:面试第一关,不懂网络等于白搭](#第一章 网络篇:面试第一关,不懂网络等于白搭)
-
- [1.1 TCP三次握手:别再只会背SYN、ACK了,搞懂为啥是三次](#1.1 TCP三次握手:别再只会背SYN、ACK了,搞懂为啥是三次)
- [1.2 TCP四次挥手:为啥分手比牵手多一步?](#1.2 TCP四次挥手:为啥分手比牵手多一步?)
- [1.3 TCP和UDP,别再只会说"面向连接和无连接"了](#1.3 TCP和UDP,别再只会说“面向连接和无连接”了)
- [1.4 HTTP/HTTPS/HTTP3,2026年面试必问的演进](#1.4 HTTP/HTTPS/HTTP3,2026年面试必问的演进)
- [1.5 面试必背的HTTP状态码,别再只知道200、404、500了](#1.5 面试必背的HTTP状态码,别再只知道200、404、500了)
- [第二章 操作系统篇:懂了OS,才算真正懂了计算机](#第二章 操作系统篇:懂了OS,才算真正懂了计算机)
-
- [2.1 进程和线程,别再背定义了,搞懂本质区别](#2.1 进程和线程,别再背定义了,搞懂本质区别)
- [2.2 进程间通信(IPC),6种方式一次性讲透](#2.2 进程间通信(IPC),6种方式一次性讲透)
- [2.3 内存管理:虚拟内存、分页、分段,通俗讲明白](#2.3 内存管理:虚拟内存、分页、分段,通俗讲明白)
- [2.4 死锁:怎么产生的?怎么预防?](#2.4 死锁:怎么产生的?怎么预防?)
- [2.5 协程:2026年高频考点,和线程到底啥区别?](#2.5 协程:2026年高频考点,和线程到底啥区别?)
- [第三章 数据库篇:CRUD程序员的面试生死线](#第三章 数据库篇:CRUD程序员的面试生死线)
-
- [3.1 索引:为啥加了索引就快?B+树到底牛在哪?](#3.1 索引:为啥加了索引就快?B+树到底牛在哪?)
- [3.2 事务ACID,别再只会念缩写了](#3.2 事务ACID,别再只会念缩写了)
- [3.3 隔离级别:脏读、幻读、不可重复读,一次分清](#3.3 隔离级别:脏读、幻读、不可重复读,一次分清)
- [3.4 锁机制:行锁、表锁、乐观锁、悲观锁,面试必问](#3.4 锁机制:行锁、表锁、乐观锁、悲观锁,面试必问)
- [3.5 SQL优化,2026年面试必踩的坑,别再写慢SQL了](#3.5 SQL优化,2026年面试必踩的坑,别再写慢SQL了)
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
兄弟们,先问个扎心的问题:2026年了,你是不是投了几十家公司,面了十几场试,不管是面Java后端、Python开发、Go工程师,还是AI算法工程、大模型应用开发,十场面试有八场,面试官上来先给你甩三个灵魂拷问:
"说说TCP三次握手和四次挥手的细节?"
"进程和线程到底有什么区别?"
"数据库索引为什么用B+树,不用二叉树?"
是不是瞬间就懵了?平时写CRUD写得丝滑流畅,靠着GPT一天能生成上百个接口,连单元测试都能一键生成,结果一到面试,被这些底层基础问得支支吾吾,背了无数遍的八股文,面试官一追问"为啥是三次握手不是两次?""幻读到底怎么解决?",当场就哑火,恨不得找个地缝钻进去?
我在AI行业摸爬滚打了22年,面过的候选人没有一千也有八百,尤其是2026年这两年,见了太多这种离谱的情况:有个小伙子,靠着开源框架1小时就能搭出一个能跑的AI智能体,大模型prompt玩得炉火纯青,结果一面被问TCP和UDP的区别,连面向连接和无连接的本质都讲不清;还有个干了8年的后端老哥,写的业务代码线上跑了几年没出过大问题,结果被问数据库事务隔离级别,连脏读和不可重复读都分不清楚。
很多兄弟都说,现在都大模型时代了,AI都能帮我们写代码了,还学这些底层八股文有啥用?我每次听到这话都想笑。你以为面试官问你这些,是想让你背定义?错了!他们是想通过这些问题,看你到底懂不懂计算机的底层逻辑,看你线上出了问题的时候,能不能定位到根因,而不是只会对着GPT复制粘贴代码,出了bug就抓瞎。
就像你开了十几年车,总不能连油门刹车、发动机基本原理都不懂吧?路上抛锚了,总不能等着救援来,连个基本的故障排查都做不到?写代码也是一个道理,不管你是做业务开发,还是做AI大模型落地,网络、操作系统、数据库,就是计算机世界的三大基石,也是面试永远绕不开的三座大山。
这篇文章,我就把这三个模块面试必问的核心点,一次性给大家讲透,不用高深的数学,不用晦涩的术语,就用大白话+生活段子,结合2026年最新的面试考点,让你看完不仅能背下八股文,还能懂背后的本质,面试的时候不管面试官怎么追问,都能对答如流。
第一章 网络篇:面试第一关,不懂网络等于白搭
兄弟们,做开发的,不管你写的是前端还是后端,是AI服务还是客户端应用,所有的程序交互,最终都要走网络。你写的接口为啥超时了?前端请求为啥跨域了?线上服务为啥端口耗尽了?追根溯源,全是网络的问题。这也是为啥面试官永远第一个问网络相关的问题,这一关过不了,后面技术再牛也白搭。
1.1 TCP三次握手:别再只会背SYN、ACK了,搞懂为啥是三次
首先就是面试100%会问的TCP三次握手,我敢说,90%的程序员都能背下来"客户端发SYN,服务端回SYN+ACK,客户端再回ACK",但是80%的人,被面试官问一句"为啥是三次?两次不行吗?四次不行吗?",当场就卡壳。
先给大家打个通俗的比方,TCP三次握手,就像你和朋友打电话,得确保双方都能听得见对方说话,才能正常聊天。
- 第一次握手:你拿起电话,拨了号,对着话筒喊:"喂?能听到我说话吗?" 这就是客户端给服务端发SYN报文,告诉服务端:我想和你建立连接,我的初始序列号是X。
- 第二次握手:你朋友听到了,对着话筒回:"我能听到!你能听到我说话吗?" 这就是服务端给客户端回SYN+ACK报文,ACK是确认收到了你的SYN,SYN是告诉客户端:我也想和你建立连接,我的初始序列号是Y。
- 第三次握手:你听到了朋友的回话,再回一句:"我也能听到你说话!那咱们开始聊吧!" 这就是客户端给服务端回ACK报文,确认收到了服务端的SYN,连接正式建立。
那为啥不能是两次?咱们来看看,如果只有两次握手,会出什么幺蛾子。
还是拿打电话举例子,你第一次打过去,信号不好,没打通,你又打了第二次,这次通了,你们聊完挂了电话。结果这时候,第一次那个没打通的电话信号,突然飘到你朋友手机里了,你朋友一接,听到你喊"喂?能听到我说话吗?",立马回了一句"我能听到!你能听到我吗?",按照两次握手的规则,这时候连接就建立了。
但是你这边呢?你根本就没在打电话,根本不会理会你朋友的回话,结果你朋友就一直拿着电话等你说话,白白占着电话线,浪费资源。
放到TCP里也是一样,客户端发送的SYN报文如果在网络里滞留了,超时之后客户端又重发了一个,服务端收到新的SYN,建立连接,传输完数据释放了。结果这时候那个滞留的SYN又跑到服务端了,服务端收到后,按照两次握手的规则,立马建立连接,但是客户端根本没有要建立连接的需求,不会给服务端发任何数据,服务端就一直维护着这个空闲的连接,白白浪费服务器的资源,尤其是高并发场景下,这种无效连接多了,服务器直接就被拖垮了。
那为啥不能是四次?其实三次就已经能确保双方的收发能力都正常了,四次完全是多此一举。就像打电话,你问一句,对方回一句同时问你,你再回一句,就已经确认双方都能听得到了,没必要对方把"我能听到"和"你能听到吗"拆成两句话说,平白多了一次交互,增加了网络延迟。
2026年了,面试官还会追问一个高频问题:三次握手过程中,服务端收到SYN之后,处于什么状态?客户端收到SYN+ACK之后,处于什么状态?这里给大家记死了:服务端收到SYN,回复SYN+ACK之后,处于SYN_RCVD状态;客户端收到SYN+ACK,回复ACK之后,处于ESTABLISHED状态;服务端收到最后一个ACK,也进入ESTABLISHED状态,连接正式建立。
还有个必问的点:SYN洪水攻击是什么?怎么防护?其实就是利用了三次握手的机制,攻击者伪造大量的虚假IP,给服务端发SYN报文,服务端每次收到都要回复SYN+ACK,然后维护一个半连接,虚假IP根本不会回ACK,服务端的半连接队列很快就被占满了,正常的SYN请求都处理不了,服务就瘫痪了。防护方式也很简单,比如调大半连接队列、开启SYN Cookie、缩短超时时间,这些面试的时候说出来,面试官立马就知道你不是只会背八股。
1.2 TCP四次挥手:为啥分手比牵手多一步?
讲完了握手,就到了挥手,也就是TCP连接的释放,面试也是必问。很多兄弟都纳闷,为啥建立连接三次就够了,释放连接要四次?
还是先给大家打个比方,握手是两个人商量着要一起聊天,得双方都同意;挥手就是两个人聊完了,要挂电话,得双方都确认自己没话说了,也确认对方没话说了,才能挂。
TCP是全双工通信,什么意思?就是客户端能给服务端发数据,服务端也能给客户端发数据,两条通道是独立的。所以释放连接的时候,得两条通道分别关闭,这就是为啥需要四次挥手。
咱们还是用大白话拆解四次挥手的过程:
- 第一次挥手:客户端给服务端发FIN报文,告诉服务端:"我这边数据发完了,我不想再给你发数据了,我要关闭我这边的发送通道了。" 这时候客户端进入FIN_WAIT_1状态。
- 第二次挥手:服务端收到FIN之后,给客户端回ACK报文,告诉客户端:"我收到你的关闭请求了,我这边还有数据没发完,你先等我发完。" 这时候服务端进入CLOSE_WAIT状态,客户端收到ACK之后,进入FIN_WAIT_2状态,等着服务端发FIN。
这里就是关键了!握手的时候,服务端的SYN和ACK可以一起发,但是挥手的时候,ACK和FIN不能一起发,因为服务端可能还有数据没传完,不能立马关闭发送通道,得先确认收到了客户端的关闭请求,等自己的数据发完了,再发FIN告诉客户端我也关了。这就是为啥挥手比握手多了一步。
- 第三次挥手:服务端把所有数据都发完了,给客户端发FIN报文,告诉客户端:"我这边数据也发完了,我也要关闭我这边的发送通道了。" 这时候服务端进入LAST_ACK状态。
- 第四次挥手:客户端收到FIN之后,给服务端回ACK报文,告诉服务端:"我收到你的关闭请求了,你可以关了。" 这时候客户端进入TIME_WAIT状态,服务端收到ACK之后,就进入CLOSED状态,连接释放了。
这里还有个面试必问的点:客户端发完最后一个ACK之后,为啥要等2MSL才关闭连接?MSL就是报文在网络里的最大生存时间。
还是用大白话讲,两个原因:
第一,怕最后一个ACK报文丢了。如果客户端发完ACK直接关了,结果这个ACK在网络里丢了,服务端没收到,就会一直重发FIN报文,但是客户端已经关了,不会理会,服务端就永远关不了连接。等2MSL的话,如果ACK丢了,服务端重发的FIN能在2MSL内到客户端,客户端就能重发ACK,重新计时2MSL,确保服务端能收到ACK,正常关闭。
第二,防止"失效报文"。等2MSL,能让本次连接里所有滞留的报文都在网络里消失,不会影响下一次新的连接。
2026年面试,还有个高频追问:CLOSE_WAIT状态过多是什么原因?怎么解决?兄弟们记好了,出现大量CLOSE_WAIT,基本都是服务端的代码写得有问题,客户端发了FIN关闭连接,服务端没调用close()方法关闭连接,导致一直停在CLOSE_WAIT状态。解决方式就是检查代码,确保连接释放的时候,正确关闭文件描述符,别只关客户端的,服务端的忘了关。
1.3 TCP和UDP,别再只会说"面向连接和无连接"了
面试的时候,问完三次握手四次挥手,面试官大概率会问:TCP和UDP有啥区别?分别用在什么场景?
很多兄弟上来就背:TCP面向连接,可靠;UDP无连接,不可靠。这话没错,但是太干了,面试官想听的是背后的本质,以及你到底会不会选。
先给大家用通俗的例子讲明白两者的核心区别:
TCP就像寄顺丰快递,你寄快递之前,得先和快递员确认地址,快递员上门取件,给你个单号,全程物流跟踪,丢了包会给你重发,顺序乱了会给你排好,确保收件人能完整、按顺序收到你寄的所有东西。但是缺点就是,流程多,开销大,速度慢一点。
UDP就像寄平信,你写好信,往邮筒里一扔就完事了,不用和邮局提前打招呼,也不知道信能不能寄到,不知道寄到的时候顺序对不对,甚至丢了都没人管。但是优点就是,快,开销极小,想发就发,不用等。
所以TCP的核心特性:面向连接、可靠传输、字节流、拥塞控制、有序传输,丢包重传,适合对数据完整性要求高的场景,比如HTTP/HTTPS请求、文件传输、邮件发送、远程登录,这些场景里,数据丢一个字节都不行,必须完整准确。
UDP的核心特性:无连接、不可靠、面向数据报、无拥塞控制、传输速度极快,延迟极低,适合对实时性要求高,但是允许少量丢包的场景。2026年了,UDP的应用场景越来越多,比如直播、视频通话、游戏、实时音视频,还有现在的HTTP3,底层就是用的UDP,就是为了低延迟。
这里给大家避个坑,很多人觉得UDP不可靠,就没用,这是大错特错。不可靠只是说UDP协议本身不保证可靠,但是我们可以在应用层自己实现可靠传输,比如加序号、加确认、重传机制,既保留了UDP的低延迟,又能保证数据可靠,现在很多实时音视频框架,都是这么做的。
1.4 HTTP/HTTPS/HTTP3,2026年面试必问的演进
2026年了,面试官已经不满足于只问你HTTP1.1了,HTTPS、HTTP2、HTTP3,都是必问的点,尤其是HTTP3,现在国内大厂的接口基本都已经适配了,不懂这个,面试很容易吃亏。
先从最基础的HTTP1.1讲起,大家天天都在用,但是很多人不知道它的核心问题。HTTP1.1是基于TCP的,最核心的问题就是队头阻塞:同一个TCP连接里,同一时间只能处理一个HTTP请求,前面的请求没处理完,后面的请求只能等着,哪怕后面的请求很简单,也得排队。虽然HTTP1.1支持长连接,也支持管道化,但是管道化的问题太多,基本没浏览器用,队头阻塞的问题根本没解决。
然后是HTTPS,很多人只知道HTTPS比HTTP安全,但是不知道为啥安全。其实HTTPS就是HTTP + TLS/SSL,在HTTP和TCP之间加了一层加密层,解决了HTTP三个核心问题:
- 明文传输:HTTP的数据都是明文的,网络上随便抓个包就能看到内容,HTTPS用对称加密传输数据,别人抓了包也看不到内容;
- 身份伪造:HTTPS用数字证书,确保你访问的是真的百度,而不是黑客伪造的钓鱼网站;
- 数据篡改:HTTPS有摘要校验机制,确保传输的数据不会被黑客中途修改。
这里面试必问:HTTPS用的是对称加密还是非对称加密?很多人上来就说非对称加密,又错了!正确答案是:两者结合用。非对称加密用来协商对称加密的密钥,对称加密用来传输实际数据。
为啥不全程用非对称加密?因为非对称加密加解密速度极慢,开销极大,不适合传输大量数据;而对称加密加解密速度快,适合大数据传输,但是密钥得安全的传给对方,这时候就用非对称加密来传密钥,完美解决了问题。
然后是HTTP2,HTTP2主要解决了HTTP1.1的队头阻塞问题,支持多路复用:同一个TCP连接里,可以同时传输多个HTTP请求和响应,不用排队,一个请求阻塞了,不影响其他请求。除此之外,还支持头部压缩、服务器推送,性能比HTTP1.1提升了一大截。
但是HTTP2有个致命的问题:它还是基于TCP的,TCP本身的队头阻塞问题解决不了。什么意思?TCP传输的时候,如果一个数据包丢了,整个TCP连接都得等着这个包重传,哪怕其他数据包已经到了,也得等着,HTTP2的多路复用,在TCP丢包的时候,性能反而会更差。
所以HTTP3就来了,2026年面试的重中之重。HTTP3底层直接抛弃了TCP,改用UDP,基于QUIC协议,彻底解决了队头阻塞的问题。QUIC协议在UDP的基础上,自己实现了可靠传输、拥塞控制、加密认证,每个HTTP请求都是独立的流,一个请求的数据包丢了,只需要重传这个请求的包,不影响其他请求,彻底解决了队头阻塞。
除此之外,HTTP3还有个巨大的优势:0-RTT握手,连接建立的速度极快,第一次连接只需要1-RTT,第二次连接就能实现0-RTT,直接发数据,延迟比HTTP2低了一大截。同时它还支持连接迁移,设备切换网络时,无需断开重连,完美适配移动IoT、车载终端等场景,2026年已经在工业监控、直播、金融交易等领域大规模落地。现在国内的阿里、腾讯、百度,基本都已经全量支持HTTP3了,面试的时候,你能把HTTP3的原理讲清楚,面试官立马就会对你刮目相看。
1.5 面试必背的HTTP状态码,别再只知道200、404、500了
最后,网络篇还有个必问的点,就是HTTP状态码,很多兄弟面试的时候,被问301和302的区别,401和403的区别,当场就懵了。这里给大家把面试高频的状态码,一次性分好类,记下来面试直接用。
HTTP状态码分为五大类,看第一位数字就知道是什么类型:
- 1xx:信息性状态码,服务器收到了请求,正在处理,比如100 Continue,101 Switching Protocols(websocket用的)。
- 2xx:成功状态码,请求处理成功。
- 200 OK:最常见的,请求成功,正常返回数据。
- 201 Created:资源创建成功,比如POST接口新增了一条数据。
- 204 No Content:请求成功,但是没有返回内容,比如DELETE接口删除数据成功。
- 3xx:重定向状态码,需要客户端进一步操作才能完成请求。
- 301 Moved Permanently:永久重定向,资源永久搬到新地址了,浏览器会缓存,下次直接访问新地址,SEO权重也会转移。
- 302 Found:临时重定向,资源临时搬到新地址了,浏览器不会缓存,下次还是访问原地址。
- 304 Not Modified:协商缓存,资源没修改,客户端直接用本地缓存,这个面试也经常问,和缓存相关。
- 4xx:客户端错误状态码,是客户端的问题,不是服务器的问题。
- 400 Bad Request:请求参数错误,服务器无法理解。
- 401 Unauthorized:未授权,需要登录认证之后才能访问。
- 403 Forbidden:服务器拒绝访问,你已经登录了,但是没有权限访问这个资源。
- 404 Not Found:资源不存在,请求的地址不对。
- 405 Method Not Allowed:请求方法不允许,比如接口是POST的,你用GET请求了。
- 5xx:服务器错误状态码,是服务器的问题,不是客户端的问题。
- 500 Internal Server Error:服务器内部错误,代码出bug了。
- 502 Bad Gateway:网关错误,服务器作为网关/代理,收到了上游服务器的无效响应。
- 503 Service Unavailable:服务不可用,服务器过载了,或者正在停机维护。
- 504 Gateway Timeout:网关超时,服务器作为网关/代理,等上游服务器的响应等超时了。
这些状态码,不用全背,把上面这些高频的记下来,面试的时候绝对够用了。
第二章 操作系统篇:懂了OS,才算真正懂了计算机
很多兄弟觉得,我就是个写业务代码的,又不是写操作系统的,学操作系统有啥用?我只能说,大错特错!你写的代码,最终都要跑在操作系统上,你的程序为啥内存泄漏了?为啥进程崩溃了?为啥CPU占用100%了?为啥多线程写代码总出bug?这些问题,不懂操作系统,你永远都定位不到根因。
面试官问你操作系统,不是想让你去写一个Linux内核,而是想看你懂不懂程序运行的底层逻辑,能不能写出更高效、更稳定的代码。这一章,我就把操作系统面试必问的核心点,全给大家讲透,还会加入2026年最新的面试考点,让你面试的时候脱颖而出。
2.1 进程和线程,别再背定义了,搞懂本质区别
面试操作系统,第一个必问的,就是进程和线程的区别,90%的程序员都能背下来"进程是资源分配的最小单位,线程是CPU调度的最小单位",但是面试官一追问,立马就懵。
还是先给大家打个通俗的比方,让大家一秒就懂。
操作系统就像一个工厂,工厂里有很多独立的车间,每个车间就是一个进程。车间里有自己的原材料、设备、场地,也就是进程拥有的独立内存资源,各个车间之间是互相隔离的,A车间的原材料,B车间不能随便用,除非走专门的通道。
每个车间里,有很多工人在干活,这些工人就是线程。同一个车间里的工人,共享车间里的原材料、设备、场地,也就是同一个进程里的线程,共享进程的内存资源。一个车间里,至少得有一个工人干活,也就是一个进程里,至少得有一个线程。
工厂要干活,得先开一个车间,分配好原材料和设备,然后让工人进去干活。操作系统要运行程序,得先创建一个进程,分配好独立的内存资源,然后创建线程,让CPU去调度线程执行代码。
现在,大家就能明白进程和线程的核心区别了,我给大家整理成大白话,不用背定义,面试直接说:
- 资源分配:进程有自己独立的内存地址空间,各个进程之间完全隔离,一个进程崩了,不会影响其他进程;线程没有独立的内存空间,共享所属进程的内存,一个线程崩了,整个进程就崩了。
- 调度开销:进程切换的时候,要切换整个内存地址空间,开销极大,耗时长;线程切换的时候,只需要切换栈和寄存器,开销极小,速度极快。
- 并发执行:一个进程里可以有多个线程,同时执行不同的任务,也就是并发。比如你打开一个浏览器,就是开了一个进程,浏览器里同时下载文件、播放视频、渲染页面,就是不同的线程在干活。
- 通信成本:进程之间通信,需要专门的IPC机制,成本很高;线程之间通信,直接读写进程的共享内存就行,成本极低,但是也带来了线程安全的问题。
2026年面试,还有个高频追问:单核CPU上,多线程有没有意义?很多人觉得,单核CPU同一时间只能跑一个线程,多线程没用,又错了!
举个例子,你写一个程序,既要从磁盘读文件,又要响应用户的操作,如果是单线程,读文件的时候,线程就被阻塞了,CPU闲着没事干,用户的操作也响应不了;如果是多线程,读文件的线程阻塞了,CPU立马就去调度响应用户操作的线程,CPU利用率直接拉满。所以,哪怕是单核CPU,多线程也是有巨大意义的,尤其是IO密集型的任务,多线程能极大提升程序性能。
2.2 进程间通信(IPC),6种方式一次性讲透
面试的时候,问完进程和线程的区别,面试官大概率会接着问:进程之间怎么通信?有哪些方式?分别用在什么场景?
很多兄弟这里就卡壳了,要么只知道管道和socket,要么说不全,今天我就把6种核心的IPC方式,用大白话给大家讲明白,面试的时候全说出来,直接碾压同批次候选人。
还是先回顾一下,进程之间是内存隔离的,A进程的内存,B进程根本访问不到,所以要通信,必须靠操作系统做中间层,这就是IPC机制的本质。
-
管道(Pipe)
管道是最基础的IPC方式,分为匿名管道和命名管道。
匿名管道,就像两个工人之间,拉了一根单向的水管,水只能从一头流到另一头,也就是半双工通信,只能单向传输数据。而且匿名管道只能用在有亲缘关系的进程之间,比如父进程和子进程,就像一个车间里的两个工人,才能用这根水管。
Linux里的shell命令里的 | 竖线,就是匿名管道,比如 ps -ef | grep java,就是把ps命令的输出,通过匿名管道传给grep命令作为输入。
命名管道,也叫FIFO,它解决了匿名管道只能亲缘进程通信的问题,就像在工厂里建了一个有名字的水管,任何车间的工人,只要知道这个水管的名字,就能用它来传输数据,支持无亲缘关系的进程通信。
管道的缺点就是,传输效率低,不适合大量数据传输。
-
消息队列(Message Queue)
消息队列,就像工厂里的快递柜,A进程把消息放到快递柜里,写上编号,B进程根据编号,去快递柜里把消息取出来。
和管道不一样,消息队列是面向消息的,不是字节流,不用关心数据的格式,而且消息可以随机读取,不用像管道那样必须先进先出。A进程放完消息就可以走了,不用等B进程来取,解耦性很强。
消息队列的缺点就是,消息有大小限制,不适合传输超大文件,而且数据拷贝两次,从用户态拷贝到内核态,再从内核态拷贝到用户态,有一定的开销。
-
共享内存(Shared Memory)
共享内存,是最快的IPC方式,没有之一。
前面的管道、消息队列,都需要经过内核拷贝数据,开销大。共享内存就不一样了,它直接让两个进程,共享同一块物理内存,A进程往这块内存里写数据,B进程立马就能看到,不需要经过内核拷贝,零拷贝,速度拉满。
就像两个车间,直接打通了一堵墙,共享同一个仓库,A车间把东西放到仓库里,B车间直接就能拿,不用经过工厂管理员中转。
共享内存的缺点就是,因为两个进程直接操作同一块内存,会出现线程安全问题,需要配合信号量或者互斥锁来做同步,不然很容易出现数据错乱。
-
信号量(Semaphore)
信号量,本质上是一个计数器,用来实现进程之间的同步和互斥,不是用来传输数据的。
就像工厂里的厕所,只有3个坑位,门口放了个计数器,有人进去,计数器减1,有人出来,计数器加1,计数器为0的时候,外面的人就得等着。这就是信号量的PV操作,P操作是减1,申请资源,V操作是加1,释放资源。
信号量分为二元信号量(0和1,也就是互斥锁)和计数信号量,不仅能实现进程之间的互斥,还能实现进程之间的同步。
-
信号(Signal)
信号,是Linux里唯一的异步通信机制,就像工厂里的紧急警报,一拉警报,所有车间都得停下手里的活,处理紧急事件。
比如我们在Linux里用 kill -9 进程号,就是给进程发了一个SIGKILL信号,让进程强制终止。进程收到信号之后,不管执行到哪了,都得停下来,去处理这个信号,要么执行默认操作,要么自己定义处理函数,要么忽略信号(SIGKILL和SIGSTOP不能忽略)。
信号只能用来通知进程发生了什么事件,不能用来传输大量数据,比如进程崩溃、子进程退出、用户按了Ctrl+C,都会发信号。
-
Socket(套接字)
Socket,也就是我们常说的网络套接字,它不仅能实现同一台机器上的进程通信,还能实现不同机器、跨网络的进程通信,是用的最多的IPC方式,没有之一。
就像两个工厂,不管在同一个城市,还是不同的城市,只要有网线,就能通过快递互相传输数据。我们平时写的HTTP接口、TCP服务,底层全是Socket。
本地进程通信的时候,用Unix域Socket,不需要经过网络协议栈,不用打包拆包、计算校验和,比网络Socket快很多。
这6种IPC方式,大家记下来,面试的时候分清楚各自的特点和场景,绝对够用了。
2.3 内存管理:虚拟内存、分页、分段,通俗讲明白
操作系统的内存管理,也是面试必问的重灾区,很多兄弟被问虚拟内存是什么,分页和分段的区别,当场就懵,觉得太抽象了,今天我就用大白话给大家讲明白。
首先,大家先想一个问题:我们的电脑内存只有8G、16G,但是能同时运行几十个进程,每个进程都觉得自己独占了整个内存,甚至一个进程的内存空间能超过物理内存的大小,这是怎么做到的?
答案就是:虚拟内存。
虚拟内存,是操作系统搞出来的一个"骗局",给每个进程都画了一个大饼,告诉进程:"整个内存地址空间都是你的,你随便用!",但实际上,进程用的不是真实的物理内存地址,而是虚拟地址,操作系统会通过页表,把虚拟地址翻译成真实的物理地址。
还是给大家打个通俗的比方:
物理内存就像酒店的房间,每个房间有真实的房间号,大小是固定的,比如16G内存,就是酒店有16384个房间,每个房间1M。
每个进程就像一个客人,酒店给每个客人都发了一张房卡,告诉客人:"我们酒店有无限个房间,编号从0到无穷大,你随便用!",这就是虚拟地址空间。
但是客人根本不知道真实的房间在哪,他要进房间的时候,酒店前台会根据他给的虚拟房间号,查一个对照表,找到对应的真实房间号,带他进去,这个对照表就是页表,这个翻译过程就是内存管理单元(MMU)做的。
这样做有什么好处?
第一,内存隔离。每个进程的虚拟地址空间是独立的,A进程的虚拟地址0x123456,和B进程的虚拟地址0x123456,翻译出来的物理地址完全不一样,进程之间不会互相干扰,一个进程崩了,不会影响其他进程。
第二,内存扩展。操作系统可以把硬盘上的一部分空间,当成虚拟内存来用,也就是我们常说的交换分区(Swap)。当物理内存不够用的时候,操作系统可以把物理内存里,很久没用的内存页,写到硬盘的Swap分区里,把物理内存腾出来给需要的进程用,等需要的时候,再从硬盘里读回来。这样就实现了用小的物理内存,运行大的进程,就像酒店房间不够了,把客人不常用的行李放到仓库里,把房间腾出来给其他客人用。
第三,内存共享。多个进程可以把虚拟地址,映射到同一块物理内存上,实现内存共享,比如我们前面说的共享内存IPC,还有动态链接库,多个进程共享同一个so库的代码,不用每个进程都加载一份,节省内存。
讲完了虚拟内存,就到了分页和分段,这两个是虚拟内存的管理方式,面试必问区别。
分页,就是把虚拟地址空间和物理内存,都分成固定大小的块,这个块就叫页,Linux里一般是4K大小。虚拟地址由页号和页内偏移组成,操作系统通过页表,把虚拟页号映射到物理页号,页内偏移是一样的,不用翻译。
分页的好处是,页的大小固定,管理起来很方便,不会产生外部碎片,只会有少量的内部碎片(最后一页用不完的空间)。缺点是,页是固定大小的,不符合程序的逻辑结构,一个函数可能被拆到多个页里,一个数组也可能被拆到多个页里。
分段,就是把虚拟地址空间,按照程序的逻辑结构分成段,比如代码段、数据段、栈段、堆段,每个段的大小是不固定的,根据程序的逻辑来定。虚拟地址由段号和段内偏移组成,段表记录了每个段的基地址和长度,通过段表就能找到对应的物理地址。
分段的好处是,符合程序的逻辑结构,段和段之间是独立的,便于代码的共享和保护,比如代码段是只读的,数据段是可读写的,不会越界。缺点是,段的大小不固定,内存分配和释放的时候,会产生大量的外部碎片,内存利用率低。
现在的操作系统,基本都是分页+分段结合的方式,也就是段页式管理,先把进程的地址空间分成段,再把每个段分成固定大小的页,既符合程序的逻辑结构,又解决了内存碎片的问题,完美结合了两者的优点。
2.4 死锁:怎么产生的?怎么预防?
死锁,也是面试必问的点,尤其是写多线程代码的兄弟,必须搞懂。很多人写多线程代码,跑着跑着程序就卡住了,CPU占用为0,啥也干不了,大概率就是死锁了。
先给大家用大白话讲明白,什么是死锁?
死锁,就是两个或多个进程/线程,互相拿着对方需要的资源,又都等着对方释放资源,谁也不先放手,就一直僵持在这,永远阻塞下去,程序就卡死了。
给大家举个最经典的例子:
张三和李四两个人吃饭,桌子上只有一双筷子,一人拿了一根。张三拿着左筷子,等着李四把右筷子放下来;李四拿着右筷子,等着张三把左筷子放下来。两个人都不先放自己手里的筷子,就一直等着,永远吃不上饭,这就是死锁。
死锁的产生,必须同时满足四个必要条件,四个条件缺一个,死锁都产生不了,这四个条件面试必背:
- 互斥条件:一个资源同一时间只能被一个进程/线程使用,别人要用,就得等着。比如一根筷子,同一时间只能一个人拿。
- 请求与保持条件:一个进程/线程,已经拿着一个资源了,又去请求新的资源,但是新的资源被别人占了,自己手里的资源也不释放。比如张三拿着左筷子,又去要右筷子,右筷子拿不到,左筷子也不放手。
- 不剥夺条件:进程/线程手里的资源,自己不释放,别人不能强行抢走,只能等它自己释放。比如张三拿着左筷子,李四不能从他手里抢过来。
- 循环等待条件:多个进程/线程,形成了一个循环等待链,A等B的资源,B等C的资源,C又等A的资源。比如张三等李四的右筷子,李四等张三的左筷子,形成了循环。
知道了这四个必要条件,死锁的预防就很简单了,只要打破其中任何一个条件,死锁就产生不了。面试的时候,问你怎么预防死锁,就从这四个条件入手,挨个说怎么打破,绝对满分。
- 打破互斥条件:让资源能同时被多个进程/线程使用。但是这个很难,因为很多资源本身就是互斥的,比如锁、打印机,同一时间只能一个人用,所以这个方法一般不用。
- 打破请求与保持条件:进程/线程在运行之前,一次性申请所有需要的资源,要么全拿到,要么一个都不拿。比如张三和李四,吃饭之前必须一次性拿到两根筷子,拿不到就不拿,这样就不会出现拿一根等另一根的情况了。缺点就是,很多资源进程只用一会,提前占着,会造成资源浪费,资源利用率极低。
- 打破不剥夺条件:进程/线程拿着一个资源,去请求新的资源拿不到的时候,必须释放自己手里已经拿着的资源,等以后需要的时候,再重新一起申请。比如张三拿着左筷子,要右筷子要不到,就必须把左筷子放了,等以后两根筷子都能拿到的时候,再一起拿。这个方法实现起来比较复杂,而且反复申请释放资源,会增加系统开销。
- 打破循环等待条件:给所有的资源都编上号,进程/线程申请资源的时候,必须按照编号从小到大的顺序申请,不能倒着来。比如给左筷子编号1,右筷子编号2,张三和李四必须先申请编号1的左筷子,再申请编号2的右筷子。这样的话,张三拿到了左筷子,李四申请左筷子就会被阻塞,不会出现张三拿左、李四拿右的情况,循环等待就打破了。这个方法是最常用的,实现起来简单,资源利用率也高。
除了预防死锁,还有死锁的避免(银行家算法)、死锁的检测与解除,面试的时候问的不多,大家知道有这么回事就行,重点把四个必要条件和四个打破方式记牢,面试绝对够用。
2.5 协程:2026年高频考点,和线程到底啥区别?
2026年了,协程已经成了面试的高频考点,Go、Python、Java、JavaScript,现在主流的语言都支持协程,面试官必问:协程是什么?和线程有啥区别?为啥协程比线程轻量?
很多兄弟这里就懵了,今天我就用大白话给大家讲明白。
先回顾一下,线程是CPU调度的最小单位,线程的调度是由操作系统内核来做的,线程切换的时候,需要从用户态切换到内核态,开销很大。而且一个线程,默认的栈大小一般是1M,开上万个线程,内存就直接爆了。
而协程,是用户态的轻量级线程,它的调度完全由用户自己的程序来控制,不需要操作系统内核参与,切换的时候,只需要在用户态切换,不用进入内核态,开销极小,小到可以忽略不计。
还是给大家打个通俗的比方:
线程就像工厂里的正式工人,每个工人都有自己的工位和工具,工厂给发工资,管调度,工人切换工位,需要工厂管理员审批,流程多,开销大。工厂里养不了太多正式工人,养多了成本太高。
协程就像正式工人自己招的临时工,临时工的调度、干活,全由正式工人自己安排,不用经过工厂管理员,临时工切换干活的内容,正式工人自己说了算,速度极快,开销极小。而且临时工不用占工厂的正式编制,不用工厂发工资,一个正式工人,能带上百个甚至上千个临时工,成本极低。
一个线程里,可以跑多个协程,同一时间,一个线程里只有一个协程在执行,协程的切换,是在同一个线程里,由用户自己控制的,不是操作系统抢占式调度的。
现在大家就能明白,协程和线程的核心区别了:
- 调度层面:线程是内核态调度的,由操作系统内核管理;协程是用户态调度的,由用户程序自己管理,内核根本感知不到协程的存在。
- 切换开销:线程切换需要从用户态切到内核态,要保存寄存器、栈等一大堆上下文,开销极大;协程切换只在用户态,只需要保存少量的上下文,开销极小,比线程切换快上百倍。
- 内存占用:线程默认栈大小一般是1M,开1万个线程,就要占用10G内存;协程的栈大小是动态的,初始只有几KB,开10万个协程,也只占用几百M内存,内存占用差了上千倍。
- 并发模型:线程是抢占式调度的,操作系统会随时切换线程,很容易出现线程安全问题;协程是协作式调度的,协程自己主动让出CPU,才会切换到其他协程,同一时间一个线程里只有一个协程在跑,不会出现同时写共享内存的情况,线程安全问题少很多。
- 多核利用:线程可以利用多核CPU,多个线程可以同时跑在多个CPU核心上;协程本身是跑在单个线程里的,要利用多核,需要配合多线程+多协程的模型,比如Go语言的GMP模型。
这就是为啥现在协程这么火,尤其是高并发IO密集型的场景,比如网关、微服务、爬虫、实时通信,用协程,能以极低的开销,实现超高的并发,性能比多线程模型强太多了。2026年了,不管你用什么语言,都必须搞懂协程,面试的时候,能把协程的原理讲清楚,绝对是加分项。
第三章 数据库篇:CRUD程序员的面试生死线
兄弟们,做后端开发的,天天都在和数据库打交道,写的最多的就是CRUD,但是面试的时候,数据库相关的问题,也是翻车最多的地方。很多人写了好几年SQL,结果被问索引为什么失效,事务隔离级别,当场就哑火。
面试官问你数据库,不是想让你背SQL语法,而是想看你能不能写出高效的SQL,能不能设计出合理的表结构,能不能解决线上的慢SQL、死锁、数据不一致的问题。这一章,我就把数据库面试必问的核心点,全给大家讲透,结合2026年MySQL最新版本特性,让你面试的时候不再翻车。
3.1 索引:为啥加了索引就快?B+树到底牛在哪?
数据库索引,绝对是面试必问的TOP1,没有之一。10场数据库面试,有9场第一个问题就是索引,剩下1场,第一个问题是事务。
很多兄弟都知道,查询慢了加索引,加了索引查询就变快了,但是你问他为啥快?索引的底层结构是什么?为啥用B+树,不用二叉树、红黑树、B树?80%的人都答不上来。
先给大家打个最通俗的比方,索引就像书的目录。你想在一本1000页的书里,找一个知识点,如果没有目录,你就得从第一页翻到最后一页,一页一页找,这就是全表扫描,慢的要死。如果有目录,你先看目录,找到这个知识点对应的页码,直接翻到那一页,几秒钟就找到了,这就是索引的作用。
索引的本质,就是通过合适的数据结构,减少查询时需要扫描的数据量,把O(n)的全表扫描,变成O(logn)的快速查找,极大提升查询效率。
现在主流的MySQL数据库,用的InnoDB存储引擎,默认的索引结构就是B+树,这也是面试必问的核心。很多人搞不懂,为啥这么多数据结构,偏偏选B+树?
咱们挨个对比一下,大家就明白了:
- 二叉查找树:二叉查找树的特点是,左子树的所有值都小于根节点,右子树的所有值都大于根节点,查询效率是O(logn)。但是它有个致命的问题,极端情况下,会变成一条链表,比如你按1、2、3、4、5的顺序插入,树就变成了一条直线,查询效率直接退化成O(n),和全表扫描没区别,所以肯定不能用。
- 红黑树:红黑树是平衡二叉树,解决了二叉查找树退化成链表的问题,左右子树的高度差不会超过1,查询效率稳定在O(logn)。但是它有个问题,树的高度太高了。比如你表里有1000万条数据,红黑树的高度大概是24层,也就是说,查一条数据,最多要找24个节点。而数据库的数据是存在磁盘上的,每个节点对应磁盘上的一个数据页,每次找节点,都要做一次磁盘IO,磁盘IO是极慢的,一次磁盘IO大概要10ms,24次磁盘IO,就要240ms,查一条数据要几百毫秒,这谁受得了?所以红黑树也不适合做数据库索引。
- B树(平衡多路查找树):B树解决了红黑树高度太高的问题,它不是二叉树,而是多路树,每个节点可以存多个key和多个子节点,路数越多,树的高度越低。比如每个节点存1000个key,那1000万条数据,B树的高度只需要3层,也就是说,查一条数据,最多只需要3次磁盘IO,比红黑树快太多了。
但是B树还是有缺点,不适合做数据库索引:
第一,B树的每个节点,既存key,又存data,也就是表里的行数据。每个数据页的大小是固定的,InnoDB里默认是16K,如果data很大,每个节点能存的key数量就会变少,树的高度就会变高,查询效率就会下降。
第二,B树不适合做范围查询,比如你要查id在1到100之间的数据,B树需要做中序遍历,多次磁盘IO,效率很低。
第三,B树的查询不稳定,最好的情况查根节点就找到了,最坏的情况要查到叶子节点,磁盘IO次数不一样。
- B+树:B+树是B树的升级版,完美解决了B树的所有缺点,也是InnoDB最终选择它的原因。
B+树和B树的核心区别,也是它的优点,我给大家用大白话讲清楚:
第一,B+树的非叶子节点,只存key,不存data,所有的data都存在叶子节点里。这样的话,非叶子节点每个能存的key数量就会变得极多,树的高度会变得极低。比如InnoDB里,一个数据页16K,key是bigint类型,8个字节,加上指针4个字节,一个节点能存上千个key,1000万条数据,B+树的高度只需要2-3层,查一条数据,最多2-3次磁盘IO,速度拉满。
第二,B+树的叶子节点,是用双向链表串联起来的,所有的叶子节点,key都是有序的。这就完美解决了范围查询的问题,比如你要查id在1到100之间的数据,只需要找到id=1的叶子节点,然后顺着链表往后遍历,直到id=100就行,只需要一次磁盘IO找到起点,后面都是顺序读取,效率极高。
第三,B+树的查询更稳定,因为所有的data都在叶子节点里,不管查什么数据,都必须从根节点查到叶子节点,磁盘IO次数都是一样的,查询性能非常稳定。
第四,B+树的全表扫描更方便,只需要遍历叶子节点的双向链表就行,不用遍历整棵树,效率比B树高太多。
这就是为啥InnoDB要用B+树做索引,面试的时候,你能把这些点讲清楚,面试官立马就知道你是真懂,不是只会背八股。
这里还有个面试必问的点:聚簇索引和非聚簇索引的区别?
聚簇索引,就是主键索引,它的叶子节点存的是整行数据,一张表只能有一个聚簇索引。就像书的目录,目录里的页码,直接对应书里的内容页,一页里就是完整的内容。
非聚簇索引,也叫二级索引、辅助索引,它的叶子节点存的不是整行数据,而是主键的值。一张表可以有多个非聚簇索引。
那用非聚簇索引查询的时候,是怎么查的?比如你给name字段建了索引,查select * from user where name='张三',首先会通过name的二级索引,找到对应的主键值,然后再通过主键的聚簇索引,找到整行数据,这个过程就叫回表。
面试的时候,还会问:什么是覆盖索引?覆盖索引就是,你查询的字段,刚好都在二级索引里,不需要回表,直接就能拿到结果,比如你建了name和age的联合索引,查select name,age from user where name='张三',不需要回表,直接就能拿到name和age的值,效率极高。这也是SQL优化里,最常用的优化手段之一。
2026年最新面试考点,MySQL 8.4 LTS版本对索引做了多项优化,新增了部分索引功能,支持只给字段的前半部分建索引,进一步降低索引占用空间,提升查询效率;同时支持自动索引碎片清理,不用再手动执行optimize table,联合索引的最左匹配逻辑也做了优化,查询效率提升了20%以上。这些点面试的时候说出来,绝对能让面试官眼前一亮。
3.2 事务ACID,别再只会念缩写了
数据库事务,也是面试必问的核心,很多兄弟只会背ACID四个字母,但是每个字母代表什么,背后的本质是什么,根本讲不清楚,面试的时候一追问就翻车。
先给大家讲明白,什么是事务?事务就是一组SQL操作,要么全部执行成功,要么全部执行失败,是一个不可分割的最小单位。最经典的例子就是转账:张三给李四转100块钱,要做两个操作,张三的账户减100,李四的账户加100,这两个操作必须都成功,或者都失败,不能张三减了钱,李四没加上,也不能李四加了钱,张三没减,这就是事务的作用。
ACID,是事务的四个核心特性,也是事务能保证数据一致性的根本,我给大家用大白话挨个讲透:
-
A(Atomicity):原子性
原子性,就是事务是最小的不可分割的单位,事务里的操作,要么全部执行成功,要么全部执行失败,不能只执行一半。
就像转账的例子,要么张三减钱、李四加钱都成功,事务提交;要么其中一个操作失败,所有操作都回滚,回到转账之前的状态,不会出现中间状态。
InnoDB里,原子性是通过undo log(回滚日志)来实现的。undo log记录了事务里SQL操作的反向操作,比如你执行了update语句,undo log就会记录对应的update回滚语句,事务执行失败的时候,就通过undo log把数据回滚到之前的状态,保证原子性。
-
C(Consistency):一致性
一致性,就是事务执行前后,数据的完整性约束没有被破坏,数据始终是一致的。
还是转账的例子,转账之前,张三和李四的账户总余额是2000块,转账之后,总余额还是2000块,不会凭空多钱,也不会凭空少钱,这就是一致性。
一致性是事务的最终目的,原子性、隔离性、持久性,都是为了保证一致性。除了数据库层面的约束,一致性还需要业务层面的保证,比如转账的时候,不能转负数,不能超过账户余额,这些业务逻辑不对,也会破坏一致性。
-
I(Isolation):隔离性
隔离性,就是多个事务同时执行的时候,互相之间是隔离的,一个事务的执行,不能被其他事务干扰。
比如两个事务同时操作同一个账户,A事务给账户加100,B事务给账户减100,两个事务同时执行,不能出现最终结果不对的情况,这就是隔离性要保证的。
InnoDB里,隔离性是通过锁机制和MVCC(多版本并发控制)来实现的,后面会给大家详细讲。隔离性有四个隔离级别,不同的隔离级别,隔离效果不一样,也是面试必问的点。
-
D(Durability):持久性
持久性,就是一个事务一旦提交成功,它对数据库里数据的修改,就是永久的,哪怕数据库宕机、断电了,重启之后,数据也不会丢。
就像你转账成功了,哪怕银行的服务器突然断电了,重启之后,你的转账记录也还在,钱也已经到对方账户了,不会消失。
InnoDB里,持久性是通过redo log(重做日志)来实现的。InnoDB修改数据的时候,不是直接修改磁盘上的数据文件,而是先写redo log,等数据库空闲的时候,再把redo log里的修改,刷到磁盘上的数据文件里。如果数据库宕机了,重启之后,会读取redo log,把没刷到磁盘的修改重新执行一遍,保证数据不会丢,这就是WAL(预写日志)机制。
很多兄弟搞不懂undo log和redo log的区别,这里给大家一句话记死:undo log保证事务的原子性,用来回滚数据;redo log保证事务的持久性,用来恢复数据。面试的时候被问,直接说这句话,绝对没错。
3.3 隔离级别:脏读、幻读、不可重复读,一次分清
讲完了ACID,就到了事务的隔离级别,这是面试必问的重灾区,很多人到现在都分不清脏读、幻读、不可重复读,更别说四个隔离级别了,今天我就一次性给大家讲明白,再也不会搞混。
首先,多个事务同时执行的时候,如果没有隔离性,会出现三个问题,也就是脏读、不可重复读、幻读,先把这三个问题搞懂,隔离级别就好理解了。
- 脏读:一个事务,读到了另一个事务未提交的数据。
举个例子:
- 事务A:把张三的账户余额从1000改成了2000,还没提交事务。
- 事务B:查询张三的账户余额,读到了2000,拿着这个数据去做业务处理了。
- 事务A:回滚了事务,张三的余额又变回了1000。
这时候,事务B读到的2000,就是脏数据,根本不存在,这就是脏读,会导致业务逻辑完全出错。
- 不可重复读:一个事务里,多次查询同一个数据,得到的结果不一样。
举个例子:
- 事务A:第一次查询张三的账户余额,是1000,事务还没提交。
- 事务B:把张三的账户余额改成了2000,并且提交了事务。
- 事务A:第二次查询张三的账户余额,变成了2000,和第一次查询的结果不一样。
这就是不可重复读,在同一个事务里,两次读同一个数据,结果不一样,没法重复读。
- 幻读:一个事务里,多次查询符合条件的数据集,得到的行数不一样,就像出现了幻觉一样。
举个例子:
- 事务A:查询账户余额大于1000的用户,第一次查到了10条数据,事务还没提交。
- 事务B:新增了一个账户余额2000的用户,并且提交了事务。
- 事务A:第二次查询账户余额大于1000的用户,查到了11条数据,平白多了一条,就像出现了幻觉。
这就是幻读,注意,幻读和不可重复读的区别:不可重复读是针对同一条数据的修改,幻读是针对数据集的新增或删除。
搞懂了这三个问题,四个隔离级别就好理解了,SQL标准定义了四个事务隔离级别,从低到高分别是:
- 读未提交(Read Uncommitted):最低的隔离级别,一个事务能读到其他事务未提交的数据,会出现脏读、不可重复读、幻读,基本没人用。
- 读已提交(Read Committed,RC):一个事务只能读到其他事务已经提交的数据,解决了脏读的问题,但是会出现不可重复读和幻读。这是Oracle、SQL Server等大多数数据库的默认隔离级别。
- 可重复读(Repeatable Read,RR):保证在同一个事务里,多次读取同一个数据,结果都是一样的,解决了脏读和不可重复读的问题,但是还是会出现幻读。这是MySQL InnoDB的默认隔离级别。
这里重点说一下,很多人说InnoDB的RR隔离级别解决了幻读的问题,对不对?其实是对的,InnoDB通过Next-Key Lock(临键锁),在RR隔离级别下,完美解决了幻读的问题,这也是MySQL和其他数据库不一样的地方,面试的时候说出来,绝对是加分项。 - 串行化(Serializable):最高的隔离级别,所有的事务都串行执行,一个事务执行完,另一个才能执行,完全解决了脏读、不可重复读、幻读的问题。但是缺点也很明显,并发性能极差,会出现大量的锁等待,基本没人用。
给大家整理成一个表格,一眼就能看清:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 会 | 会 | 会 |
| 读已提交 | 不会 | 会 | 会 |
| 可重复读 | 不会 | 不会 | 会(InnoDB解决) |
| 串行化 | 不会 | 不会 | 不会 |
这个表格,大家一定要记牢,面试的时候,不管面试官怎么问,都不会翻车。
3.4 锁机制:行锁、表锁、乐观锁、悲观锁,面试必问
数据库的锁机制,也是面试必问的核心,很多人写SQL的时候,出现死锁、并发更新数据错乱,都是因为没搞懂锁机制。今天我就用大白话,把MySQL里的锁,一次性给大家讲明白。
首先,MySQL里的锁,从粒度上划分,分为表锁和行锁,这是最基础的。
-
表锁:锁住整张表,一个事务锁住表之后,其他事务对这张表的所有读写操作,都得等着。
表锁的优点是:开销小,加锁快,不会出现死锁。
缺点是:锁的粒度太大,并发性能极差,只要有一个事务锁表,其他所有事务都得排队,只适合全表数据更新的场景,基本不用。
MyISAM存储引擎默认用的就是表锁,这也是为啥MyISAM被淘汰了,并发性能太差了。
-
行锁:锁住表里的一行或多行数据,一个事务锁住某几行之后,其他事务对这几行的操作要等着,但是对其他行的操作,完全不受影响。
行锁的优点是:锁的粒度小,并发性能极高,多个事务可以同时操作表里的不同行,互不干扰。
缺点是:开销大,加锁慢,会出现死锁。
InnoDB存储引擎支持行锁,这也是InnoDB比MyISAM强的核心原因之一,现在的业务系统,基本都用InnoDB,就是因为它的行锁,能支持高并发。
这里重点说一下,InnoDB的行锁,是加在索引上的,不是加在数据行上的。如果你的SQL语句没用到索引,InnoDB就会锁住整张表,行锁就变成表锁了,并发性能直接归零。比如你写update user set age=20 where name='张三',name字段没建索引,那InnoDB就会把整张表都锁住,其他所有更新操作都得等着,这是很多新手容易踩的坑,面试的时候也经常问。
然后,从锁的类型上划分,InnoDB的行锁分为共享锁(S锁)和排他锁(X锁)。
-
共享锁(S锁):也叫读锁,多个事务可以同时加S锁,互不干扰,但是加了S锁之后,其他事务不能加X锁,只能读,不能写。
加S锁的方式:select * from user where id=1 lock in share mode;
-
排他锁(X锁):也叫写锁,一个事务加了X锁之后,其他事务不能加任何锁,既不能读,也不能写,只能等着。
InnoDB里,update、delete、insert语句,默认都会加X锁,select语句默认不加锁,要加X锁的话:select * from user where id=1 for update;
讲完了表锁和行锁,就到了面试必问的乐观锁和悲观锁,这两个是并发控制的两种思想,不是数据库里具体的锁。
-
悲观锁:顾名思义,就是很悲观,总觉得并发操作一定会出问题,所以每次操作数据之前,都先加锁,不让别人操作,自己操作完了,再释放锁。
就像你去厕所,一进去就把门锁上,其他人都不能进,等你上完了,开门出来,其他人才能进。
悲观锁就是靠数据库的锁机制实现的,比如select ... for update,就是典型的悲观锁。
悲观锁的优点是:完全保证数据安全,不会出现并发更新问题。
缺点是:加锁会导致并发性能下降,容易出现死锁,适合写多读少的场景。
-
乐观锁:顾名思义,就是很乐观,总觉得并发操作不会出问题,所以操作数据的时候,不加锁,只有在提交更新的时候,检查一下有没有人修改过这个数据,如果没人修改,就提交成功;如果有人修改了,就回滚重试。
就像你写文档,总觉得没人会和你同时改,等你改完了要保存的时候,才检查一下文档有没有被别人改过,没改过就保存,改过了就处理冲突。
乐观锁的实现方式,最常用的就是版本号机制:给表加一个version字段,每次更新数据的时候,都把version加1,更新的时候,把查询到的version带过去,只有where条件里的version和数据库里的version一致,才更新成功。
比如:
- 第一步:select id,name,age,version from user where id=1; 查到version=1。
- 第二步:update user set age=20,version=version+1 where id=1 and version=1;
如果更新的时候,有人已经修改了这条数据,version就变成2了,where条件不满足,更新就失败了,不会出现并发更新错乱的问题。
乐观锁的优点是:不加锁,并发性能极高,不会出现死锁。
缺点是:在写多读少的高并发场景下,会出现大量的更新失败,重试成本很高,适合读多写少的场景。
面试的时候,问你乐观锁和悲观锁的区别,以及适用场景,把这些点讲清楚,绝对满分。
3.5 SQL优化,2026年面试必踩的坑,别再写慢SQL了
最后,就是SQL优化,这是面试必问的,也是线上最容易出问题的地方。很多兄弟写的SQL,测试环境跑的好好的,一到线上,数据量一大,就变成慢SQL,把数据库拖垮,就是因为没掌握SQL优化的核心技巧。今天我就给大家分享几个面试必问、线上必用的SQL优化技巧,全是我22年实战踩坑总结出来的,记下来绝对有用。
- 最核心的原则:尽量让SQL用到索引,避免全表扫描。
这是SQL优化的根本,所有的优化,都是围绕着让SQL正确用到索引来做的。很多人加了索引,但是SQL写的不对,索引失效了,还是全表扫描,慢的要死。
这里给大家列一下,最常见的索引失效的场景,面试必问,线上必避坑:
- 索引字段上做了函数运算、表达式计算、类型转换,索引会失效。比如where year(create_time)=2026,where id+1=10,where phone='123456'(phone是int类型),这些都会导致索引失效。
- 使用like模糊查询,以%开头,索引会失效。比如where name like '%张三',索引失效;但是where name like '张三%',是可以用到索引的。
- 使用or连接条件,只要有一个条件没建索引,整个索引都会失效。比如where id=1 or name='张三',id有索引,name没索引,那这个SQL就会全表扫描。
- 使用not in、!=、<>,索引会失效。比如where id not in (1,2,3),where age != 20,这些都会导致索引失效。
- 联合索引,不满足最左前缀原则,索引会失效。这个是面试必问的重点,后面单独讲。
- 联合索引,必须遵守最左前缀原则。
联合索引,就是给多个字段建一个索引,比如给name、age、gender建了联合索引,索引的顺序是name→age→gender。
最左前缀原则,就是查询的时候,必须从索引的最左边的字段开始查,不能跳过中间的字段,否则索引就会失效,或者只能用到一部分。
举个例子:
- where name='张三' and age=20 and gender='男':全字段匹配,索引完全生效。
- where name='张三' and age=20:用到了name和age的索引,生效。
- where name='张三':用到了name的索引,生效。
- where name='张三' and gender='男':只用到了name的索引,gender的索引用不到,因为跳过了age。
- where age=20 and gender='男':索引完全失效,因为跳过了最左边的name字段。
- where gender='男' and name='张三' and age=20:索引完全生效,MySQL的查询优化器会自动调整字段顺序,匹配联合索引。
最左前缀原则,面试100%会问,大家一定要记牢。
-
尽量使用覆盖索引,避免回表。
前面讲过,覆盖索引就是,你查询的字段,刚好都在索引里,不需要回表查询整行数据,效率极高。
比如你建了name和age的联合索引,查询select name,age from user where name='张三',就是覆盖索引,不需要回表;但是查询select * from user where name='张三',就需要回表,因为*里的其他字段,不在二级索引里。
所以,写SQL的时候,尽量不要用select *,需要什么字段,就查什么字段,既可以减少数据传输,又有更大的概率用到覆盖索引,避免回表。
-
分页查询优化,数据量越大越有用。
很多人写分页查询,都是limit offset, size,比如select * from user limit 1000000, 10,查询第100万条之后的10条数据,这个SQL在数据量小的时候没问题,但是数据量一大,就会变得极慢。
因为limit 1000000, 10,需要先扫描前100万条数据,然后扔掉,只取后面的10条,前面的100万条扫描,全是无用功,能不慢吗?
优化方式很简单,两种常用的:
- 主键分页优化:select * from user where id > 1000000 limit 10; 先找到上一页的最大id,然后通过id过滤,直接定位到位置,不用扫描前面的100万条数据,速度提升上百倍。
- 覆盖索引+回表优化:select * from user u inner join (select id from user limit 1000000, 10) t on u.id = t.id; 先通过覆盖索引,查到需要的10条数据的id,然后再通过id回表查询整行数据,不用扫描全表,效率极高。
- 其他高频优化技巧
- 尽量用小的数据类型,比如能用tinyint就不用int,能用int就不用bigint,数据类型越小,索引占用的空间越小,查询效率越高。
- 索引不是建的越多越好,一张表的索引,尽量不要超过5个。索引越多,查询越快,但是insert、update、delete的时候,需要更新所有的索引,写入性能会变得极差。
- 尽量避免在where条件里用null值判断,会导致索引失效,建表的时候,尽量给字段设置not null默认值。
- 多表关联查询的时候,尽量用inner join,少用left join和right join,关联的字段必须建索引,而且关联字段的类型、字符集必须完全一致,否则索引会失效。
- 大表尽量做分库分表,单表数据量超过1000万条,SQL性能会急剧下降,提前做好分库分表规划。
- 开启慢查询日志,用explain分析SQL的执行计划,查看是否命中索引,有没有全表扫描、回表、临时表、文件排序等问题,这是定位慢SQL最核心的手段。
这些SQL优化技巧,都是面试必问,线上必用的,大家记下来,不管是面试还是工作,都绝对有用。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。