如何在面试中筛选/不做一个「背题家」

2020-07-23

众所周知,国内互联网大厂的面试流程一般都比较公开透明,网上也会有不少所谓的面试经验分享,其形式内容基本大同小异,会谈及诸如有几面,每一面都问了哪些问题,做了什么笔试题云云。刨去一些跟每个面试者个人相关的项目问题,剩下的大多都是都是一些通用知识考察,比如操作系统,计算机网络,数据库等。面试者与被面试者往往都是科班出身,要说这些问题的难度也不过尔尔,只要有过系统性的学习,也基本都能从容应对。不过计算机领域之广袤,知识的广度和深度都非常可观,想要面面俱到,也并非易事,但作为企业,面试官总归是想要招到能力更强更好的全才,所以面试前的准备功课不可或缺。但常常出于功利的目的以及追求效率的考虑,很多面试者会选择这样一条道路——背题。上到算法题,下到基础性的概念,无所不背,如此急功近利的做法,往往还会颇有成效。想必在你的漫长面试生涯中,一定听过面试官问过这几个问题(甚至可以做到一字不差):

  1. 进程和线程有什么区别?
  2. 什么是 TCP 的三次握手和四次挥手?
  3. HTTP 中的 GET 和 POST 方法有什么区别?
  4. B 树和 B+ 树有什么区别?
  5. 什么是数据库的 ACID?
  6. ......

如果你身经百战,这些问题的答案应该信手拈来,很多时候甚至这些知识在你脑中已经不再是一种储备,而是近乎肌肉记忆的条件反射,一听到关键词,答案在嘴边如同施法一般就脱口而出......毕竟面试官问的次数实在是太多了!然而,大多数时候面试官的问题便在此戛然而止,少数面试官会继续问一句「为什么」,但也不过是浅尝辄止,看你说个差不多就「满意」地点了点头,进入下一个问题。几乎没有面试官会继续追问问题更为本质的一面,即「为什么背后的为什么」,也是在此,理解者和背题家的差距便会被暴露。上面这几个问题刚好涵盖了操作系统,计算机网络和数据库,我们挑选其中一些来一一分析讲解,来看看这些问题到底简单在哪里,又难在哪里,以至于大多数人都知其然却不知所以然,通过不停的追问即可到达我们今天的目的:筛选背题家。

并发 vs 并行

比起进程和线程的区别,我想先来讲讲「并发」和「并行」这两个词。中文语境下这两个词的区别似乎并不大,意思也难有较大区分,所以我们先来看看它们的英文:Concurrency 和 Parallelism。这两者是有区别的:「并发 Concurrency」意指在同一时间我们同时处理多个事情,而「并行 Parallelism」意指在同一时间我们同时多个事情。让我们来用一个比喻解释这两者行为的区别:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这是串行。
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这是并发。
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这是并行。

可以看到,完成「吃饭」和「接电话」两件事情所需要的时间,串行 == 并发 > 并行。计算机中,并发和并行无论是在底层设计还是上层编程中都是我们用于提高计算机处理效率以及性能的主要思想。看到这你可能会有这样一个疑惑:这并发也没让我们处理事情的效率变高啊,不还是和串行要用一样的时间吗?我们知道,计算机进行计算是需要资源的,这些资源包括但不限于 CPU 算力,内存以及网络 I/O 等,在有限的资源上完成更多的事情显然是我们的一个目标,而并发和并行分别从两个角度来达成这一目的:

  • 并发通过提高执行任务时的资源利用率来提高效率
  • 并行通过减少执行任务的耗时来提高效率。

