第七节 面试问题-中级版

亮子 2025-09-06 11:32:45 2774 0 0 0

1、你都用过哪些设计模式或者了解哪些设计模式?

按三类来说:

创建型:单例用得最多,工厂模式在框架里随处可见,建造者适合构造复杂对象,比如 Lombok 的 @Builder。

结构型:代理模式——Spring AOP 的核心就是动态代理;适配器——比如 SpringMVC 的 HandlerAdapter;装饰器——不改变原类的基础上增强功能。

行为型:观察者模式——Spring 的事件监听机制就是;策略模式——不同算法可以动态切换,比如支付策略;模板方法——固定流程、可变步骤,JdbcTemplate 就是典型。

2、重写和重载有什么区别?

重写是父子类之间的事,方法签名完全一样,子类重新实现父类方法,用来实现多态。访问权限不能比父类更严格,返回值可以是父类返回值的子类(协变返回类型)。

重载是同一个类里,方法名一样但参数列表不同——个数、类型或顺序不同。返回值可以不同,但光靠返回值不同不算重载。

核心区分:重写看运行时的实际类型,重载看编译时的引用类型。

3、什么是深克隆和浅克隆?有什么区别?

浅克隆:只复制对象本身,基本类型字段直接复制值,引用类型字段复制的是引用地址——所以克隆对象和原对象的引用字段还是指向同一个对象。

深克隆:递归地把所有引用字段指向的对象也复制一遍,克隆对象和原对象完全独立,改一个不影响另一个。

实现方式的话,要么手动递归复制所有引用对象,要么用序列化反序列化,或者用第三方库。

4、Java 的引用类型有哪些?

四种,按强度递减:

强引用:我们天天用的 new 出来的对象,只要引用在就不会被 GC。

软引用(SoftReference):内存不够的时候才回收,适合做缓存。

弱引用(WeakReference):GC 时不管内存够不够都会回收,ThreadLocal 里的 Entry 的 key 就是弱引用。

虚引用(PhantomReference):最弱的,拿不到引用对象本身,只能配合 ReferenceQueue 用来跟踪对象什么时候被 GC,堆外内存管理会用。

5、说一下 JVM 中的垃圾收集器有哪些?

按发展代来分:

老年代收集器:Serial 单线程,Parallel 多线程注重吞吐,CMS 并发低延迟但会浮动垃圾和碎片问题,JDK14 移除了。

现代收集器:G1 从 JDK7 引入,JDK9 成为默认,把堆分成多个 Region,兼顾吞吐和延迟。ZGC 和 Shenandoah 是超低延迟的,JDK11 引入,现在 JDK21 已经很成熟了,停顿能做到几毫秒级别。

实际项目里 JDK8 默认是 Parallel,JDK9+ 默认 G1。大内存、低延迟场景考虑 ZGC。

6、如何破坏双亲委派机制?

重写 ClassLoader 的 loadClass() 方法就行。默认逻辑是先委托父加载器,我们可以改成自己先加载,加载不了再找父加载器。

最典型的例子:Tomcat 的 WebAppClassLoader。每个 Web 应用用自己的类加载器优先加载自己的类,这样才能隔离不同应用的同名类——不然两个应用各自的 User 类就冲突了。

JDBC 的 DriverManager 也是,通过线程上下文类加载器绕过了双亲委派。

7、如何判断对象是否可以被回收?

JVM 用的是可达性分析算法。从 GC Roots 出发,顺着引用链往下找,链上够得着的对象都是活的,够不着的就标记为可回收。

GC Roots 主要包含:虚拟机栈里引用到的对象、静态变量引用的对象、常量引用的对象、本地方法栈里引用的对象。

还有个引用计数法,循环引用解决不了,JVM 没采用。

8、synchronized 锁的升级机制是什么?

JDK6 以后做了锁升级优化,路径是:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,只能升级不能降级。

偏向锁:同一线程反复获取同一把锁,就偏给它,只在 Mark Word 里记个线程 ID,基本没开销。

轻量级锁:有另一个线程尝试获取偏向锁时升级过来,通过 CAS 自旋竞争,适合线程交替执行的场景。

重量级锁:自旋超过一定次数或者竞争太激烈,升级为重量级,底层依赖操作系统的 mutex,线程会阻塞。

9、数据库查询语句中 where 和 having 有什么区别?

