靖安科技二面:60 问从 Nacos 追到缓存击穿与 TCP/UDP
- 轮次
- 二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 请做自我介绍。
- 有没有了解过我们公司是做什么的?
- 有了解过我们有哪些产品吗?
- 有没有自己来看过我们具体做过哪些东西?
- 你在学校做的项目,是已经在校园里用起来了,还是自己做验证?是你独自完成的吗?
- 这个项目大概讲一下:项目背景、技术栈、设计了哪些模块。
- 订单模块、鉴权模块之间是怎么通信的?
- 服务之间通信的配置都是写死的吗?
- Nacos 是用来做什么的?
- 客户端是怎么注册到 Nacos 上去的?服务是怎么被发现的?
- 订单模块有两个实例,是怎么做到负载均衡的?
- 轮询是哪里做的?
- 服务发现和负载均衡,整条链路是怎么设计的?
- 负载均衡的策略都有哪些?
- 你引用的 MQ 中间件是用来解决什么问题的?
- 监听 binlog 实现同步,是做什么、解决什么问题?
- 你在实习的那家公司做的是什么业务?
- 这个业务是给谁用的?
- 接口性能优化是怎么优化的?怎么发现的问题、怎么排查的?
- 大量请求进来、Redis 没命中,请求全打到数据库怎么办?
- 同一个订单查询没命中,后续十个请求进来,第一个去查库了,剩下九个处于什么状态?
- 剩下的请求怎么同步拿到结果?
- 乐观锁和悲观锁的区别?
- 可重入锁是什么?
- 有没有实现过分布式锁?
- 实现一个函数:并发上传一个超大文件(1G),上传完同步响应结果,怎么设计?
- 开多少线程?
- 为什么是这个数字?
- 线程数是怎么算出来的?
- 为什么 IO 密集乘 2、CPU 密集加 1?有没有研究过为什么这么设计?
- 实习项目里 10 万级无人机,上报频率有多高?
- 这些无人机是哪里来的?
- 上报走的什么协议?
- 用 WebSocket + Protobuf 改造做了什么?
- 后端有多少台?什么规格?
- 10 万级无人机的虚拟线程是在服务器还是本地发起的?
- 上报请求的 QPS 是多少?
- 说 10 万级,测试才测到几千,这个指标是怎么得出来的?
- 前端是你实现的吗?
- 前端有什么了解?熟悉的前端技术/语言是什么?
- Vue 有什么特性?
- 虚拟 DOM 是什么?
- 纯 Java 实现一个缓存(不用中间件),怎么做?
- 什么是 LRU 算法?怎么实现?
- 服务器上搭建了一个服务,我访问不到,怎么排查?
- 不借助工具,我是非技术人员的客户,怎么配合排查?
- 请求结果是超时,怎么排查?
- 不借助 postman,一个 TCP 端口(非 HTTP,就是 TCP 监听程序),怎么查连通性?
- TCP 和 UDP 有什么区别?
- UDP 会丢包、乱序,为什么视频通话没出现这种情况?
- 重连是什么意思?UDP 不是没连接吗?
- 视频通话的网络通信是怎么设计的?
- WebSocket 了解吗?基于 TCP 还是 UDP?
- 服务器监听一个 UDP 的 8080 端口,还能不能再监听一个 TCP 的 8080?
- 服务器启动了一个 UDP 端口,怎么测试本地和服务器之间的连通性?
- TCP 监听程序(非 HTTP),怎么判定本地和服务器之间这个端口是连通的?
- 你是哪里人?现在在哪里?学校还有课吗?
- 你比较大的优势是什么?
- 还需要进一步提升的是什么?
- 三年以后想达到一个什么样的目标/状态?
《参考解析》
服务注册发现与负载均衡的整条链路
注册中心干两件事:服务注册与健康检查(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。这题常被当作「数据结构基本功 + 知不知道现成类」的区分题。