今日八股:一条没索引的 UPDATE,锁住了半张表

今日八股:一条没索引的 UPDATE,锁住了半张表

题目一:方法引用放回 Stream 里就不好使了

users.stream().map(User::getName) 里 User::getName 顶替的是哪个函数式接口?写成 map(u -> u.getName()) 有区别吗?map(s -> s.substring(1, 3)) 能不能直接写成 map(String::substring)?

我答的是 Function<User, String>,写成 lambda 没区别,只是写法不同。第二个问题我答「不行,substring 有重载,还得要参数,lambda 里带的是固定实参」。

方向对,但归因不准。真正的卡点是参数个数:map 要的是 Function<String, String>,一个进一个出;String::substring 无论哪个重载都至少要吃一个起始索引,写成方法引用就等于 s -> s.substring(),而这个无参版本 JDK 里根本没有。重载本身不是障碍,编译器能按目标类型挑;不行的原因是方法引用表达不了「把 1 和 3 写死」这件事。这种地方老老实实留着 lambda 就行。

reversed 放哪个位置

要按 age 倒序、age 相同再按 name 正序,有人会写 comparing(User::getAge).thenComparing(User::getName).reversed()。这个链跑出来 name 那一级也跟着反了,因为 reversed 反转的是「到它为止整条链的结果」。把 reversed 提到 comparing 后面、thenComparing 前面,它就只作用在 age 那一级。

顺带问了 comparing 和 comparingInt 的差别,我答的是前者通用但可能装箱、后者专门给 int 用少一层装箱。助手补了一句:comparingInt 拿到的是原始 int,比较时连 Comparable 都不用碰。

题目二:用闭包做一个防抖

搜索框每敲一个字就发一次请求太浪费,改成等用户停手 500ms 再发。这个 debounce 里,timer 为什么非得放在外层函数里?换成每次调用时自己在返回的函数里 let timer,会出什么问题?防抖和节流差在哪,各举一个更适合它的场景。

我答的是:timer 要在多次调用之间共享。返回的那个函数会被反复调用,每次都要清掉上一次的定时器、再设一个新的,所以 timer 必须是同一个变量;外层函数的作用域正好提供这个共享变量,返回的函数通过闭包一直引用它。写在里面的话,每次调用都是新的 timer,clearTimeout 清不到上一次。

防抖和节流我答的是「防抖等你停手一段时间再执行,节流是固定间隔内最多执行一次」,场景给的搜索框和按钮防重复点击。

助手补了两个工程上的细节。那段代码用 function 而不是箭头函数,是为了保住 this,fn.apply(this, args) 那一行也是配合它的;另外组件卸载时要记得 clear,不然后台定时器一直挂着,闭包里的变量也释放不掉。

题目三:插槽三种,各管什么

子组件 Card 的模板里写了 <div class="card"><slot /></div>,父组件写 <Card>这里是内容</Card>。slot 是干嘛的?子组件模板里压根没写 slot,那段内容会怎样?子组件分头部和主体两块要父组件分别填,用哪种插槽、父组件怎么写?能把子组件内部数据交给父组件那段的,叫什么插槽?

我答的是:slot 是插槽出口,告诉 Vue 把父组件写在子组件标签中间的内容渲染到这里;子组件没写 slot 的话那段内容会被丢掉。第二问给的具名插槽,子组件里两个带 name 的 slot,父组件用 template 加 #header、#main 分别填。剩下那个叫作用域插槽。

助手把写法补全了:父组件那边的 #header 是 v-slot:header 的简写;作用域插槽在子组件侧要写成 <slot :user="user" />,父组件用 <template #default="{ user }"> 解构出来用。

题目四:sticky 为什么粘不住

列表页想让表头滚到顶之后粘在窗口顶部。sticky 和 fixed 差在哪,它相对谁定位、什么时候才开始粘?写了 position: sticky; top: 0 却没生效,常见原因有哪些?

我答的是:fixed 相对视口定位,元素脱离文档流,一直固定在视口某个位置;sticky 相对最近的滚动祖先定位,到阈值之前表现得像 relative、跟着正常流走,碰到 top 或 bottom 才像 fixed 一样粘住;只写 position: sticky 不写 top 或 bottom 是不会粘的。

没生效的原因我给了三条:没写阈值、祖先元素有 overflow: hidden 或 auto 或 scroll(滚动容器变成了它,粘不住或者粘错地方)、父元素高度不够。

助手补了表格场景里的第四条:sticky 设在 thead 上经常不生效,要设在 th 上,另外 border-collapse: collapse 会让它彻底失效。后台系统里那种「表头怎么都不粘」基本都栽在这。

题目五:ORDER BY 什么时候能用上索引

orders 表上有联合索引 idx_a_b (a, b)。SELECT * FROM orders WHERE a = 1 ORDER BY b 能不能直接按索引顺序读出结果、不再额外排序?ORDER BY b DESC 呢?条件换成 WHERE a > 1 ORDER BY b 还能免掉排序吗?这个查询很常用的话索引怎么调?

我答的是能,因为索引里先按 a 排序、a 相同再按 b 排序;反过来用 b DESC 也行,倒着扫一遍就是。换成 a > 1 就不行了,因为 a 不是定值。索引我给的调整是 CREATE INDEX idx_b_a ON orders (b, a)。

助手把这个判断的关键点说明白了:a 是定值时,a = 1 在索引里圈成连续的一段,段内 b 天然有序;a 变成范围就不止一个 a 值,每个 a 段里的 b 都是从头开始的,全局看 b 不再有序,所以必须再排一次。这也是为什么「范围条件后面的列用不上有序性」这条规律成立。

题目六:一条没索引的 UPDATE 锁了多少行

500 万行的 orders 表,RR 级别,执行 UPDATE orders SET status = 2 WHERE phone = '13800000000',而 phone 这列没有索引。这条语句实际会锁住多少行?由此解释两个线上现象:为什么这个接口一跑,不相干的订单更新也开始排队?为什么把隔离级别降到 RC 会好一些?

我答的是会锁非常多行、甚至接近全表,因为没索引只能全表扫描;不相干的订单开始排队,是因为这条 UPDATE 扫了很多行也锁了很多行,其中就包含别的订单的记录;RC 会好一些,是因为条件锁的范围更小。

助手把这句话坐实了:RR 下扫描过的记录会一直锁到事务结束,RC 下不匹配的行在 server 层过滤完就能把锁放掉,另外 RC 没有间隙锁。所以同样是全表扫,RC 锁住的时间短、范围也小,但根上的解法还是给 phone 加索引,让锁精确落在那一行上。

题目七:从 keep-alive 到 HTTP/3

HTTP/1.1 的 keep-alive 长连接解决了什么问题?连接复用之后还剩什么毛病?HTTP/2 用什么办法治了这个问题?那它自己又留下了什么坑,导致 HTTP/3 把底层的 TCP 换成 UDP?

我答的是:keep-alive 不用每个请求都重新建 TCP 连接,复用能省掉握手和慢启动的开销;剩下的是同一条连接上同一时刻只能跑一个请求。HTTP/2 是在一条 TCP 连接上跑多个流,每个请求响应是一个 stream,带 stream id,可以交错发送、乱序到达再按 stream 重组。它留下的坑是 TCP 层的队头阻塞,一个包丢了后面的全堵住。

助手补了一个常被追问的差别:HTTP/2 治的是应用层的队头阻塞,TCP 层的那个它治不了,因为底层还是「一条有序字节流」;HTTP/3 换到 QUIC 上,把流的概念搬到了传输层,一个流丢包不影响别的流。

← 返回文章列表