执行时机不同:where 在分组前过滤行数据,having 在分组后过滤分组结果。

聚合支持不同:where 不能用聚合函数——你不能 where sum(age) > 100;having 专门配合聚合函数用,having sum(age) > 100 是合法的。

简单说:where 筛行,having 筛组。

10、左连接、右连接、内连接、全连接有什么区别?

内连接(INNER JOIN):两表都匹配才保留,取交集。

左连接(LEFT JOIN):左表全保留,右表匹配的连上,不匹配的字段填 NULL。

右连接(RIGHT JOIN):反过来,右表全保留。

全连接(FULL JOIN):两张表都全保留,不匹配的填 NULL,取的是并集。MySQL 原生不支持 FULL JOIN,需要用 LEFT JOIN UNION RIGHT JOIN 来模拟。

11、请说说事务中的 MVCC

MVCC 是多版本并发控制,核心思想是:每行数据保留多个版本,读操作不用加锁,直接读历史版本就行,读写互不阻塞。

实现上,InnoDB 的每行数据有两个隐藏列:trx_id(创建该版本的事务 ID)和 roll_pointer(指向 undo log 里的旧版本)。事务通过 ReadView 判断哪些版本对它可见——RC 级别每次查询生成新的 ReadView,RR 级别整个事务用同一个 ReadView,所以能重复读。

12、如果 Redis 服务崩溃了怎么办?

分层应对:

第一层,持久化恢复:如果开了 RDB 或 AOF,重启后会自动加载数据文件,数据能回来。

第二层,主从切换:如果配了主从 + 哨兵,哨兵会自动把从库提成主库,做到故障自动转移。

第三层,集群兜底:如果是 Cluster 模式,其他节点继续服务,故障节点的从节点会接管。

关键平时要做好持久化配置和主从/集群部署,别等崩了再想办法。

13、请说说 Redis 哨兵模式的机制

哨兵是 Redis 官方提供的高可用方案,一个独立的进程,核心做三件事:

监控:持续 ping 主从节点,检测健康状态。

自动故障转移:主库挂了,哨兵之间先选举出一个 Leader,然后由 Leader 从从库中选一个升级为主库,通知其他从库去复制新主库。

通知:故障转移完成后,通知客户端新的主库地址。

至少部署三个哨兵节点才能保证选举的可靠性。

14、说说 Redis 分布式集群是如何存储数据的

Redis Cluster 用的是哈希槽方案,总共 16384 个槽。每个 key 经过 CRC16 哈希后对 16384 取模,就确定归属哪个槽,槽再分配给各个节点。

客户端请求时,如果请求的节点不负责这个槽,节点会返回 MOVED 指令告诉客户端正确的节点地址,客户端重定向过去。这也是为什么批量操作如果跨了槽,在 Cluster 模式下会受限。

15、MyBatis 的一级缓存和二级缓存有什么区别,怎么开启?

一级缓存:默认开启,SqlSession 级别的,同一个 SqlSession 里重复查同一条 SQL 会命中。关不掉,因为它是 SqlSession 内部维护的。

二级缓存:需要手动开启,Mapper namespace 级别的,跨 SqlSession 共享。在 Mapper.xml 里加 标签就行,实体类要实现 Serializable。

实际开发里一级缓存默认用着就行,二级缓存要谨慎——因为别的 namespace 的更新可能导致缓存脏数据。

16、MyBatis 的 XML 文件中,#{} 和 ${} 的区别是什么?

#{} 是预编译占位符,MyBatis 会把 SQL 中的 #{} 替换成 ?,参数通过 PreparedStatement 设进去,自动加引号、能防 SQL 注入,安全。

${} 是直接字符串拼接,不预编译,就纯粹拼 SQL。有注入风险,但动态表名、动态排序字段这种场景必须用它,因为占位符不能替换表名。这种场景一定要自己对参数做白名单校验。

17、MyBatis 的动态标签有哪些?

常用的五个:

:条件判断,满足才拼 SQL。

:自动处理开头的 AND/OR,配合 用很常见。

:UPDATE 语句用,自动处理逗号。

:遍历集合,批量插入、IN 查询必用。

:多条件分支,相当于 switch-case。

18、说说类的反射机制

