题目一:Lambda 里的 this 指谁
在 Lambda 里写 this,指的是谁?把这段改回等价的匿名内部类写法,同一个位置的 this 又变成指谁?一句话说清根因,为什么这两者不一样?
我答的是:指外层作用域的 this,也就是定义这个 Lambda 的那个方法所在的对象。外层是实例方法时 this 就是当前实例;外层是静态方法时 this 没有意义,编译直接报错。根因那句我也答了。Lambda 不生成新的类,也不新开一层作用域,它和外部共享同一个 this。匿名内部类会生成一个新类,有自己的作用域和自己的 this。
题目里"改回匿名内部类之后 this 又指谁"这半问我漏了。助手补的答案很直白:匿名内部类里那个 this 指的就是匿名内部类自己的实例。他还顺手确认了我那句"静态方法里 this 没有意义",准确说法是编译不过,不是"没意义"。
同一天还问了 Stream 的收尾方式。判断有没有、是不是全部、是不是一个都没有,分别用 anyMatch、allMatch、noneMatch,返回都是 boolean。想找满足条件的那一条就停用 findFirst,它返回的不是裸对象,是 Optional。把一堆元素压成一个结果用 reduce,两个参数是初始值和累加器函数。这三问我答得比较顺,助手补了一句 reduce 还有单参数版本,返回 Optional,因为流为空时压根没有结果。
Optional 那边问的是链式取值。有值就取 name 继续往下走,用 map 没问题。但如果 getName() 本身返回的就是 Optional,map 会套成两层,这时候要换 flatMap。助手给了个记忆法:flatMap 做的事就是"里面还包着一层包装时帮忙拍平",跟 Stream 里的 flatMap 是同一个思路。
题目二:连着改十次样式,浏览器真的重绘十次吗
一段同步代码里连着改同一个元素的 10 次样式,浏览器会老实重绘 10 次吗?不会的话,真正的重绘发生在事件循环的哪一步?微任务队列正在清空时,某个 then 里又冒出一个新的微任务,它排到哪儿?这一轮什么时候才算清干净?
我答的是不会重绘十次,浏览器会先标记这个元素要变,等宏任务微任务都清空之后才重新渲染,所以那十次会合并成一次。微任务那问我答的是新任务排到当前队列的末尾,继续在这一轮检查点里执行。这一轮清干净的标志是队列为空,没有新的微任务再加进来。
助手补了一层:真正的渲染发生在事件循环的渲染更新步骤里,浏览器还有帧率节流,60Hz 下大约 16.7 毫秒才一帧,所以同一帧里改十次必然合并成一次。前端这块还有一问是解构。const { count } = state 之后 count 不再是响应式的。解构是取值复制,Proxy 的 getter 追踪到这里就断了。要保持响应式用 toRefs 或者 toRef。前者把整个对象转成每个属性都是 ref 的普通对象,适合一次解构多个。后者只转某一个属性。
题目三:间隙锁防的是幻读
RR 级别下,一条 WHERE id = 5 的 update,除了 id=5 这一行,锁还会顺带罩住旁边一段范围吗?这种把缝也锁上的锁是为了防脏读、不可重复读还是幻读,为什么光锁住那一行防不住?
我答的是 Next-Key Lock,也就是记录锁加间隙锁,会锁住上一条记录到 id=5 之间那道缝。防的是幻读,因为幻读的本质是同一个事务里两次范围查询,第二次多出别人新插入的行,把前面的范围一起锁上就能挡住。
助手给了个例外,这条我觉得是今天最值钱的补充。如果 id 是主键或者唯一索引,并且等值查询真的命中了那一行,InnoDB 会退化成只加记录锁。间隙那把锁是不加的。只有范围查询、等值没命中、或者索引不唯一的时候,才会连间隙一起锁。他说这个例外面试官很爱追,因为背过"行锁加间隙锁"的人一被问到唯一索引就会卡住。
题目四:改完数据库,缓存怎么处理
库里一条记录改了,Redis 那份缓存怎么办?主流是先更库再删缓存,还是先删缓存再更库,为什么这么选?万一删缓存那一步失败了,后果是什么,常见怎么兜?
我答的是先更新数据库再删缓存。理由我举的是反例:如果先删缓存,这时候来一条读请求,它会读到数据库里的旧数据,然后把它写回缓存,缓存就一直是旧值了。删缓存失败的后果我也说了,缓存里留着旧值,后面读请求会一直读到旧的,直到它过期。兜底我列了四条:把删除丢进消息队列异步重试、延迟双删、给缓存设 TTL、写入时带版本号或者逻辑过期。
助手补了两处。一是更省心的做法是订阅 binlog 异步删缓存,业内比较出名的就是那套基于 canal 的方案。二是延迟双删解决的是"先更库再删缓存"自己残留的那个极小并发窗口,不是给"先删缓存"擦屁股的,这个因果我原先没分清。
题目五:慢启动的"慢"是起点小
滑动窗口管的是接收方还接不接得住,拥塞控制担心的是另一码事,它担心什么?这两个值怎么合成一个实际能发的窗口?慢启动名字里带慢,实际干的是什么,为什么不直接按最大速度发?
我答的是拥塞控制担心网络堵不堵,实际发送窗口取 min(接收窗口, 拥塞窗口)。慢启动那个"慢"指的是起点小,从小量慢慢加大;一上来就按最大速度发,网络撑不住就是丢包。
助手补了增长曲线:慢启动其实是指数增长,每过一个往返翻一倍,到阈值之后转成拥塞避免、变成线性加一;真丢包了走快重传和快恢复。所以名字里的"慢"说的是起点低,不是说涨得慢。