面灵AI→

靖安科技二面:60 问从 Nacos 追到缓存击穿与 TCP/UDP

轮次
二面
时间
2026-09
来源
牛客网

《面试题目》

  1. 请做自我介绍。
  2. 有没有了解过我们公司是做什么的?
  3. 有了解过我们有哪些产品吗?
  4. 有没有自己来看过我们具体做过哪些东西?
  5. 你在学校做的项目,是已经在校园里用起来了,还是自己做验证?是你独自完成的吗?
  6. 这个项目大概讲一下:项目背景、技术栈、设计了哪些模块。
  7. 订单模块、鉴权模块之间是怎么通信的?
  8. 服务之间通信的配置都是写死的吗?
  9. Nacos 是用来做什么的?
  10. 客户端是怎么注册到 Nacos 上去的?服务是怎么被发现的?
  11. 订单模块有两个实例,是怎么做到负载均衡的?
  12. 轮询是哪里做的?
  13. 服务发现和负载均衡,整条链路是怎么设计的?
  14. 负载均衡的策略都有哪些?
  15. 你引用的 MQ 中间件是用来解决什么问题的?
  16. 监听 binlog 实现同步,是做什么、解决什么问题?
  17. 你在实习的那家公司做的是什么业务?
  18. 这个业务是给谁用的?
  19. 接口性能优化是怎么优化的?怎么发现的问题、怎么排查的?
  20. 大量请求进来、Redis 没命中,请求全打到数据库怎么办?
  21. 同一个订单查询没命中,后续十个请求进来,第一个去查库了,剩下九个处于什么状态?
  22. 剩下的请求怎么同步拿到结果?
  23. 乐观锁和悲观锁的区别?
  24. 可重入锁是什么?
  25. 有没有实现过分布式锁?
  26. 实现一个函数:并发上传一个超大文件(1G),上传完同步响应结果,怎么设计?
  27. 开多少线程?
  28. 为什么是这个数字?
  29. 线程数是怎么算出来的?
  30. 为什么 IO 密集乘 2、CPU 密集加 1?有没有研究过为什么这么设计?
  31. 实习项目里 10 万级无人机,上报频率有多高?
  32. 这些无人机是哪里来的?
  33. 上报走的什么协议?
  34. 用 WebSocket + Protobuf 改造做了什么?
  35. 后端有多少台?什么规格?
  36. 10 万级无人机的虚拟线程是在服务器还是本地发起的?
  37. 上报请求的 QPS 是多少?
  38. 说 10 万级,测试才测到几千,这个指标是怎么得出来的?
  39. 前端是你实现的吗?
  40. 前端有什么了解?熟悉的前端技术/语言是什么?
  41. Vue 有什么特性?
  42. 虚拟 DOM 是什么?
  43. 纯 Java 实现一个缓存(不用中间件),怎么做?
  44. 什么是 LRU 算法?怎么实现?
  45. 服务器上搭建了一个服务,我访问不到,怎么排查?
  46. 不借助工具,我是非技术人员的客户,怎么配合排查?
  47. 请求结果是超时,怎么排查?
  48. 不借助 postman,一个 TCP 端口(非 HTTP,就是 TCP 监听程序),怎么查连通性?
  49. TCP 和 UDP 有什么区别?
  50. UDP 会丢包、乱序,为什么视频通话没出现这种情况?
  51. 重连是什么意思?UDP 不是没连接吗?
  52. 视频通话的网络通信是怎么设计的?
  53. WebSocket 了解吗?基于 TCP 还是 UDP?
  54. 服务器监听一个 UDP 的 8080 端口,还能不能再监听一个 TCP 的 8080?
  55. 服务器启动了一个 UDP 端口,怎么测试本地和服务器之间的连通性?
  56. TCP 监听程序(非 HTTP),怎么判定本地和服务器之间这个端口是连通的?
  57. 你是哪里人?现在在哪里?学校还有课吗?
  58. 你比较大的优势是什么?
  59. 还需要进一步提升的是什么?
  60. 三年以后想达到一个什么样的目标/状态?

《参考解析》

服务注册发现与负载均衡的整条链路

注册中心干两件事:服务注册与健康检查(Nacos 的临时实例靠客户端心跳或 2.x 的长连接续约),以及服务发现(把实例列表推给订阅方)。服务端启动时把自己的 ip:port 与元数据写进 Nacos;消费端通过 SDK 订阅服务列表,Nacos 推送变更、客户端在本地缓存一份实例列表——这也是 Nacos 短暂不可用时已有调用还能继续的原因。

关键在于「轮询是哪里做的」:不是 Nacos 做的。Nacos 只负责给出可用实例列表,真正选谁发生在消费端(客户端侧负载均衡)——Spring Cloud LoadBalancer 从本地列表里按策略挑一个实例再发起调用,Ribbon 时代的轮询、随机、权重、最少活跃连接等策略都在这一层生效。所以完整链路是「注册 → 订阅/推送 → 本地缓存 → 负载均衡器选实例 → 发起调用」,最后再用重试与熔断兜住选中的实例恰好挂掉的情况。

Redis 未命中:缓存击穿与请求合并

热点 key 失效的瞬间,大量并发会同时打到数据库,这是缓存击穿。三种主流解法:① 互斥重建(分布式锁或进程内 singleflight):只放一个请求去查库,其余等待,拿到结果后共享——注意锁粒度按 key、加超时,否则持锁者挂掉会全部饿死;② 逻辑过期:value 里带过期时间,读到过期不删 key,而是异步重建、其余请求先返回旧值,用一致性换吞吐;③ 空值缓存或布隆过滤器拦住不存在的 key,热点数据提前预热并且不设过期。