反射就是程序在运行时能拿到类的元数据——类名、字段、方法、构造器——并且可以动态操作它们。核心入口是 Class 对象,不管是通过 类名.class、对象.getClass(),还是 Class.forName() 拿到 Class 对象,后续操作都一样。

实际应用场景:框架底层大量用反射——Spring 的依赖注入、MyBatis 的结果映射,还有动态代理。缺点是性能有损耗、破坏封装性,所以框架里一般会做缓存。

19、获取 Class 对象的三种方式

第一种:类名.class,编译时就确定了,最直接。

第二种:对象.getClass(),已经有实例了就用这个。

第三种:Class.forName(“全限定类名”),运行时动态加载,会触发类的静态初始化。JDBC 加载驱动就是这种。

20、Java 创建对象的方式有哪些

四种:

new 关键字:最常用。

反射:Class.newInstance() 或者 Constructor.newInstance(),框架里大量用。

克隆:实现 Cloneable 接口,调用 clone(),浅拷贝。

反序列化:ObjectInputStream.readObject(),从流里恢复对象,不经过构造方法。

21、对于 IOC 你是怎么理解的

IOC 就是控制反转,把对象创建和依赖管理的控制权从业务代码交给容器。不用在自己代码里 new 对象,而是由 Spring 容器统一管理 Bean 的生命周期和依赖关系。

核心好处是解耦。传统方式——类 A 依赖类 B,A 得自己 new B,两者强耦合。用了 IOC 之后,A 只需要声明需要什么,Spring 帮你注入进来,A 根本不关心 B 怎么创建的。依赖注入是 IOC 的具体实现方式。

22、对于 AOP 你是怎么理解的

AOP 是面向切面编程,解决的是横切逻辑的问题。比如日志、事务、权限校验这些——它们在很多方法里都要用,跟核心业务无关但又不能少,散落得到处都是。

AOP 的思路就是把这些横切逻辑抽成一个切面,定义好在哪些方法(切点)的什么时机(通知)织入进去,核心业务代码完全不用改。

实际项目里,Spring 事务就是 AOP 的典型应用——加个 @Transactional,框架自动帮你开启、提交、回滚事务。

23、AOP 底层是如何实现的

动态代理。Spring 会根据目标类的情况选代理方式:

如果目标类实现了接口,用 JDK 动态代理,基于接口生成代理对象,通过反射调用目标方法。

如果目标类没实现接口,用 CGLIB,通过继承目标类生成子类代理,重写方法来做增强。

Spring Boot 2.x 之后默认就用 CGLIB 了,因为不需要强制基于接口,更灵活一些。

24、使用 AOP 技术需要用到哪些注解

核心的几个:

@Aspect:把类声明为切面类。

@Pointcut:定义切点表达式,指明增强哪些方法,比如 execution(* com.xxx.service.*.*(..))。

然后是五种通知类型的注解:

@Before:方法执行前。

@After:方法执行后,不管有没有异常。

@AfterReturning:正常返回后。

@AfterThrowing:抛异常后。

@Around:环绕,包裹整个方法,最灵活——可以控制是否执行目标方法,也能修改参数和返回值。

25、AOP 都有哪些通知方式

五种,按执行时机分:

前置通知(@Before):目标方法执行前。

后置通知(@After):目标方法执行后,相当于 finally,不管是否异常都会执行。

返回后通知(@AfterReturning):目标方法正常返回后,能拿到返回值。

异常通知(@AfterThrowing):目标方法抛异常后,能拿到异常信息。

环绕通知(@Around):最强大,方法执行前后都可以写逻辑,还能决定要不要执行目标方法。

26、AOP 通知的先后顺序是什么

同一方法被多个通知增强时,执行顺序是:

环绕通知的前半部分 → 前置通知 → 目标方法 → 返回后/异常通知 → 环绕通知的后半部分 → 后置通知。

不同切面之间,默认按切面类名的字母顺序排序,也可以用 @Order 注解指定优先级,数字越小越先执行。

27、Spring 对象常用的注入方式有哪些

三种:

构造器注入:通过构造方法注入依赖,Spring 官方推荐的方式。好处是依赖不可变、对象创建完就能用、方便单元测试 mock。

Setter 注入:通过 setter 方法注入,可选依赖适合用这种方式。

字段注入:直接在字段上加 @Autowired,用起来最方便,但不好测试,也不符合不可变原则。实际开发里用得最多但也最不推荐。

