我见过不少简历很唬人的Java候选人,但很少见到简历和实际水准能差成蔡虚昆这个程度的。上个月我作为技术面试官,面了一位自称“精通Java、擅长高并发、在电商核心链路摸爬滚打三年”的后端开发。看完简历我挺兴奋,聊完第一轮我挺失望,聊完第三轮我彻底确认了:这就是典型的“水货程序员”。
之所以把这个面试过程整理出来,不是想嘲讽某个人,而是因为蔡虚昆在泥潭里趟过的坑,很多正在准备Java面试的人其实也在趟。你可能是应届生,也可能是工作了两三年想跳槽的Java开发,或者你本身就是被拉去当面试官的同行。这篇文章对这三类人都适用:我会站在面试官视角,把三轮问答的“提问意图”和“期望答案”摊开讲清楚,也把蔡虚昆那些常见的、危险的自毁式回答拿出来逐条拆。
先交代一下背景:蔡虚昆,3年经验,简历上技能栏写着“熟练掌握Java基础与JVM调优,熟悉Spring Cloud微服务、Redis、Kafka”,项目经历里有一条“负责电商订单中心核心模块开发,日均处理百万级请求”。单看简历,这就是一个典型的“大厂薪资候选人”放在面试官面前。可惜,简历上的每个字,最终都成了拆穿他的照妖镜。
1. 第一轮:简历上写得越漂亮的点,我越要往死里问
1.1 第一个照妖镜:一句“说说你最熟的项目”就把他钉在墙上
我习惯先不急着问技术细节,而是问一个开放式问题:“挑一个你最熟的项目,讲一下你的技术方案和你在里面的定位。”这不是寒暄,这是在测试三件事:一,他是否真的做过这个项目;二,他对自己负责模块的理解深度;三,他能不能把业务需求翻译成技术方案。真正做过的人,听到这个问题眼睛会亮,会主动说“当时的瓶颈是什么,我选了哪个方案,为什么选它”。蔡虚昆的反应是眼神躲闪,停顿了五六秒,然后说:“我主要负责订单模块和支付回调,用的Spring Cloud那一套,然后就是写接口,也没啥的。”
这句话基本就给我留了两个可以深挖的口子:支付回调。在电商场景里,支付回调是第一道幂等考题。我立刻追问:“支付回调你会怎么保证不重复处理?”他跟我扯了一堆“用Redis锁”“用唯一订单号做setnx”,但问到“setnx的key过期了怎么办”“重复通知来了你的状态判断逻辑写在哪一步”时,他沉默了。这个问题并不超纲,它拷问的是一个基础得不能再基础的业务边界:你在写项目的时候,到底有没有想过“同一事件发生两次”这种常态?
后来我在复盘时给候选人建议过一套回答模板,这里顺便送给读者。成熟的回答应该是:“支付网关回调是异步且可能乱序、重复的,所以我会先按支付流水号查本地订单状态,如果已经是‘已支付’就幂等返回;如果状态是‘待支付’,就加一把分布式锁,再执行支付成功后续处理,最后更新订单状态。”这套回答里有状态机、有幂等、有并发控制三步,缺一步都是没想清楚。
1.2 缓存一致性:他背过“双删”,但完全没理解为什么删
蔡虚昆的简历里详细写了“使用Redis做缓存,保证了缓存与数据库的一致性”。因此我问了一个几乎每个Java岗位都会问的题:“你项目里商品信息有缓存,用户下单后库存和商品详情的数据一致性怎么保证?”
他马上给我背了一段网上范文:“先更新数据库,再删除缓存;如果删除失败,就发MQ去删,或者做延迟双删。”背得挺顺,像背书机。但我不可能就此打住,紧接着就是连环追问:“先更新数据库再删缓存,还没有删缓存的那一瞬间,另一个线程读到旧缓存并写脏数据库怎么办?”“延迟双删的延迟时间设多少?为什么?”“你是只删这个key还是用模糊匹配去删?”
蔡虚昆愣了,只挤出两个字:“我项目中好像没遇到这种情况。”
这个问题背后的原理,值得在这里彻底讲透。缓存和数据库做一致性,业界最经典的方案就是Cache Aside Pattern,核心逻辑是:读时,先读缓存,读不到就读数据库,回填缓存;写时,先更新数据库,再删除缓存。为什么是“删缓存”而不是“更新缓存”?因为更新缓存是写放大,而且两个并发写请求顺序错乱时,缓存里很容易留下旧值。而删缓存之后,下次读请求会触发重新加载,数据自然被修正。
那为什么“先删缓存,再更新数据库”不行?因为旧缓存被删掉后,一个新读请求把旧数据库值回填到缓存,等数据库更新完成后,缓存里存的还是旧值,而且可能很久不会被纠正。所以正确顺序只能先更库再删缓存。至于“先更库后删缓存”的窗口期问题,主要是删除缓存这个动作失败了怎么办,所以才有延迟双删、MQ异步删、甚至Binlog监听后异步清缓存这些兜底手段。所有方案的目的都不是追求“强一致”,而是把脏数据窗口压缩到最小,再配合过期时间实现最终一致。这些逻辑只要他自己推导一遍就能讲清楚,但蔡虚昆只背了结论,没有推导过程。面试官一眼就能看出,这人没解决过真实线上问题。
1.3 集合与并发:没有源码支撑的“精通”是空的
蔡虚昆的简历上写着“精通Java集合框架”。我随口抛了一个高频中的高频:“HashMap在并发场景下有什么问题?ConcurrentHashMap在JDK 8里是怎么优化并发度的?”
他的回答是:“并发下HashMap会死循环,所以要用ConcurrentHashMap,JDK8里用了CAS和synchronized。”这话说得只能说没错,但信息量等于零。我追问:“JDK7的并发扩容是怎么造成死循环的?JDK8为什么解决了这个问题?JDK8 ConcurrentHashMap的size()方法和JDK7有什么不同?”他开始答非所问,甚至说出“size方法用的CAS循环”这种错误结论。
这里给所有准备面试的Java选手一个忠告:可以不知道“死循环”的每一次链表指针移动,但至少要能说清楚“头插法在并发扩容时会形成环形链表,所以get的时候会形成死循环;JDK8改成尾插法,环形链表问题在扩容时没有了,但并发读写的条件竞争仍然存在,例如扩容期间另一个线程插入数据可能丢失,修改和读取之间没有线程安全保证。”
ConcurrentHashMap这块,JDK7是分段锁,最多支持16段并发;JDK8废弃分段锁,改成对单个Bucket的头节点加锁,配合CAS做插入。这个设计直接说明了“锁粒度为什么更细、并发度为什么更高、内存占用为什么更低”。至于size(),JDK8先无锁统计,如果遇到竞争则再加锁统计,而不是纯靠CAS。这些细节,是判断一个人是真读过源码还是只记了结论的关键。
1.4 顺手考了几个冷门的小题,他的“精通”彻底失效
为了让面试结论更可靠,我还会在基础轮次穿插几个“小题大问”的点:数组越界异常会发生在哪些场景、Lambda表达式里为什么不能修改外部局部变量、正则表达式在项目里是怎么用的。这些题不算难,但能测出一个人是否只是“背大题”。
蔡虚昆在“数组越界”上勉强说了个“循环下标错了”,在“Lambda为什么不能改外部变量”上直接慌了。其实这个问题的本质是:Lambda捕获的局部变量必须是effectively final,因为Java的lambda本质上是语法糖,最终会生成一个内部类或者使用invokedynamic机制,被捕获的变量会被作为参数传入,如果允许在lambda里修改外部变量,变量副本和外部变量就会不一致。更深一层是Java语言设计者希望lambda保持“无状态行为”,避免并发环境下的可见性问题。蔡虚昆显然没想过这么深。
到第一轮结束,我心里已经给这个人打上了“简历有水分”的标签。但作为面试官,还不能立刻停止,我要再看看他到底能兜住多少,于是进入了第二轮。
2. 第二轮:并发、缓存与一致性不是会念术语就够了
2.1 幂等设计:他不清楚自己想说的是“防重”还是“业务幂等”
第一轮里“支付回调”已经露了怯,第二轮我专门把幂等单独拉出来问深一层:“你负责的订单模块里,有哪些接口必须做幂等?你怎么设计?”
这是一个非常暴露水平的题。蔡虚昆的答案还是那句“用Redis的setnx,做分布式锁,保证了不会重复处理”。这句话错在,他混淆了“接口防重”和“业务幂等”两个概念。分布式锁只能解决“同一时间只有一个请求在跑”,解决不了“前一个请求失败了,下一个请求带着同一笔订单号再次进来,系统能不能识别并返回正确结果”的问题。
更完整的幂等设计,通常有四条路可以走,面试里能讲出两条就已经及格了:
第一,请求方生成全局唯一的幂等键,比如“用户ID + 业务ID + 场景码”。服务端拿到幂等键后,先查防重表,如果存在则直接返回历史结果。防重表里要对幂等键建唯一索引,把“唯一约束”变成数据库层面强制,而不是只靠Redis。因为Redis有容量、过期和集群脑裂问题,数据库唯一索引是最后一道保险。
第二,业务表自身的唯一约束。比如支付流水表对“支付单号+支付渠道”建唯一索引,重复插入会报DuplicateKey,代码里拦截异常后走幂等返回逻辑。
第三,状态机约束更严格。订单状态必须是“待支付→已支付→退款中→已退款”,当前状态只有等于某个前置状态,才能流转到下一步。重复请求如果试图把“已支付”更新成“已支付”,在SQL层面加状态条件update...where status = '旧状态',更新0行就说明是重复请求。这比单纯靠Redis锁要可靠得多。
第四,最终兜底是对账。跨系统之间就算代码写得再严谨,也可能出现消息丢失,所以要定时拉取对账单,用对账结果修正不一致的数据。
蔡虚昆听完这些面露难色,只补了一句“我项目里好像没那么多问题”。这句话在面试中非常致命。你可以坦诚说“我们业务量没到这一步,但如果有这个场景,我会优先考虑数据库唯一约束加状态机”,这是合理的。但说自己“没遇到”还理直气壮,等于承认自己从来没有思考过系统的边界和异常场景。
2.2 分布式事务:能说出来不代表懂,AT和TCC的区别答不出来就是白搭
聊到电商订单,绕不开分布式事务。我问他:“订单服务和库存服务不在同一个数据库,下单那一刻,订单写成功了,库存扣减失败了,你怎么处理?”
蔡虚昆说:“我们用的Seata,配置一下就行。”我继续追问:“Seata的AT模式和TCC模式有什么区别?AT模式为什么能自动回滚?它依赖什么?”他答不上来,只说“反正是配置”。
这道题的标准答案其实不复杂,但必须理解到原理层面。AT模式的核心是:业务SQL照常执行,Seata框架在数据源上做了一层代理,在执行业务SQL之前,自动生成前镜像(数据修改前的快照),执行业务SQL之后,再生成后镜像,并把这两个镜像写入undo_log表。如果全局事务需要回滚,那就根据undo_log里的前镜像,用反向SQL把数据恢复回去。它的前提是本地事务必须由Seata的数据源代理接管,而且脏数据隔离靠的是全局锁。
TCC模式则完全不同,它属于业务侵入式设计,需要你手写三个方法:Try阶段做资源预检查和预留,Confirm阶段确认执行业务,Cancel阶段把Try预留的资源释放。TCC的优点是性能好、强隔离,缺点是编码量大,还要处理空回滚和悬挂问题。空回滚指的是Cancel执行时Try还没执行过,你要能判断出来;悬挂指的是Try被阻塞,Cancel超时执行了,之后Try才到达,导致预留的资源永远被卡住。
蔡虚昆一个点都没接住。可见他简历里的“Kafka、Redis、Seata”只是名字,业务里真正怎么落地的,他完全没有概念。
2.3 限流、熔断与降级:他懂得“用”,不追求“懂原理”
我又问了最后一个并发方向的问题:“你们那个每天百万请求的项目,怎么应对突发流量?”蔡虚昆的回答是:“我们用Sentinel,配了限流规则,不会崩。”
我追问:“你配置的是QPS限流,还是并发线程数限流?Sentinel底层用的滑动窗口还是令牌桶?熔断和降级有什么区别?”他又哑了。
这里把关键概念快速捋一遍,帮助读者建立体系。限流是保护自己:单位时间内只让一定数量的请求进来,进来的人正常处理,进不来的直接拒绝或排队,常用算法有固定窗口、滑动窗口、令牌桶、漏桶。Sentinel默认用的是滑动窗口计数,可以做到按秒级精确限流。令牌桶允许一定的突发流量,漏桶则强制匀速处理,更平滑。熔断是保护下游:当某个接口的错误率或慢调用比例超过阈值,就主动把调用链断掉,直接返回兜底结果或错误,避免自己这边的线程池被拖垮;熔断器有“关闭→打开→半开”三个状态,半开时会允许少量试探请求,成功则关闭熔断器。降级是提前准备好的兜底策略,比如接口挂了返回缓存数据或默认提示。它们三者通常配合使用,不是一回事。
蔡虚昆在第二轮的评分已经是“不建议进入终面”。不过按照面试流程,我还是要给他一轮场景题,因为场景题往往能看出一个人最后的底线。
3. 第三轮:场景题和排查题才是“水货”的终局考场
3.1 接口变慢的定位:他说“重启加日志”,我需要的是排查链路
我出一道真实的线上题:“你们有个订单查询接口,最近一周从P99 100ms涨到P99 500ms,你怎么定位?”这是每一个大厂后端都可能遇到的场景。蔡虚昆的回答让我失望到极点:“先重启一下,不行就加日志看哪里慢,再不行就加缓存。”
“先重启”,这是操作习惯,不是排查思路。一个合格的后端应该有一套成熟的排查顺序。我在这里把完整链路写出来,建议大家直接背进脑子:
第一步,看整体指标。先用top看机器的CPU、内存、负载;CPU飙高优先查代码逻辑和GC,内存飙高优先查堆占用和OOM。再用监控面板看接口的耗时趋势,确定是从哪个时间点开始变慢的,是否伴随发布、流量突增、依赖变更。
第二步,看线程和堆。如果CPU高,用jstack多次抓线程栈,对比两次抓取结果,找到一直在运行的线程,定位到具体业务代码。如果内存高,用jmap或arthas查看堆的类直方图,看是不是某个缓存对象无限增长,必要时抓一份heap dump放在一边,等会深度分析。
第三步,看慢SQL。MySQL开启慢查询日志,或者通过Druid/MyBatis的统计插件把超过阈值的SQL捞出来,用EXPLAIN看执行计划,重点看type是不是ALL全表扫、possible_keys和key是否一致、rows估算行数是不是几十万,索引失效通常就是字段函数处理、隐式类型转换或不符合最左前缀。
第四步,看下游依赖。用SkyWalking或者Zipkin看调用链,找到耗时分布在整个链路里的哪一段,如果是下游Redis或RPC慢,就进一步看对应集群的健康状态。
第五步,看是否并发瓶颈。如果单台没问题、整体流量高,那就看当前副本数和连接池是否打满,再考虑限流或者扩容策略,而不是无脑加机器。
这套链路每个环节都有明确目的,面试官听到的是你的“排查意识”,而不是只看到一个词条。
3.2 一个真实到让人头疼的报错:lombok与编译器版本不匹配
第三轮我故意抛了一个“经验题”:“你开发环境里Java项目编译,报错了‘You aren’t using a compiler supported by lombok, so lombok will not work’,你会怎么做?”这个问题看起来冷门,但实际上很多Java开发都踩过,也最能区分“只在业务里搬砖”和“真解决过问题”的人。
蔡虚昆说:“百度一下,照着改一下就好了。”可面试官想听的,是你能不能判断“这个报错背后到底是谁和谁不兼容”。这里我直接把排查思路展开给各位:
Lombok是在编译期通过注解处理器(Annotation Processor)来修改或生成字节码的,所以它对JDK版本和编译器版本非常敏感。当你的JDK升到比较新的版本后,项目中用的Lombok版本太老,就会出现这个提示,意思是“你当前使用的javac编译器是Lombok这个版本不认得的编译器”。
排查的起点是版本匹配问题,而不是代码问题。你先运行mvn -v和javac -version,确认当前JDK版本;再去pom.xml里看lombok版本。如果发现JDK已经到19、21乃至更高,lombok还是1.18.24这类老版本,基本就是根因。解决办法是把lombok升级到官方支持的版本,例如升级到1.18.30以上的版本,如果你的项目还在维护,甚至可以升级到官方最新稳定版。
还有一个非常隐蔽但常见的坑:IDEA的构建编译器默认可能不是JDK的javac,而是IDE自己的编译器,或者Maven的Java编译器指向了另一个JDK。所以哪怕项目里jdk版本显示正常,也要检查IDEA的“Settings → Build Tools → Maven → Runner → JRE”以及“Project Structure → SDKs”中实际用的JDK,必要时还要看Maven是不是用的独立安装的JDK而项目又是另一个JDK。很多人卡在这个报错上很久,就是因为只查了项目环境,没查编译器的实际执行环境。
这个题的价值在于:它考的不是你背没背过,而是你遇到“编译器黑盒”时,敢不敢把它当系统来排查。蔡虚昆显然没有这种经验。
3.3 系统设计题:短链服务,考的不只是“会用Redis”
最后一轮,我留了十五分钟给他一道典型的系统设计场景题:“让你设计一个短链服务,长链接怎么变短?用户访问短链返回什么?用什么存?并发高怎么办?”
这种题没有绝对标准答案,但面试官在考核你的抽象能力、边界思考和工程落地意识。蔡虚昆的回答不到三句话:“用Redis存长连短连映射,访问的时候去Redis查一下就是咯。”这句话的空洞在于,完全没有考虑重复、无效、容量、并发和跳转方式。
一个能拿高分的回答,大致应该拆成四个模块:
第一,生成的短码怎么来。最稳妥的方式是用分布式发号器:从数据库拿一个ID段,或者用雪花算法生成唯一ID,然后把数字转成62进制字符串,这样短码长度可控。不推荐直接对长URL做MD5再截取,因为碰撞和可读性都差。这里还要想清楚一个问题:同一个长链接传进来两次,是返回同一个短码,还是短码应该不同?这个没有标准答案,但要能说出你的取舍,比如为了方便去重统计,就要求同链同码;为了隐私和防探测,就可能选择每次生成不同短码。
第二,短码到长码的存储。缓存层用Redis做KV是常规操作,Key设计可以用短码,Value用完整长链接并设置过期时间。存储层用数据库做持久化兜底,表中要有短码、长链接、创建时间、过期时间,并对短码建唯一索引。Redis命不中再到数据库查一次,回填缓存。
第三,访问流程。用户在浏览器里输入shortUrl,网关解析出短码,去Redis里查,找不到就去数据库查,查到后返回一个302重定向,让浏览器发第二次请求到真正的长网址URL。这里还有一个隐藏的知识点:301和302的区别。301是永久重定向,浏览器会对结果做缓存,之后所有访问都不再来打你的短链服务;302是临时重定向,每次访问都会经过短链服务一次,这样才能做点击统计、安全校验、IP黑白名单,所以大多数短链平台都用302。
第四,并发抗压。缓存可以拦住绝大多数访问流量,数据库只处理Cache miss的请求,所以服务本身压力不大;但如果高频访问集中在某个热门短码上,就要防止“缓存击穿”,可以在Redis层加锁,或者把缓存过期时间加随机抖动避免同一时刻集体失效。真正正负责任的设计还会考虑对短链的访问做限流,防止有人用脚本疯狂扫描短码枚举。
蔡虚昆听完这个框架,表情极其难看。这几乎宣告了他与“大厂Java开发”之间隔着的不只是一个知识量,而是长期的工程思维训练。
4. 复盘:蔡虚昆们身上最危险的五个共同信号
三轮面试结束,我给他的评价是:简历有夸大成分,基础不扎实,项目经验水分大,不建议进入下一轮。这个结论并不出人意料,但我在他身上看到的几个信号,几乎在每一批“水货程序员”身上都会重合出现。如果你现在正在准备Java面试,请拿这几条对照一下自己。
4.1 术语密度很高,信息密度很低
“Redis setnx”“Seata”“Sentinel”“分布式锁”这些词,蔡虚昆说得出来,但每到一个词,他都给不出“为什么选它、用在哪个环节、失效时怎么办”这三层解释。术语是你经验的浓缩,不是你简历上的装饰品。能讲出取舍和边界,术语才有价值;只报菜名,只会让面试官觉得你在术语里躲藏。
4.2 只回答“是什么”,从来不回答“为什么”
问他“Cache Aside为什么先更新库再删缓存”,他说“规则就是这样”。规则之所以成为规则,是因为背后有脏数据路径;把脏数据路径想通了,你就不需要背规则。面试官真正检验的是你脑中的路径模型,而不是记忆模型。
4.3 一被追问就退缩,把“追问”当成“刁难”
我承认有些面试官的追问会让你不舒服,但大部分追问都是在给你机会展示深度。蔡虚昆被追问时的口头禅是“这块我没怎么接触”“项目里没遇到”,一句话就把自己的上限锁死了。面试时并不知道所有问题,但你完全可以说“这个问题我确实没在线上遇到过,但按照我对这套系统的理解,我会先去查数据库的唯一约束和状态字段,再做一层MQ消费幂等”,这种回答即便不完美,也能证明你在思考。
4.4 技术栈写得很全,但每个都说不深
简历上写“熟悉消息队列Kafka”,就要至少能说出分区、副本、消费者组、消息重投和幂等消费。写“精通Redis”,就要说得清持久化、过期策略、内存淘汰、分布式锁和缓存穿透。技术栈不是越多越好,写上简历的技术,默认就等于“敢被面试官往死里问”。写一项就要能扛住十连问,扛不住就删掉,不要给自己埋雷。
4.5 把“背八股文”当成“准备充分”
八股文可以背,但背的目的不是为了原样输出,而是为了提取骨架和逻辑。比如那道缓存一致性的题,你背“延迟双删”很容易,但如果能自己画一个时间轴,把三个线程的读写时序画出来,你就知道延迟时间到底要多久,为什么叫“延迟”,为什么还存在失败场景。蔡虚昆准备面试的时间肯定不少,但全花在了“记住答案”,没有花在“推导答案”,这是最可惜的事。
5. 逃过“水货面试”的Java选手,提前做了哪几件事
我很清楚,文章写到这里,很多读者会有点焦虑。所以最后一部分,我只讲怎么自救,不讲怎么嘲讽。避免成为蔡虚昆,其实没有那么玄,核心就是几个能在日常工作中逼自己养成的习惯。
5.1 简历里的每个字都必须经得起三连问
拿你现在的简历,对每一个技能点写三连问,然后自测:
比如写了“精通Java并发编程”,就对自己问:synchronized和ReentrantLock的底层实现各自是什么?volatile的可见性是靠什么实现的?线程池的7个参数怎么配?拒绝策略有哪些,各适合什么场景?答不上来,要么去查,要么把“精通”改成“熟悉”甚至“了解”。面试前把这个动作做一遍,相当于给自己做了一次全身体检。
5.2 项目复盘用“业务背景—技术选型—挑战冲突—数据验证—取舍反思”去讲
我强烈建议每个Java候选人准备一套属于自己的“项目故事”,结构固定为五段:业务背景是什么、当时有几套技术方案、你选了哪个为什么、上线后用什么指标证明效果、如果再给你一次机会哪里会改。这五段讲完,面试官基本不需要刻意追问,因为里面已经涵盖了大量考点。蔡虚昆最大的问题,就是他连“业务背景”都讲不清,只停留在“写了什么接口”的层面。
5.3 高频基础题要动手画图,不要背答案
并发、缓存、JVM、集合框架这几大类,每一题都在纸上画图。画HashMap的put流程,画ConcurrentHashMap的CAS和synchronized加锁逻辑,画JVM堆内对象分配和GC过程。手画一遍等于在脑子里跑一遍代码路径,画完之后你会发现,很多以前需要“背”的答案,自己都能推出来了。面试时可以不带纸笔,但你脑中的画面感会帮助你在高压环境下保持逻辑稳定。
5.4 面试中的诚实不是劣势
最怕的不是你不会,而是你假装会然后被问穿。面试中遇到不会的题,大方的回答:“这个原理我知道的不够深入,不过按我的理解它大概涉及哪些点,我可以顺着推测一下吗?”然后给出一个基于常识的推理过程,哪怕推测不准确,面试官也看到的是你的思考路径而不是背诵能力。“不会”加“会查”加“能推理”,远比“我都知道”加“一问三不知”有价值。
5.5 让我这个老面试官说句掏心窝的话
我面了这么多年Java候选人,最后给谁发offer,通常不是因为谁把所有题都答对了,而是因为在某一次追问中,候选人眼里闪过光,说“这个问题我真正追查过,是因为什么原因导致的,我当时怎么一步步缩小范围找出来的”。那种瞬间,面试官会很清楚地知道,这个人不论基础强弱,他至少拥有程序员最值钱的东西:对所构建系统的敬畏和好奇心。蔡虚昆缺的恰好就是这个东西。他以为简历写满、八股背熟,就能混过面试,但他不知道面试官找的不是一台复读机,而是一个愿意在报错、异常、慢查询和海量并发面前较真到底的技术伙伴。如果你正在准备面试,请把这句话放在心上,然后从今天起,试着不背答案,而是去问自己一句“为什么”。