面灵AI→

用友全栈开发高潜面经:从半同步复制到并发渲染

时间
2026-09
来源
牛客网

《面试题目》

  1. MySQL 已开启半同步复制,用户提交修改后立即查询从库,为什么仍可能读到旧数据?
  2. React 页面中的防抖搜索为什么会同时出现旧状态读取和旧响应覆盖新结果?两类问题分别如何解决?
  3. ReentrantLock 的条件等待为什么必须写在循环中?收到 signal() 后线程为什么不能立即继续执行?
  4. 将 Snowflake 替换为 UUIDv7,能否同时解决数据库写入局部性、严格顺序和业务信息泄漏问题?
  5. React 并发渲染时,直接读取外部可变对象为什么可能产生界面撕裂?useSyncExternalStore 对快照有什么要求?
  6. Java 接口返回 long 类型主键,前端再原样提交,为什么可能操作到错误记录?JSON 序列化还能破坏哪些语义?
  7. 两名审批人同时退出值班,数据库使用快照隔离,为什么仍可能违反「至少保留一人值班」的规则?
  8. 一个 BFF 请求需要聚合六个下游接口,为什么把所有调用并行化仍然可能放大尾延迟?
  9. 没想过考研吗?

《参考解析》

半同步复制仍读到旧数据:半同步只保证「至少一个从库收到并写入 relay log 后主库才返回」,它保证的是数据不丢,不是从库已经重放完成。从库的 SQL 线程是异步回放的,所以紧接着在从库上查还是旧值。要读到自己刚写的,需要走主库(读写分离按业务分流)、用 WAIT_FOR_EXECUTED_GTID_SET / MASTER_POS_WAIT 显式等待重放,或者接受最终一致并在产品层把「提交后立即回显」改成用本地状态渲染。

防抖搜索的两类竞态:一是旧状态读取——闭包里捕获的是创建时的 state,等定时器触发时 state 已经变了;解法是用函数式更新(setX(prev => ...))或把最新值放进 ref。二是旧响应覆盖新结果——先发的请求后返回,把新结果盖掉了;解法是给每次请求打递增序号或 AbortController,回来时校验是不是最新一次,不是就丢弃。这在搜索类输入框上是必现问题,尤其后端响应时间不稳定时。

ReentrantLock 条件等待必须写在循环里:await() 会把线程放进条件队列、释放锁,被 signal() 唤醒后只是从条件队列转移到同步队列,重新竞争锁之后才真正继续。signal() 不替调用者释放锁,也不保证等待者立即运行。等线程拿到锁继续执行时,目标条件可能已经被其他线程改变了,另外还有虚假唤醒。所以规范写法是 while (!condition) { cond.await(); }——条件判断、状态修改和通知都必须在同一把锁的保护下;用 if 代替 while,就可能在线程被唤醒后发现条件已经不成立却继续往下执行。

Snowflake 换 UUIDv7:不能同时解决三个问题。UUIDv7 把毫秒时间戳放高位,比纯随机 UUID 更有利于 B-tree 插入局部性,但同一毫秒内的顺序取决于实现,不天然满足跨节点严格递增,唯一性依赖随机位。时间字段还会暴露大致创建时间,不能承担访问控制职责。选型要一起考虑 128 位索引的空间成本、时钟回拨处理和数据库存储方式;业务若需要严格顺序,应另设权威序列,而不是从标识符里推导。

并发渲染与 useSyncExternalStore:并发渲染可能在一次更新中途暂停,而外部可变对象此时已经变了,不同组件就读到了不同版本的数据,界面上表现为撕裂。useSyncExternalStore 为外部状态提供「订阅 + 快照读取」协议,React 据此检查一致性:getSnapshot 在数据未变化时必须返回稳定结果,不能每次 new 一个新对象(否则会触发无限重渲染);变化时必须反映新快照,不能原地改对象后继续返回同一引用。SSR 还需要匹配的 getServerSnapshot,保证服务端输出与客户端 hydration 的初始视图一致。

long 主键经 JSON 的精度丢失:JavaScript 的 Number 是 IEEE 754 双精度,安全整数上限是 2^53 - 1,雪花 ID 早就越界了,JSON.parse 阶段就已经丢精度,之后再转字符串也救不回来。正确做法是在接口契约里就把主键定义成十进制字符串,并贯穿查询参数、状态管理和缓存键。JSON 还会破坏其他语义:无法原生表达对象引用和循环结构、Map/Set 会变成 {}、日期变成字符串、NaN 序列化成 null、直接序列化 BigInt 会抛错——所以序列化往返不是通用深拷贝,更不是无损的跨语言类型协议。

快照隔离下的写偏差:两个事务各自从自己的快照里看到两人都在岗,然后分别更新不同的记录——因为没有修改同一行,普通的写写冲突检测放行,两个事务都提交,最后没人值班。这是经典的写偏差(write skew),它不是靠某个「隔离级别名字」解决的。对策是让不变量对应一个共同的并发控制点:先锁住值班组记录再检查和更新成员状态(物化冲突),或者用真正提供可串行化保障的隔离级别并处理事务中止重试。也不要假定「可重复读」在所有数据库里都能防住它。

并行聚合为何放大尾延迟:如果响应必须等全部结果,整体耗时取决于最慢的分支;分支越多,碰到慢请求(GC、热 key、网络抖动)的概率越高,P99 尾延迟被放大得越明显。解法不是”并行就行”,而是:给每个分支设独立超时并做降级,让非关键分支可以缺失;用 hedged request(慢到阈值就发第二个副本,取先返回的);把可缓存的调用提前预热;对关键路径做限流与隔离(每个下游独立线程池/连接池),避免一个慢下游把公共资源占满。