28、@Autowired 与 @Resource 的区别

来源不同:@Autowired 是 Spring 的,@Resource 是 JDK 标准 javax.annotation 包里的。

匹配逻辑不同:@Autowired 默认按类型(byType)注入,同类型有多个 Bean 时配合 @Qualifier 指定名称。@Resource 默认按名称(byName),找不到同名的再降级按类型。

作用位置不同:@Autowired 可以用在构造器、方法、字段上;@Resource 不支持构造器注入。

29、Spring Bean 的作用域有哪些

常用的五个:

singleton:默认,容器里只有一个实例,全局共享。

prototype:每次获取都创建新实例,适合有状态的 Bean。

request:Web 环境下,每个 HTTP 请求一个实例。

session:Web 环境下,每个会话一个实例。

application:Web 环境下,整个 ServletContext 一个实例。

实际开发 95% 的场景都是单例,prototype 用得很少——要注意 prototype Bean 的生命周期管理,Spring 只管创建不管销毁。

30、Bean 生命周期有哪些

整体分四步:实例化 → 属性注入 → 初始化 → 销毁。

展开说:

实例化——调用构造方法创建对象。

属性注入——@Autowired 这些依赖注入进来。

然后是一系列 Aware 回调:BeanNameAware → BeanFactoryAware → ApplicationContextAware。

接着是 BeanPostProcessor 的前置处理(postProcessBeforeInitialization)。

再是初始化阶段:@PostConstruct → InitializingBean 的 afterPropertiesSet() → 配置的 init-method。

BeanPostProcessor 的后置处理(postProcessAfterInitialization)。

这时候 Bean 就绪了,可以用了。

容器关闭时,调用 @PreDestroy → DisposableBean 的 destroy() → 配置的 destroy-method。

31、Spring Bean 生命周期有哪些

这道题和上一题是同一个问题,补充强调四个大阶段:实例化、属性填充、初始化、销毁。其中初始化阶段最复杂,包含了 Aware 回调、BeanPostProcessor 处理、自定义初始化方法等,面试时重点讲这个阶段就行。

32、Spring 如何解决 Bean 循环依赖问题

Spring 靠三级缓存来解决,但只限于单例 Bean 的 setter 注入——构造器注入的循环依赖解决不了,会直接抛 BeanCurrentlyInCreationException。

三级缓存分别存什么:

一级缓存 singletonObjects:存成品 Bean,完全初始化好的。

二级缓存 earlySingletonObjects:存提前暴露的半成品 Bean,创建了但属性还没注入完。

三级缓存 singletonFactories:存能生成 Bean 的工厂,可以提前创建代理对象。

流程:A 和 B 互相依赖时,A 先实例化,把自己提前暴露到三级缓存,然后去填充属性,发现需要 B。B 创建时发现需要 A,就从三级缓存拿到 A 的半成品引用,B 初始化完成。然后回到 A,此时 B 已经有了,A 完成初始化。一圈走下来,两个都好了。

33、SpringMVC 常用的注解有哪些

按使用场景说:

控制器层:@Controller、@RestController(= @Controller + @ResponseBody)。

请求映射:@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping。

参数绑定:@RequestParam(请求参数)、@PathVariable(路径变量)、@RequestBody(请求体 JSON)、@RequestHeader(请求头)。

其他:@ResponseBody、@ModelAttribute、@SessionAttributes、@CrossOrigin。

34、SpringMVC 的主要组件有哪些

核心七个组件,按请求流程串起来:

DispatcherServlet:前端控制器,所有请求入口。

HandlerMapping:根据请求 URL 找到对应的 Handler(Controller 方法)。

HandlerAdapter:真正去执行 Handler。

Handler(Controller):业务逻辑处理。

ModelAndView:封装处理结果和视图信息。

ViewResolver:把视图名解析成具体的 View 对象。

View:渲染页面,返回给客户端。

35、SpringMVC 的运行流程是什么

按请求流转说:

请求先到 DispatcherServlet。它调 HandlerMapping 找到对应的 Handler,然后找能执行这个 Handler 的 HandlerAdapter,由 Adapter 调 Handler 方法。方法执行完返回 ModelAndView。DispatcherServlet 拿到 ModelAndView 后,调 ViewResolver 解析出具体 View,然后渲染视图返回给客户端。