可以看到,前者省下的是资源,而后者省下的是时间(要做的这一点也许还会消耗更多资源)。回到刚才打电话吃饭的例子,串行和并发的两个场景,虽然用时相同,但有一个显著的区别,即在串行的场景下,你的电话在你吃饭的过程中一直在响(暂时忽略你太久不接它会自动或主动挂断这个设定),在整个过程中,你的电话一直处于这一个电话的「等待响应」状态中,此时如果有另一个人也想给你打电话,他显然只能听到忙音而无法联系上你,这时候我们就可以说「电话」这个资源在整个过程中因持续占用却不被处理而浪费了。反观并发的场景,你立马停止了吃饭接听电话,尽管并没有节约时间,但是你的电话资源很快便被处理并释放了出来,此时如果有第二个电话到来,比起第一个场景,你在相同的时间里更多地利用了「电话」,假设第二个电话直接帮助你谈成了一笔 1 个亿的大合同,比起串行场景下接不上可能导致的错亿,可以不可以说并发帮助我们提高了任务处理的效率呢?答案是显然的。

在了解了以上概念后,「进程和线程有什么区别」这个问题又可以引申出多个问题:

  • 在多核 CPU 场景下,线程的执行是并行的吗?
  • 在单核 CPU 场景下,使用多线程可以帮助我们提高程序的运行效率吗?
  • GIL 的存在是否意味着 Python 无法做到并发?它会影响所有的多线程场景吗?
  • 不同语言实现并发主要通过哪几种方法?

在此我不提供解答,希望大家能够自己发掘这些问题的答案,帮助自己更好地理解进程,线程,并发以及并行这些玩意儿。

GET vs POST

想成为一个 CRUD Boy,HTTP 协议想必你一定了解,面对面试官「HTTP 中的 GET 和 POST 方法有什么区别?」的提问,你心里也许已经想好了一大票答案来回答:

  • GET 作为书签可收藏,POST 作为书签不可收藏。
  • GET 使用 URL 传递参数,POST 使用表单传递参数。
  • GET 对传输数据长度有限制,POST 没有限制。
  • GET 用于获取数据,POST 用于发送数据。
  • ......

很遗憾,以上所有都是片面的表象,甚至有些错误,而且也都不是 GET 和 POST 方法的本质区别。事实上,GET 和 POST 从理论上的技术使用来说,没有任何实质区别。GET 能做的事情 POST 也能做到,反之亦然。你完全可以只用 GET 方法来完成你 Web 应用里的所有请求,从读到写一应俱全,POST 也可以在 URL 里带参数,GET 也可以用 Body 来发送数据,实际产生的报文也仅有一些格式上的区别。那么既然「没区别」,那为什么还要区分不同的 HTTP 请求方法呢?要回答这个问题,我们需要从一个词入手:语义。

想必你一定会发现,实际场景中 GET 往往用于获取数据,而 POST 往往作为某些需要发起写数据的请求方式来使用。这几点其实并不是大家逐渐形成的使用习惯,而是早就在 HTTP 的 RFC 中有过明确的定义:

The GET method requests transfer of a current selected representation for the target resource. GET is the primary mechanism of information retrieval and the focus of almost all performance optimizations. Hence, when people speak of retrieving some identifiable information via HTTP, they are generally referring to making a GET request. A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request.

The POST method requests that the target resource process the representation enclosed in the request according to the resource’s own specific semantics.

现在你可以发现,GET 和 POST 其实仅有语义上的区别。

  • GET 意味向请求选择获取一系列资源,也就对应着「读资源」的场景,其需要满足安全,幂等和可缓存,即请求无害,多次请求结果一致,不会改变服务端的状态,并且读结果可以被缓存。对 GET 请求的携带消息并没有任何定义,如前文所述可以通过 URL 也可以通过 Body,但考虑到不同应用(诸如浏览器)的实现不同,选用后者也许会有一些问题。
  • POST 根据请求负荷对制定资源作出处理,也就对应着「写资源」的场景,其不一定安全,不保证幂等,大部分实现不可缓存。

看到这里,再次面对「HTTP 中的 GET 和 POST 方法有什么区别?」的提问,你又会怎么回答呢?

B Tree vs B+ Tree

