最近面了家拿了千万美金投资的初创公司,可能我投的简历比较具有独特性,所以他们的首席架构师开场也比较直接。
"我们需要招募一位技术能力非常全面的人才,希望精通前后端主流技术,并且拥有架构师经验。"
"没问题,我觉得我们应该还是比较匹配的。"
我还是一如既往的自信。
架构师挺客气,和我说他姓谢,叫他谢哥就行。虽然我表面叫他谢哥,但看到他那满脸胡茬的脸,我心里还是称呼他谢叔,虽然我不知道他的真实年龄,但是凭直接我也能感觉到我和他不是一辈人。
虽然谢叔表面上很谦和低调,但是不知是有意还是无意中告诉我他们公司拿了千万美金投资,不缺钱,公司里都是一些 500 强的海归啊、qs500 的硕博啊什么的。更重要的是不小心透漏出来他是公司的联合创始人,拿股份的那种。虽然在他眼里似乎都是不经意的、无意间的言辞,但我心知肚明。
简单寒暄过后,谢叔开始和我聊了一些技术。
从编程语言:Node.js、Java、Go、Rust、Python;到数据库:Oracle、PostgreSQL、MySQL、MongoDB;再到一些中间件:ElasticSearch、Kafka、Spark、Flink、Redis;再到 CICD DevOps:Jenkins、Docker、K8S;再到 Cloud:AWS、Azure;再到研发范式:DDD、TDD、BDD;再到流行的框架:Nest.js、Koa、Express、React、Next.js......
其间还聊了其他很多技术,我记不清楚了。我只能说此人绝对是个不折不扣的高手。大部分都能和谢叔聊聊,当然我也不是什么都懂什么都擅长,有一些技术可能只是跑过 Demo,像 ES、Spark、Flink 这些偏大数据相关的技术,我就更不擅长了,只是简单调用过 API,不敢多发言。不过也有很多技术非常精通,像 Node.js、PostgreSQL、React、Next.js,我就侃侃而谈,聊了很多。
因为时间有限,我们聊得多,但聊的大多数主题都不是很全,主要就是些核心理念、原理、高频场景、核心 API 之类的。虽然我最近面了挺多场,也不乏一些技术过硬的大佬面试官。但是技术栈如此之广,技术的深度也如此扎实,这是第一次碰到。
(因为这块的面试比较随意,大而杂,我不好容易整理成面试题,后面会有涉及到场景的面试题)
我内心不禁惊叹,一个首席架构师,日常琐事繁忙,哪来的时间把这么多技术吃的这么透呢?
经过前面的技术初试,我能感觉到谢叔的兴奋,好像是一个痴迷剑术的人碰到了另一位能彼此欣赏的剑客。所以无形之中大家刚见面的那种尴尬感荡然无存,也增加了些许亲近感。趁着氛围不错,我忍不住把心中疑问问了出来。
谢叔解释说他不做管理方面的事务,自己几乎全身心投入在技术研发这一块,所以技术能力还算不错。之所以参与招聘工作,是因为招聘的是自己团队的人,所以格外用心。
到这里,我心里的疑惑算是解开了。
接下来,谢叔开始问了几个真正有价值的问题。
你掌握了很多技术,但是我发现有些技术你并不是非常擅长,这是不是意味着你在某些方面缺乏深度呢?
我认为像您这样技术如此全面又具有如此深度的人实在是罕见。(开局捧他一下)
但是我认为,(开始转入我的逻辑和节奏里)我们不需要 100% 掌握某一门技术才开始进入开发阶段或者去寻找工作。大多数情况下,我们只需要掌握一门技术的 60% 就完全够用了。
举个例子,您即使同时精通 Java、Go、Node.js,但当您以开发者的身份进入某个项目时,其实也大概率只是在使用一门语言。我们再进一步来讲,在开发阶段,您也只是在使用一小部分 API 来做一大部分的事情。
当然,我不否认深度的重要性(再次反转),而且我认为具有技术深度是非常有必要的。但每个人的时间和精力是有限的。我会优先把我最常用的技术,比如前端的 React 和 Nextjs;后端的 Node.js 和 Express 吃透。这部分属于我的领域,需要达到 90 分,也就是说在这些领域里面我应该是一个专家,要具备权威性。
再次就是数据库的 PostgreSQL 和 Redis,这也是我非常常用的技术,但使用频率没有上面提到的语言和框架高,不过它们很关键和重要,这部分我需要达到 80 分。
再次就是一些通用的技术,比如通信协议:TCP/IP、HTTP;研发范式:DDD、BDD 这些技术,它们虽然很重要,但是在日常工作中并不会频繁使用,所以这部分我需要达到 70 分。
剩下的一些技术,像 ES、Kafka 等,并不是每个项目都用得上,事实上我做的很多项目都用不到它们。另外一些数据库技术,像 Oracle、MySQL,它们和 PostgreSQL 相似性很高。还有 CICD 的技术 Jenkins 等,也不会常用。所以以上这部分都不是我高频使用的技术,所以我只需要知道它们的用法或者 API,并且理解它们的核心理念,主要场景,以及能解决什么问题就够了。这样当需要我去操作它们的时候,对照着文档搞定就 OK。所以这部分我只需要达到 60 分甚至 50 分就可以了。
总之我不是盲目的学习技术(合理分析后再次总结),是由两个因素驱动。首要因素是实用性,次要因素是兴趣。比如我做一个 AI 的项目,那么就一定会去研究 LangChain,因为 LangChain 解决了很多开发中的问题,大幅度降低了我们的开发成本和代码复杂度。能够学以致用的东西是最容易掌握的。另一类,比如说游戏开发,我一直对游戏开发感兴趣,所以我会去学 Cocos、Spine、Three.js 这些开发框架和工具。但它们只能当作兴趣,在时间有限的情况下,我就会去停止学习它们,而是转向更具实用性的技术。为什么兴趣不是首要的呢?(对总结做出合理解释)因为我认为兴趣并不能持久,如果纯粹依靠兴趣去做事,很容易半途而废。相反,实用性才是学习技术的首要驱动力。
听完我的一顿输出后,谢叔露出了满意的微笑。
接下来我们又聊了几个具体的技术性问题,这里简单记录三道题。
第一道是网络协议的典型八股文:TCP 三次握手与四次挥手的流程和设计原理。
第二道是前端的一个场景:在 React 中设计一个全局的 Toast 方法。
第三道是后端的系统设计题:设计一个短链服务。
请解释一下 TCP 三次握手与四次挥手的流程和设计原理
(我很讨厌八股文,因为大多数情况下它们没什么用。但我也很喜欢八股文,因为大多数情况下它们唯一的作用就是作为送分题。)
TCP 是一个可靠的传输协议,它有几个特点:数据无破损、无间隔、非冗余和按序排列。
三握四挥都是为了完成这些能力。
三握是在客户端首次连接服务端时发生的事情。在一切开始之前,客户端和服务端都处于 CLOSE 关闭的状态。首先服务端先启动,监听某个端口,就是 TCP 的端口,这时候服务端就处于监听 LISTEN 状态。然后客户端会通过服务端的 IP 和端口,发送第一个报文给服务端。这个报文一般也叫 SYN,就是同步序列号 Synchronize Sequence Numbers 的意思。它会在报文的序列号里附带一个客户端生成的随机初始化序号。然后在报文的 SYN 标志位设置成 1,SYN 在报文的 111 位。发送完这个报文之后,客户端就进入了等待的状态 SYN-SENT。
服务端收到 SYN 报文之后,首先初始化自己的随机序号,组合一个新的报文发给客户端。这个报文的序列号位置时自己的随机序号,应答号的位置是客户端序号 +1。同时把 SYN 和 ACK 标志位都设置成 1。ACK 在报文的 108 位。发送完之后,服务端进入了已接收 SYN-RCVD 的状态。
客户端收到这个报文之后,开始第三次握手。它会再把服务端的序号 +1,放到应答号的位置。然后把 ACK 设置成 1,发给服务端。发完之后客户端会进入已建立连接 ESTABLISHED 的状态。服务端收到这个报文之后,也会进入已建立连接状态。三次握手到此结束。
三次握手的设计主要是为了解决两个问题。第一个是防止重复连接,节省资源,第二个是可以保证双方具有接收和发送的能力。
四挥是在断开连接的时候发生的事情。双方都可以主动断开连接。第一次挥手,首先断开方会发送一个报文,在 FIN 的标志位设置为 1,FIN 在 112 位。然后进入终止等待 FIN-WAIT-1 状态。第二次挥手,被动方收到之后,回复 ACK 报文,进入关闭等待 CLOSE-WAIT 状态。主动方收到后,进入 FIN-WAIT-2 状态。第三次挥手,被动方处理完数据后会再次向主动方发送 FIN 报文,然后进入 LAST-ACK 状态。第四次挥手,主动方收到后,回复一个 ACK 报文,进入 TIME-WAIT 状态。被动方收到 ACK 报文后,进入关闭 CLOSE 状态。主动方等待 2MSL 时间后,自动进入 CLOSE 状态,连接断开。2MSL 就是 2 倍的报文最大生存时间 Maximum Segment Lifetime。
四次挥手的设计主要是为了解决一个问题。就是断开连接需要等待数据处理完成。任意一方一旦发送了 FIN 后就不会再去继续发数据了,但仍然可以接数据。
(因为我在大学当老师,教的还是网络信息专业的学生,这种书本上的知识倒背如流其实是一种老师的基本素养,所以这一题对我来说就是不折不扣的送分题。)
请在 React 中设计一个全局的 Toast 方法
(我以为谢叔应该是一个偏后端的工程师,没想到前端也会如此细致。)
Toast 组件通常用在一些操作的反馈上。比如操作成功、操作失败等。
我们主要有两个步骤,第一是设计并实现一个 Toast 组件,第二是让这个组件支持全局使用。
我们先来做第一个步骤:
设计 Toast,它应该具备以下属性:
- 颜色 color:枚举类型,通常会有四个值,成功 success、失败 fail/error、警告 warning 和信息 info。
- 是否可关闭 closeButton:布尔值,开启的话会有一个关闭按钮。
- 自动关闭时间 authHideDuration:数字,毫秒数,设置的话自动关闭。
- 图标 icon:Node,Toast 的图标。
- 开启的回调 onOpne
- 关闭的回调 onClose
然后开始实现 Toast,我们为 Toast 在 body 中创建一个 div 容器作为 root,然后通过 Portal 把 Toast 直接渲染到 toast root 中。这样它不会受到父组件的样式和事件影响。
实现上比较简单,只需要把 toast 设置成一个 fixed 布局,然后调整对应的样式就可以了。
接下来是第二个步骤,如果要实现全局的 Toast 组件,那么就必须在整个应用中共享一个 Toast 组件,也就是单例模式。同时需要考虑全局的状态管理。
我们可以设计一个 ToastContext,用它的 Provider 组件包裹在应用的最外层,通常是 App 组件。然后把 Provider 的 value 设置为一个 show 方法,show 方法应该有一个 message 参数和一个 options 参数,在 options 参数中可以设置上面设计的属性。然后在最外层的组件中维护所有的状态,同时增加一个 open 状态,用来显示 toast。当调用 show 方法时,根据 options 的值设置对应的状态,并且把 open 设置为 true。这样就完成了全局状态的注入。
然后我们应该导出一个 useToast 的方法,通过 useContext 引用 ToastContext。这样就可以在项目的任意组件中调用到对应的 show 方法,控制 Toast 组件的显示。
(我开发过很多组件库和组件,这种题都是轻车熟路的事儿。)
请设计一个短链服务
短链服务比较复杂,我可能不能很好的把所有细节都讲清楚,这里就尽量把重要的部分讲清楚吧。(开局先留三分退路)
短链的场景基本都是因为原始链接太长了,比如链接上有一大堆参数,在一些互联网平台上进行分享会受到限制,比如阿里云的短信原来最多只能有几百个字符,现在改成 1500 个字符了。甚至连浏览器都有长度限制,比如 Chrome 是 8182 个字符,Firefox 是 65535 个字符。(为什么记得这么准确?因为大学里面给学生讲过)短链服务就是把长链接改成短链接。访问短链接重定向到原始链接。重定向时使用的状态码应该是 302,它表示临时重定向,不会从浏览器中缓存。
流程其实很简单:在系统输入长链,生成唯一 ID 作为短链,将短链和长链进行映射,并保存到数据库。用户访问短链,返回 302,并跳转到长链。
其中两个技术难点分别是发号器和映射关系的存储。
因为需要持久化存储,我们可以使用 PostgreSQL 作为数据库,保存映射关系。同时可以在数据库上面用 Redis 做一层缓存来提升读的性能。为了防止缓存击穿,我们还可以在 Redis 之上再加一层布隆过滤器,可以提升系统稳定性。
分布式发号器比较难,我们这里可以设计为中心化发号器。发号器需要做的事有两点,一个是保证 ID 全局唯一,这是基础。第二个是尽量可以递增,这样可以在入库时顺序写入,提升写的性能。具体的生成方案也有很多,最简单的是直接使用数据库或者 Redis 的自增主键,这是最简单粗暴的方式,不过并发量很低,需要慎用。第二种方法是 UUID,优点也是简单,不过性能比自增主键高,缺点是 36 个字符太长了,太占空间,而且不是递增的。第三种是雪花算法,优点还是简单,不过更稳定、长度只有 19 位。唯一的缺点是依赖服务器系统时间,如果能处理好服务器系统时间的话,基本是没有缺点。所以推荐使用雪花算法。如果时间有限,可以考虑前两种做法。
(刚好从零到一研发过短链服务,甚至连分布式 ID 发号器都是我从零开发的)
这次面试整体来说体验还是非常不错的,让自己从技术面试上感受到了久违的快感。希望可以拿到他们的 offer。
以上就是本次面试中碰到的一些技术性问题和对应的回答,希望对你有所帮助。