整个流程的核心是 DispatcherServlet,所有调度都是它在做,其他组件各司其职。

36、Spring Bean 自动装配有哪些方式

三种:

byName:按 Bean 的名字匹配,属性名和 Bean 的 id 一致就能注入。

byType:按类型匹配,同类型只有一个可以,多个的话就报错了——除非配合 @Qualifier 或 @Primary。

构造器注入:根据构造器参数类型自动匹配,Spring 4.x 以后如果只有一个构造器,@Autowired 都可以省略。

37、Spring 框架中都用到了哪些设计模式

列举几个最典型的:

工厂模式:BeanFactory、ApplicationContext 就是工厂,负责创建和管理 Bean。

单例模式:默认 Bean 就是单例的。

代理模式:AOP 的底层实现,JDK 动态代理和 CGLIB。

模板方法:JdbcTemplate、RestTemplate,固定流程 + 钩子方法。

观察者模式:ApplicationEvent 和 ApplicationListener,事件发布订阅。

适配器模式:HandlerAdapter,适配不同类型的 Controller。

38、Spring 框架中的单例 Bean 是线程安全的吗

不是。Spring 只保证 Bean 是单例的,不保证线程安全——线程安全得靠你自己。

如果你的单例 Bean 是无状态的——没有可变成员变量——那自然是线程安全的。但如果你在单例 Bean 里放了个可修改的成员变量,多线程并发访问一定出问题。

所以不要在 Service 或 Controller 里定义成员变量来存请求相关的数据,用局部变量或者 ThreadLocal。

39、Spring 事务实现方式有哪些

两种:

声明式事务:基于 AOP,在方法上加 @Transactional 就行,最常用。灵活,改事务行为改注解就行。

编程式事务:通过 TransactionTemplate 或 PlatformTransactionManager 手动编码控制。更精细但代码侵入大,一般只在需要精确控制事务边界的场景用。

40、Spring 的事务传播行为有哪些

七种,最常用的是前四个:

REQUIRED(默认):有事务就加入,没有就新建。最常用的行为。

SUPPORTS:有事务就用,没有也无所谓,非事务执行。

MANDATORY:必须在事务里,没有就抛异常。

REQUIRES_NEW:无论有没有,都挂起当前事务,开个新的。内部事务和外部事务完全独立。

NOT_SUPPORTED:非事务执行,有事务就挂起。

NEVER:不能有事务,有就抛异常。

NESTED:嵌套事务,通过保存点机制,内部回滚不影响外部事务。

41、哪些情况会导致 Spring 事务失效

常见的六个坑:

非 public 方法:@Transactional 基于 AOP 动态代理,代理只能拦截 public 方法。

同类自调用:同一个类里 A 方法调 B(加了事务注解的),代理失效,事务不生效。解决方式是注入自己或者拆到另一个类里。

异常被 try-catch 吃了:异常被捕获没抛出去,AOP 感知不到,不回滚。

异常类型不对:默认只回滚 RuntimeException 和 Error,受检异常不回滚。可以配置 rollbackFor。

数据库引擎不支持事务:比如用了 MyISAM,加多少 @Transactional 都没用。

传播行为配错了:比如配了 NOT_SUPPORTED。

42、Spring 拦截器和过滤器的区别

几个维度区分:

归属:过滤器是 Servlet 规范的,拦截器是 Spring 的。

执行时机:过滤器在请求到 DispatcherServlet 之前执行,拦截器在 DispatcherServlet 之后、Controller 前后执行。

作用范围:过滤器拦截所有请求(含静态资源),拦截器只对 Spring 管理的请求生效。

实现机制:过滤器基于方法回调,拦截器基于反射和动态代理。

典型用途:过滤器做编码设置、跨域、安全检查;拦截器做权限校验、日志记录。

43、说说你对 CAP 理论的理解

CAP 是分布式系统的核心理论:一致性(C)、可用性(A)、分区容错性(P),三者不可兼得,只能同时满足两个。

但实际情况是 P 必选——网络分区一定会发生,你没法回避。所以实际是在 C 和 A 之间做权衡。

比如银行系统优先保证一致性,扣了钱就得准确,宁可短暂不可用。社交平台优先保证可用性,用户发的动态一时刷不到可以接受,但不能刷不出来。