B 树和 B+ 树的区别想必也是老生常谈的话题了,说起来区别,什么一个根结点的儿子数为 [2, M],一个数据只存在于叶节点等等,大多数人也是朗朗上口,倒背如流。MySQL 默认的存储引擎 InnoDB 会使用 B+ 树来存储数据,无论是表中数据的主索引还是辅助索引都会使用 B+ 树来存储数据,所以......为什么要用 B+ 树而不是 B 树呢?刚才说的那些区别,到底是为什么呢?

B 树和 B+ 树可以说都是平衡二叉树的变种。说区别前,我们先来看看平衡二叉树是为了解决什么问题而存在的。平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树数据结构,学过算法和数据结构的你一定知道,树的高度越矮,两边的节点越平均(平衡),越有利于我们将查找数据的速度优化到近于二分法查找,即 O(log n) 中的树高 n 越小,我们做一次数据访问和修改的代价越低。B 树和 B+ 树均为平衡多路查找树,对比二叉树最明显的区别就是一个父节点的查找路径不再是只有两个,可以是多个,这样一来便很直观地达到了一个目的——让我们的树更矮了。至于其他的区别,我们一一来辨明。

  • B 树的非叶子节点同时保存关键字和对应数据,而 B + 树的非叶子节点只保存关键字,具体数据只存在于叶子结点

B 树和 B+ 树均以页为单位,每一页的大小默认即为操作系统的页大小(大多数情况下为 4KB),设想 B+ 树在页大小不变的情况下,只在非叶子结点存储关键字而不是全部数据,此优化让 B+ 树比起 B 树可以在同样的页大小下增加更多的关键字数量,相应的树层数也会减少,增加读取效率。

  • B + 树叶子节点保存了父节点所存储关键字的所有具体数据

如上所说,两者均以页为单位划分数据,而操作系统在发现需要的数据不在内存中时,会以页为单位从硬盘上进行加载,这每一次加载便会触发一次 I/O 操作。假设我们要做一次范围查找,而范围的左右边界均在不同的页上,那么意味着想要找所有范围内的关键字数据,B 树需要我们多次从根节点页出发,依照查找路径访问不同的页,而 B+ 树中就不存在这个问,因为所有的数据都存储在叶节点中,并且这些叶节点之间往往又会通过类似链表的方式按顺序进行连接,在范围查找的场景下,我们只需要一次从跟节点页出发,到达范围的左边界后直接在多个子节点之间进行跳转,这样势必能比 B 树的多次换页查找节省大量的磁盘 I/O。

以上两个可以说是 B 树和 B+ 树最大的区别和其背后的设计原理,总结而言,B + 树的优点是:

  • 更矮,因为每个非叶子结点只需要存储关键字。
  • 更稳定,因为具体数据只存在于叶子结点上,所以每次查找的次数一定相同——为树的高度。
  • 更快,因为叶子节点间互相链接且保证有序,所以进行扫描遍历更快。

B 树也不是一无是处,如果经常访问的数据离根节点很近,也就是说数据的访问频率和树的高度相关联场景下,B 树因为在非叶子结点中本身存了数据,会比 B+ 树更快。

下次再被问同样的问题,除了说出这些区别,主动再讲讲区别背后的考量和出发点,一定会让面试官眼前一亮。比起只能说出区别,而讲不出为什么,高下立判。

结语

由于篇幅有限,我不能针对更多的问题进行分析,所以只挑选了几个我认为具有代表性的问题来进行阐述。本文旨在抛砖引玉,作为面试者,我希望你能在日后的面试过程中能够注重对问题本质认识的发掘,这样可以更快的发现候选人身上的闪光点,筛选掉水平良莠不齐的面试「背题家」;作为被面试者,我希望你能在日后的学习过程中更深入的了解自己所学知识,比起知道 How 和 Why,知道 Why why 才是真正对许多事物有透彻理解的终点。在这里送大家一句话:

Stay foolish, stay hungry.

求知若渴,虚心若愚。

参考文献