JavaSE
1.流式输出和非流式输出
对比点
流式输出
非流式输出
数据传输
边生产边传输
生成完后一次传输
响应延迟
首字节快,用户能尽快看到结果
必须等所有数据生成后才能看到
内存占用
占用更少内存(分段处理)
可能占用大量内存(一次性加载)
实现复杂度
较高(需要支持分段协议/推送机制)
较低(一次性返回)
应用场景
视频流、日志实时消费、AI Chat逐字打印
小文件下载、查询一次性返回结果
非流式输出是等数据全部生成后一次性返回,而流式输出则是边生成边返回,能降低延迟和内存占用,更适合大数据量和实时场景。
2.HashMap remove 方法的实现细节
首先,remove(key)方法会计算key的hash值。
根据hash值定位到它在底层table数组中的索引位置(即bucket)。
如果该bucket为空,直接返回null。
如果bucket不为空,则遍历该位置的链表或红黑树,逐个节点使用hash值和equals()方法进行比较,直到找到要删除的目标节点。如果遍历完没找到,也返回null。
如果当前是链表结构,是头节点的话,即让 ...
JVM
1.那有哪些对象是可以直接在栈上分配呢?
在Java中,并不是特定类型的对象能够直接在栈上分配,而是取决于该对象的作用域。JVM通过一种叫做**“逃逸分析”(Escape Analysis)**的技术来判断一个对象是否可以安全地在栈上分配。
如果一个对象的引用没有“逃逸”出它被创建的方法之外,那么它就可能被优化为在栈上分配。这样做的好处是,当方法执行结束时,栈帧被弹出,对象的内存会立即被回收,无需等待垃圾回收(GC),从而提高性能。
逃逸分析是JIT(即时编译器)的一项优化技术,默认在现代JVM中是开启的。只有那些生命周期完全局限于单个方法调用内、体积较小且线程安全的对象,才最有可能被优化到栈上进行分配。
未逃逸的定义:
仅在方法内部使用:对象的引用完全封装在方法体内,没有被方法返回。
未赋值给外部变量:没有将该对象的引用赋值给任何类变量(static字段)或实例变量。
未传递给可能逃逸的方法:没有将该对象的引用作为参数传递给其他方法,或者传递给了但能确定其他方法也不会让它“逃逸”。
逃逸的例子:
比如对象作为方法的返回值,他就是逃离了这个方法的作用域
对象引用赋值给实 ...
Mybatis
1.UserMappe这个类为啥要是接口呢?
MyBatis的Mapper之所以必须定义为接口,其根本原因在于MyBatis框架在底层使用了**JDK动态代理(JDK Dynamic Proxy)**技术,来为我们自动地生成这个接口的实现类。
只定义了UserMapper接口,并在XML文件中写了SQL,但我们从来没有手动编写过一个class UserMapperImpl implements UserMapper。然而,在Service层,我们却可以直接@Autowired注入一个UserMapper的实例并调用它的方法。
启动时扫描:当Spring容器启动时,MyBatis的MapperScannerConfigurer会扫描指定的包路径(如com.example.mapper),找到所有被@Mapper注解标记的接口,或者所有继承了特定标记接口的接口。
注册Bean定义:对于找到的每一个Mapper接口(比如UserMapper.class),MyBatis并不会去创建一个真实的实现类,而是在Spring容器中注册一个特殊类型的Bean定义——MapperFac ...
计网
1.对比一下 HTTP/1.0, HTTP/1.1, 和 HTTP/2.0 这三个版本的主要区别。
请从连接管理、性能优化、头部处理等角度展开,并说明每一个版本的演进分别解决了上一代的什么核心痛点?
1.0->1.1
长链接 (Keep-Alive): 是 HTTP/1.1 相对于 HTTP/1.0 最核心的改进之一。HTTP/1.0 默认是短连接,每个请求/响应对都需要一次 TCP 连接。而 HTTP/1.1 默认开启了长链接,允许在一个 TCP 连接上发送多个 HTTP 请求,极大地减少了 TCP 连接建立和关闭的开销。
HTTP/1.1 还引入了管道机制 (Pipelining),允许客户端在收到上一个响应之前就发送下一个请求。但这只是部分解决了队头阻塞(Head-of-Line Blocking)问题,因为服务端的响应仍然必须按顺序返回。
2.0
多路复用 (Multiplexing): 这是 HTTP/2.0 最核心的优势。它允许在一个 TCP 连接上,同时、并行地收发多个请求和响应,并且不按顺序。这彻底解决了 HTTP/1.1 的队头阻塞问题。
头部 ...
MQ
1.消息队列(MQ)消息积压处理
当被问及线上Topic消息积压如何处理时,你的第一反应是“清空队列,然后恢复”,这在线上环境中是绝对禁止的操作。在引导下,你提到了扩容消费者。
方案1 紧急扩容消费者并监控下游依赖
监控分析:在扩容前,必须先快速查看消费者应用的CPU、内存、GC情况,以及其下游依赖(如数据库、外部API)的负载情况。确认瓶颈在于消费者本身,而不是下游。
水平扩容:如果瓶颈在消费者,立即增加消费者实例数量。在Kubernetes等云原生环境中,可以通过调整Deployment的replica数量快速实现。
注意Partition数量:确保消费者实例数不超过Topic的Partition数量,因为多余的消费者将处于空闲状态。
方案2 消息转储与异步回补
编写转储程序:快速开发一个简单的程序,它的唯一作用就是消费积压Topic中的消息,然后原封不动地存储到另一个临时Topic或一个临时存储(如文件、数据库)中。
启动转储:启动该程序,快速将积压消息“搬空”。
修复与回补:在修复了原始消费者的Bug或性能问题后,再编写一个回补程序,以一个受控的速率,从临时Topi ...
Redis
1.多级缓存数据一致性与失败回滚
当被问及如何保证Redis和本地缓存更新的原子性,以及在更新失败时如何回滚,你的回答提到了不甚准确的“编程式事务”,并最终倾向于人工处理。
方案1:引入消息队列(MQ)进行可靠的异步处理
修改架构:Canal不再直接调用消费逻辑,而是将解析后的binlog事件作为消息发送到MQ的一个Topic中。
消费者逻辑:消费者服务从MQ拉取消息。其处理逻辑是:先失效Redis缓存,再发布一个广播消息(如通过Redis Pub/Sub)通知所有应用实例失效本地Caffeine缓存。
失败处理:只有当所有步骤成功后,消费者才向MQ发送ACK。如果处理过程中任何一步失败(如Redis连接超时),消费者不发送ACK。MQ会在超时后将该消息重新投递给其他消费者,实现自动重试。
方案2 死信队列
在Canal的消费者逻辑中,使用Spring Retry等框架对缓存失效操作进行封装。
配置重试策略,例如重试3次,每次间隔采用指数退避(如1s, 2s, 4s),避免在故障期间频繁冲击下游服务。
配置一个RecoveryCallback。当所有重试都失败后,将这 ...
SpringCloud
Nacos
1.Nacos动态配置刷新的原理是什么?
核心机制: 长轮询(Long Polling)。
客户端行为: 应用启动后,客户端向Nacos Server请求配置,并建立一个长轮询连接,询问配置是否有更新。
服务端行为: 如果配置无变更,服务端会hold住请求30秒(默认);如果期间配置发生变更,立即响应;如果超时,也返回一个空响应。
刷新流程: 客户端收到变更响应后,拉取最新配置,发布EnvironmentChangeEvent事件。
Spring侧响应: @RefreshScope注解的Bean监听到事件后,会销毁并重新创建,从而加载到新配置。
2.Nacos 1.x 和 2.x 有什么核心区别?
通信模型升级: 最大的变化是从HTTP短连接轮询模型升级为gRPC长连接模型。
性能提升: gRPC基于HTTP/2,使用长连接和二进制协议,大大降低了通信开销和服务器压力,服务注册/发现和配置推送的性能提升了一个数量级。
推送机制: 从1.x的UDP推送通知+HTTP拉取数据,变为2.x的gRPC直接推送数据,实时性更强,更可靠。
架构演进: 2.x引入 ...
设计模式
1.当被问及如何在多个接口中统一管理以避免代码重复时
你的初步想法是提取一个公共方法。面试官进一步引导你思考过滤器和拦截器。
方案1:使用Spring MVC的HandlerInterceptor(拦截器)
HandlerInterceptor是Spring MVC提供的AOP实现,专门用于在Controller方法执行前后进行预处理和后处理。它与请求生命周期紧密耦合,是处理用户认证、日志记录、上下文设置等横切关注点的标准方式。
创建一个类实现HandlerInterceptor接口。
在preHandle方法中,从请求(如Header)中获取Token,解析出用户信息,然后调用工具类的set()方法将用户信息存入ThreadLocal。
在afterCompletion方法中,无论Controller方法执行成功还是失败,都调用工具类的remove()方法清理ThreadLocal,通常放在finally块中以确保执行。
创建一个配置类实现WebMvcConfigurer,重写addInterceptors方法,将你的拦截器注册到Spring容器中,并配置其拦截路径(如/ ...
Spring框架
1.SpringBoot的配置加载优先级
首先我们先确定一下配置加载优先级是按照我以下的顺序,由高到低的。分别是:
先是命令行参数(--server.port=9000 或 java -jar app.jar --spring.config.location=...)
然后是我们的系统的环境变量和JVM系统属性,比如设置端口为8080,比如我们在这里设置API的KEY
然后**RandomValuePropertySource**(random.* 占位符,用于生成随机数/字符串,可在配置中引用)
接着是外部配置文件(properties / yml)
JAR 包外部的 ./config/
JAR 包外部的 ./
JAR 包内部的 classpath:/config/
JAR 包内部的 classpath:/
接着是我们@PropertySource注解指定的配置
最后是我们Springboot默认的配置
然后在配置文件中,properties的配置大于yml,因为springboot是按加载顺序来的,后加载的prope ...
Mysql
1.多表join的时候,小表驱动大表
在Mysql的 Nested Loop Join 中
驱动表(outer table):首先被扫描的表。
被驱动表(inner table):对驱动表每一行,根据 Join 条件去查找匹配行的表。
核心原则:过滤后剩余行数少的表,应该作为驱动表,这样可以减少被驱动表的访问次数。这就是小表
执行过程:
扫描驱动表(全表扫描或索引扫描)。
对驱动表的每一行,根据连接条件在被驱动表中查找(通常用索引 B+Tree 查找)。
如果被驱动表使用二级索引且需要回表,则访问主键索引。
小表驱动大表,大表负责命中索引。
比如
1select * from A straight_join B on A.a = B.a;
数据库会全表扫A,然后每拿到一行就去比较条件 A.a=B.a,去B表里面查,B表命中索引的查询。实际上就是一个搜索树,查询的时间复杂度近似log2^B^,然后加上一次回表,可能就是2Log2 ^B^,所以总体的时间复杂度为A+2log2^B^*A,如果是覆盖索引的话,复杂度可降为 O(A + log₂(B) × A)
所以我的们A越小越好 ...