Nacos 比较特殊,它在服务发现场景用 AP,配置管理场景用 CP,通过不同的协议来适配。

44、SpringCloud 有哪些常用组件

比较核心的几个:

服务注册发现:Nacos、Eureka。Nacos 同时支持 AP 和 CP。

负载均衡:Spring Cloud LoadBalancer,替代了 Ribbon。

服务调用:OpenFeign,声明式 RPC。

熔断降级:Sentinel 为主流,Hystrix 已经停更了。

网关:Spring Cloud Gateway,基于 WebFlux,性能比 Zuul 好。

配置中心:Nacos,支持动态刷新。

45、SpringBoot 的常用注解有哪些

分层来说:

启动类:@SpringBootApplication,组合了 @Configuration、@EnableAutoConfiguration、@ComponentScan。

控制器层:@RestController(= @Controller + @ResponseBody)、@RequestMapping 系列。

依赖注入:@Autowired、@Qualifier、@Value。

分层标识:@Service、@Repository、@Component。

配置类:@Configuration + @Bean。

46、SpringBoot 的自动装配原理是什么

核心就是 @EnableAutoConfiguration。

入口是 @SpringBootApplication 里的 @EnableAutoConfiguration,它通过 @Import 导入 AutoConfigurationImportSelector。这个 Selector 在启动时扫描 classpath 下 META-INF/spring.factories 文件(Spring Boot 3.x 改成了 org.springframework.boot.autoconfigure.AutoConfiguration.imports),加载所有 XXXAutoConfiguration 类。

每个 AutoConfiguration 类上都有一堆 @Conditional 条件注解,比如 @ConditionalOnClass(类路径有这个类)、@ConditionalOnMissingBean(容器里没有这个 Bean)——条件满足才生效,条件不满足就跳过。

生效的配置类通过 @Bean 向容器注入组件。这就是“约定大于配置”的底层支撑。

47、Nacos 如何同时支持 AP 和 CP 模式?如何切换

Nacos 通过不同模块用不同协议来实现:

服务发现场景用 AP 模式,底层是自研的 Distro 协议,保证可用性和分区容错性,性能更高。

配置管理场景用 CP 模式,底层是 Raft 协议,保证数据强一致性——配置不能丢、不能错。

切换方式有两种:可以通过控制台或者 API 临时切换服务实例的模式。但一般没必要手动切,默认设计就已经区分开了。

48、RabbitMQ 中 Topic 模式,RoutingKey 中的 # 号和 * 号有什么区别

都是通配符,区别在匹配数量:

(星号):匹配一个单词。比如 a. 匹配 a.b,但不能匹配 a.b.c 或者 a。

#(井号):匹配零个或多个单词。a.# 可以匹配 a、a.b、a.b.c 都行。

实际应用中,# 更常用,因为更灵活。

49、RabbitMQ 中如何保障消息不丢失

从消息的全程链路来看,三个环节都要保证:

生产者端:开启 Publisher Confirm 模式,消息发到 Broker 后等确认 ack,没收到就重试。消息还要设 deliveryMode=2 持久化。

Broker 端:队列声明为 durable 持久化,消息持久化到磁盘。集群场景配合镜像队列,多节点同步数据。

消费者端:关掉自动确认,消费者处理完业务逻辑后手动 basicAck。这样消费者挂了消息还在 Broker 那边,能重新投递。不过手动确认后要做好幂等——因为网络问题可能重复投递。

50、如何确保消息不会被重复消费

核心就是幂等性。几种常用方案:

唯一 ID 校验:每条消息带一个全局唯一 ID,消费前先去 Redis 或数据库查这个 ID 处理过没有,处理过就跳过。

数据库唯一约束:比如订单号建唯一索引,重复插入直接报唯一约束冲突,自然就幂等了。

状态机:业务数据加状态字段——待处理→处理中→已完成,重复消息来了发现状态已经是已完成,直接跳过。

51、RabbitMQ 如何确保消息顺序消费

RabbitMQ 本身不保证全局顺序,因为一个队列多个消费者并发消费,顺序一定乱。

解决思路:让需要顺序的消息进同一个队列,并且这个队列只有一个消费者。比如同一个订单 ID 的消息,通过路由键全部发到同一个队列,然后这个队列只配一个消费者。性能牺牲是有的,但能保证顺序。如果还要提升性能,消费者内部可以用内存队列二次排序,再批量处理。