「剩下九个处于什么状态」就是追问你到底有没有想清楚等待这一步:要么阻塞在分布式锁上(要警惕连接池被占满,需要限制等待超时与并发量),要么用进程内 singleflight 把同进程请求合并成一个,要么直接返回旧值或降级。答的时候把「等」与「不等」各自的代价讲明白,比背方案名字有效。

乐观锁、悲观锁、可重入锁与分布式锁

乐观锁假设冲突少,靠版本号或 CAS 在提交时校验,冲突就重试,适合读多写少(update ... where version = ?);悲观锁假设冲突多,先 select ... for update 拿锁再操作,一致性强但并发度低、容易锁等待甚至死锁。可重入锁指同一线程能重复获取同一把锁,ReentrantLock 靠 AQS 的 state 计数加持有者标记实现,意义是自调用时不会把自己锁死,也方便把加锁逻辑拆进多个方法。

分布式锁要讲实现细节:Redis 用 SET key value NX PX 加锁,value 放唯一标识以便安全释放(Lua 里比对再删),配上看门狗续期;单实例的可用性问题是 RedLock 争议的来源,对一致性要求高时可以用 ZooKeeper/etcd 的临时顺序节点。数据库唯一索引也能当分布式锁,但性能和可用性取舍要能说清。

大文件并发上传的线程数怎么估

上传是 IO 密集型:线程大部分时间在等网络和磁盘,所以线程数可以显著大于核数。工程上的起点是「核数 ×(1 + 等待时间/计算时间)」,很多团队直接取核数的 2 倍作为默认值——「IO 密集乘 2」就是这么来的经验表达;CPU 密集加 1 则是「尽量贴近核数、减少上下文切换」的意思。两者都是起点不是定理。

真正的上界由下游决定:连接池大小、出口带宽、对象存储的分片限流、每个线程的缓冲区内存乘线程数,任何一项都会先撞墙,所以先用固定大小分片(几 MB 一块)、再用线程池并发传,比一味加线程有效。另外现代做法不一定用线程,NIO/异步 IO 加信号量控制并发度更省资源;完整链路还要有服务端分片合并、分片校验(ETag/CRC)、失败重试与断点续传。

MQ 与 binlog 同步分别解决什么

MQ 的作用是解耦、削峰、异步化:把同步链路上的非核心动作(发通知、写日志、更新索引)挪出去,主链路只保证核心事务。监听 binlog 做同步解决的是另一个问题——业务代码里双写数据库和下游容易不一致,改成业务只写库,下游解析 binlog 拿到变更事件,天然不漏、不侵入代码,还能按位点续传与重放。

代价要一起说:链路变长、延迟增加,事件可能重复投递所以消费端必须幂等(按主键加版本号判断),大事务与 DDL 会引起解析抖动。被追问「那为什么不直接用 MQ 发」时的答案就是:双写的一致性与事务边界问题。

10 万级设备上报:指标要经得起追问

这类追问考的是「这个数字是怎么来的」。可复现的口径是「设备数 × 单机上报频率 = 理论峰值 QPS」,压测值如果明显低,就要能解释差距在哪:压测机或出口带宽先到瓶颈、只压了部分链路、还是生产上并非所有设备同时在线。稳妥的答辩结构是「计算口径 + 实测数据 + 差距原因」三段;拿不出实测就明确讲这是设计容量而不是实测值,不要把压测数字说成生产数字。

协议侧 WebSocket 加 Protobuf 的收益要能讲清:长连接省掉反复握手与头部开销,二进制编码比 JSON 体积小、解析快,适合高频小报文;代价是服务端要维护海量连接与会话状态,需要心跳、重连退避、粘性路由或把连接归属存 Redis。虚拟线程的价值在于让「一连接一线程」这种阻塞模型变得便宜,它降低的是线程成本,不能解决连接数与下游写入能力。

TCP/UDP、端口复用与连通性排查

TCP 面向连接、可靠有序,有流量控制与拥塞控制,代价是握手、重传与队头阻塞;UDP 无连接、不保证送达与顺序,开销小、延迟低。视频通话并不是「不丢包」,而是应用层做了补偿:RTP/WebRTC 打时间戳与序列号,接收端用抖动缓冲平滑乱序,丢包靠 FEC 前向纠错、NACK 重传或让解码器做错误隐藏,音频还有丢包隐藏——业务对丢包容错,所以体感上「没出现这种情况」。

端口复用:TCP 与 UDP 是两套独立的协议栈命名空间,同一个 8080 可以分别被一个 TCP 监听和一个 UDP 监听占用,互不冲突(ss -tlnp 与 ss -ulnp 分开看)。连通性排查:HTTP 用 curl;非 HTTP 的 TCP 端口用 nc -vz host port、telnet 或 /dev/tcp 重定向;UDP 没有连接,只能发探测包看有没有回包,没回包不能判定不通,更可靠的是让服务端暴露回显或健康检查。排查顺序一般是本机进程与监听、防火墙与安全组、网络可达、端口可达,最后才看应用日志与超时配置。

纯 Java 手写 LRU 缓存

O(1) 的 LRU 是哈希表加双向链表:HashMap 存 key 到节点,链表头是最近使用、尾部是最久未使用;get 命中后把节点摘下来插到头部,put 时 key 存在就更新并前置,不存在就新建插头,超出容量就删尾节点并同步删 map。手写时用带哨兵头尾的链表能省掉大量边界判断。

并发场景下最省事的是 LinkedHashMap 的 accessOrder=true 加 removeEldestEntry,但它非线程安全,要么包一层同步、要么用 ConcurrentHashMap 自己维护链表。还要能说出 LRU 的局限:一次全表扫描式的偶发访问会把真正的热点挤出去,所以生产缓存常用 LRU-K、LFU 或分段 LRU。这题常被当作「数据结构基本功 + 知不知道现成类」的区分题。