三轮技术练习:Future 与中断、组件内守卫、GROUP BY 与元数据锁

三轮技术练习:Future 与中断、组件内守卫、GROUP BY 与元数据锁

这一批练了三轮:Java 的 Future 与中断,前端的组件内守卫与移动端单位,数据库的分组能否借索引、长查询怎么卡住 DDL,以及一条空闲连接凭什么判断对端已经消失。上一批 Future.get() 只答到"异常被包装",这次从那里接着往下走。

Future 的取消、超时和原始异常

Future.get() 在任务内部抛异常时,抛出的是 ExecutionException,原始异常挂在它的 cause 上,用 getCause() 取。任务被取消之后 get() 抛 CancellationException,这个不是"因为它是运行时异常",而是 Future 的状态已经是 cancelled,get() 按状态直接抛,根本不会去读任务结果。

get(3, TimeUnit.SECONDS) 超时抛 TimeoutException。超时只是当前线程不再等了,任务本身照旧在跑,它不会替调用方去中断工作线程。想让任务真的停下来,得调用 future.cancel(true),这一步才会给工作线程发中断信号,任务内部要配合中断标志才有机会退出。

synchronized 锁的是哪个对象

写在实例方法上,锁的是当前实例 this;写在静态方法上,锁的是这个类的 Class 对象,比如 Box.class;写在代码块里,锁的是括号里那个对象,synchronized (this) 和实例方法等价。

因为锁的对象不同,一个实例方法和一个静态方法可以同时执行,互不阻塞。同理,synchronized (new Object()) 每次执行到这一行都新建一个对象,每个线程锁各自的锁,等于没有互斥。静态同步方法的影响面更大:同一个类的所有实例在 Class 对象这把锁上互斥。

interrupt 是标志位,不是杀线程

Thread.interrupt() 不强制终止线程,它设置的是中断标志。线程正在 sleep、wait、join 时,会立刻以 InterruptedException 提前返回,不会等满时间。线程如果在跑普通计算,标志只是被置上,需要自己在循环里检查。

两个查询方法容易记混:isInterrupted() 是实例方法,读取目标线程的标志,不清除;Thread.interrupted() 是静态方法,只能作用于当前线程,读取并清除标志。

InterruptedException 抛出时,JVM 已经把中断标志清掉了。所以在 catch 里要么继续往上抛,要么重新调用 interrupt() 把标志补回去,否则上层不知道有人叫停过,线程会被当成正常结束。

组件内的三个守卫

beforeRouteEnter 在进入该组件对应路由之前触发,这时组件实例还没创建,拿不到 this。老写法用 next(vm => { ... }) 在导航确认之后拿到实例。beforeRouteUpdate 在当前路由变化但组件被复用时触发,典型场景是 /user/1 跳到 /user/2,同一个 User 组件复用,不会重新创建。beforeRouteLeave 在离开前触发,实例还在,this 可以用。

组合式 API 里只有 onBeforeRouteLeave 和 onBeforeRouteUpdate,没有对应 beforeRouteEnter 的版本。原因是这些守卫要在 setup 里注册,而 setup 本身是组件创建时才执行的,不存在"组件还不存在"这个注册时机。

守卫的执行顺序

一次完整导航的顺序是:beforeRouteLeave(离开的组件)→ beforeEach(全局前置)→ beforeRouteUpdate(复用时)→ beforeEnter(路由独享)→ beforeRouteEnter → beforeResolve(全局解析)→ afterEach(导航确认)→ DOM 更新 → beforeRouteEnter 里 next(vm => ...) 的回调。

next(false) 取消这次导航,停在原页面,URL 也不变;next('/login') 中断当前导航并跳去登录页。典型用途是表单没保存时弹确认,以及离开时清理定时器、取消未完成的请求。

类数组转数组,以及 Array.from 与展开的差别

arguments 不是 Array 的实例,Array.isArray(arguments) 是 false,原型链上没有 Array.prototype,所以没有 map、filter 这些方法。转成数组可以用展开运算符、Array.from,或者最笨的循环逐个读取。

两者的差别在"接受什么"。展开要求对象实现了 Symbol.iterator,arguments 和 NodeList 现在都有,所以能展开;一个只有 length 和下标、没有迭代器的普通对象,[...obj] 会直接抛 TypeError: obj is not iterable。Array.from 更宽容,类数组对象只要有 length 就能处理,第二个参数是映射函数,作用类似 map 的回调。