52、Kafka 中如何保障消息不丢失

同样是三个环节:

生产者:acks=all,等所有 ISR 副本都确认才返回;加上 retries 重试机制。

Broker:replication.factor 至少 3,min.insync.replicas 至少 2,确保消息真正写入了多个副本。合理设置 retention 策略,别让消息被提前删除。

消费者:enable.auto.commit 关掉,消费逻辑处理完后手动 commit offset。这样如果消费者挂了,offset 没提交,重启后能重新消费。

53、Kafka 如何确保消息顺序消费

Kafka 的顺序保证在分区级别——单个分区内消息是有序的。所以思路就是:把需要顺序的消息发到同一个分区。

做法:生产者发送时指定相同的 key,Kafka 对 key 哈希后路由到固定分区。一个分区只被一个消费者消费,自然保证了顺序。

全局顺序的话只能一个分区,性能太差,基本不用。

54、RabbitMQ 消息积压怎么办

分三步处理:

紧急止损:扩容消费者数量,快速提升消费能力。如果队列有上限先调大或者扩容集群。

排查根因:看消费者是不是有 bug 导致处理慢或卡死,还是上游消息量突然暴涨。通过监控和日志定位。

长期优化:修复消费者 bug,优化消费逻辑——比如批量处理提升吞吐,做好监控预警,在积压初期就发现问题。

55、Producer 的发送确认机制(acks)有哪几种

三种:

acks=0:发送后不等确认就认为成功,吞吐最高但可能丢消息。

acks=1(默认):Leader 写入成功就确认。如果 Leader 刚写入还没同步到 Follower 就挂了,消息丢了。

acks=all 或 -1:等所有 ISR 副本都写入成功才确认,最可靠,吞吐最低。对一致性要求高的场景用这个。

56、消息如何被路由到指定的 Partition

三种方式:

直接指定分区号:生产者代码里写死 partition。

指定 key(最常用):对 key 做哈希映射到分区,保证同一个 key 的消息进同一个分区,这是实现有序消费的基础。

都不指定:默认轮询,均匀分布到所有分区。

57、说说 ElasticSearch 的倒排索引

倒排索引是 ES 快的核心。传统正排索引是从文档找词,倒排索引反过来——从词找文档。

流程:先对文档内容分词,拿到一系列词条(Term),然后建立“词条→文档列表”的映射。查询时直接按词条匹配,找到包含这个词的所有文档,不用全表扫描。

比如两篇文档“Java编程”和“Python编程”,倒排索引里“编程”会映射到两篇文档,“Java”只映射到第一篇。查“编程”时直接拿到两篇文档,非常快。

58、text 和 keyword 数据类型的区别

必考。核心区别是分不分词:

keyword:不分词,整个字符串存成一个完整的 term。用于精确匹配——term 查询、排序、聚合、过滤,比如邮箱、ID、标签、状态码。

text:会分词,经过分析器拆成词条存入索引。用于全文搜索——match 查询、相关性评分,比如文章内容、商品描述。

text 字段会自动生成一个 .keyword 子字段,需要精确匹配时可以用它。

59、分片和副本的作用是什么?如何设置分片数

分片:把索引数据水平拆分到多个分片上。主要作用是让数据量突破单机限制,同时把读写操作并行分布到各分片,提升吞吐。

副本:每个分片的拷贝。两个作用:一是高可用——主分片挂了副本能顶上;二是提升读吞吐——查询可以打到副本上分摊压力。

分片数设置是个重要决策,主分片数创建后不能改。建议:每个分片控制在 20-40GB,不要超过 50GB;分片数能被节点数整除,分布均匀;别贪多——分片越多开销越大。

60、ElasticSearch 的深度分页该怎么做

默认的 from+size 方式深度分页性能很差——它要取前 N 条数据排序后只返回最后几条,数据量大了内存扛不住。

两种替代方案:

scroll:生成一个快照游标,后续按游标滚动取数,适合批量导出。缺点是非实时,占用资源。

search_after:用上一页最后一条的排序值做查询条件,类似游标但支持实时。需要指定唯一排序字段,适合实时分页场景。

另外业务上限一下分页深度也行——比如最多只让翻 100 页。