移动端:断点顺序与 rem、vw

min-width 表示视口至少这么宽时生效,max-width 表示最多这么宽时生效。移动优先就是把不带媒体查询的样式写成小屏基础样式,再用 min-width 一段段往上加。两条规则如果同时命中,胜负取决于源码顺序,后写的覆盖前面的,所以顺序写反会失效;移动优先的写法天然让大屏规则排在后面,不容易翻车。

rem 相对根元素的 font-size,vw 是视口宽度的百分之一,两者都能做不同屏幕下的等比缩放。rem 方案通常要一段脚本按视口宽度动态设置 html 的字号,脚本执行前页面用的是默认字号,首屏会跳一下,而且改根字号会影响用户在浏览器里自己调的字号。vw 方案是纯 CSS,但 1px 边框在缩放后可能被算成小数像素,手机地址栏收起时 100vh 的变化也要单独处理。

GROUP BY 能不能借索引

SELECT user_id, COUNT(*) FROM orders GROUP BY user_id,如果表上有 idx_user_status (user_id, status),分组可以顺着索引的有序性走,不必另建临时表再排序,分组的本质是把相同的值凑到一起,而索引里的行已经按 user_id 排好了。

换成 GROUP BY status 就不行了。status 是联合索引的第二列,它只在同一个 user_id 内部有序,跨 user_id 就乱了,存储顺序不再满足分组要求,MySQL 需要建临时表并重新排序。这就是最左前缀原则在分组场景下的具体表现。

EXPLAIN 的 Extra 列里,Using temporary 表示建了临时表,Using filesort 表示额外排了一次序,两个可以同时出现。分组能借索引时,可能看到 Using index for group-by。

一条长 SELECT 怎么把 ALTER 卡住

会话 A 开事务跑了一条普通 SELECT 之后不提交,会话 B 执行 ALTER TABLE。B 会一直等,等到超时。它等的是元数据锁(MDL),不是行锁:A 的 SELECT 持有这张表的 MDL 共享读锁,ALTER 需要 MDL 排他锁,两者互斥。

关键是这把锁什么时候放。MDL 不是语句执行完就释放,而是在事务结束(提交或回滚)时释放;A 没提交,锁就一直攥着。

更糟的是传染性。等锁的请求按先来后到排队,B 的排他请求一旦进了等待队列,后面所有新来的、只需要共享锁的普通 SELECT 也必须排在它后面。这是为了防止写请求被源源不断的读请求饿死,代价是一句挂住的 DDL 能把整张表的读请求一起堵住。

线上要避免这种事,提交 DDL 前先确认有没有长事务或长查询没结束,再把 lock_wait_timeout 调到一个合理值。这个参数默认 31536000 秒,等于一年,基本就是无限等。表结构变更又不想锁表时,常见做法是用 gh-ost、pt-online-schema-change 这类工具做影子表切换。

空闲的长连接凭什么知道对面没了

客户端进程被 kill -9,或者笔记本合盖断网,服务端的 netstat 里那条连接还是 ESTABLISHED。TCP 自带的保活机制默认要空闲 7200 秒(2 小时)才发第一个探测包,之后按 75 秒一次、连续 9 次无响应才判定断开,整条链路走完要两个多小时。

即便能等,它也未必有效。中间隔着 NAT 或防火墙时,映射表通常几分钟就老化,连接被中间设备悄悄丢掉,而探测包要到不了对端,服务端看到的是"发了没人回",不代表协议上真的收到了 RST。所以线上一般不指望 TCP keepalive。

应用层补这一刀的办法是心跳:按固定间隔主动发探测包并要求对端响应,间隔一般 30 秒左右,判定超时取间隔的 2 到 3 倍。心跳必须验证对端的回包,只发不验等于没做。清理不及时的后果是连接一直占着文件描述符、内存和连接表项,攒多了就是句柄耗尽、连接池被假活连接占满。

这一批里最值得记住的是 MDL 那条链:一条没提交的 SELECT,加上一句 ALTER,就能把整张表堵住。它不体现在慢查询日志里,而是体现在"突然所有查询都卡了"。

← 返回文章列表