按三类来说:
创建型:单例用得最多,工厂模式在框架里随处可见,建造者适合构造复杂对象,比如 Lombok 的 @Builder。
结构型:代理模式——Spring AOP 的核心就是动态代理;适配器——比如 SpringMVC 的 HandlerAdapter;装饰器——不改变原类的基础上增强功能。
行为型:观察者模式——Spring 的事件监听机制就是;策略模式——不同算法可以动态切换,比如支付策略;模板方法——固定流程、可变步骤,JdbcTemplate 就是典型。
重写是父子类之间的事,方法签名完全一样,子类重新实现父类方法,用来实现多态。访问权限不能比父类更严格,返回值可以是父类返回值的子类(协变返回类型)。
重载是同一个类里,方法名一样但参数列表不同——个数、类型或顺序不同。返回值可以不同,但光靠返回值不同不算重载。
核心区分:重写看运行时的实际类型,重载看编译时的引用类型。
浅克隆:只复制对象本身,基本类型字段直接复制值,引用类型字段复制的是引用地址——所以克隆对象和原对象的引用字段还是指向同一个对象。
深克隆:递归地把所有引用字段指向的对象也复制一遍,克隆对象和原对象完全独立,改一个不影响另一个。
实现方式的话,要么手动递归复制所有引用对象,要么用序列化反序列化,或者用第三方库。
四种,按强度递减:
强引用:我们天天用的 new 出来的对象,只要引用在就不会被 GC。
软引用(SoftReference):内存不够的时候才回收,适合做缓存。
弱引用(WeakReference):GC 时不管内存够不够都会回收,ThreadLocal 里的 Entry 的 key 就是弱引用。
虚引用(PhantomReference):最弱的,拿不到引用对象本身,只能配合 ReferenceQueue 用来跟踪对象什么时候被 GC,堆外内存管理会用。
按发展代来分:
老年代收集器:Serial 单线程,Parallel 多线程注重吞吐,CMS 并发低延迟但会浮动垃圾和碎片问题,JDK14 移除了。
现代收集器:G1 从 JDK7 引入,JDK9 成为默认,把堆分成多个 Region,兼顾吞吐和延迟。ZGC 和 Shenandoah 是超低延迟的,JDK11 引入,现在 JDK21 已经很成熟了,停顿能做到几毫秒级别。
实际项目里 JDK8 默认是 Parallel,JDK9+ 默认 G1。大内存、低延迟场景考虑 ZGC。
重写 ClassLoader 的 loadClass() 方法就行。默认逻辑是先委托父加载器,我们可以改成自己先加载,加载不了再找父加载器。
最典型的例子:Tomcat 的 WebAppClassLoader。每个 Web 应用用自己的类加载器优先加载自己的类,这样才能隔离不同应用的同名类——不然两个应用各自的 User 类就冲突了。
JDBC 的 DriverManager 也是,通过线程上下文类加载器绕过了双亲委派。
JVM 用的是可达性分析算法。从 GC Roots 出发,顺着引用链往下找,链上够得着的对象都是活的,够不着的就标记为可回收。
GC Roots 主要包含:虚拟机栈里引用到的对象、静态变量引用的对象、常量引用的对象、本地方法栈里引用的对象。
还有个引用计数法,循环引用解决不了,JVM 没采用。
JDK6 以后做了锁升级优化,路径是:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,只能升级不能降级。
偏向锁:同一线程反复获取同一把锁,就偏给它,只在 Mark Word 里记个线程 ID,基本没开销。
轻量级锁:有另一个线程尝试获取偏向锁时升级过来,通过 CAS 自旋竞争,适合线程交替执行的场景。
重量级锁:自旋超过一定次数或者竞争太激烈,升级为重量级,底层依赖操作系统的 mutex,线程会阻塞。
执行时机不同:where 在分组前过滤行数据,having 在分组后过滤分组结果。
聚合支持不同:where 不能用聚合函数——你不能 where sum(age) > 100;having 专门配合聚合函数用,having sum(age) > 100 是合法的。
简单说:where 筛行,having 筛组。
内连接(INNER JOIN):两表都匹配才保留,取交集。
左连接(LEFT JOIN):左表全保留,右表匹配的连上,不匹配的字段填 NULL。
右连接(RIGHT JOIN):反过来,右表全保留。
全连接(FULL JOIN):两张表都全保留,不匹配的填 NULL,取的是并集。MySQL 原生不支持 FULL JOIN,需要用 LEFT JOIN UNION RIGHT JOIN 来模拟。
MVCC 是多版本并发控制,核心思想是:每行数据保留多个版本,读操作不用加锁,直接读历史版本就行,读写互不阻塞。
实现上,InnoDB 的每行数据有两个隐藏列:trx_id(创建该版本的事务 ID)和 roll_pointer(指向 undo log 里的旧版本)。事务通过 ReadView 判断哪些版本对它可见——RC 级别每次查询生成新的 ReadView,RR 级别整个事务用同一个 ReadView,所以能重复读。
分层应对:
第一层,持久化恢复:如果开了 RDB 或 AOF,重启后会自动加载数据文件,数据能回来。
第二层,主从切换:如果配了主从 + 哨兵,哨兵会自动把从库提成主库,做到故障自动转移。
第三层,集群兜底:如果是 Cluster 模式,其他节点继续服务,故障节点的从节点会接管。
关键平时要做好持久化配置和主从/集群部署,别等崩了再想办法。
哨兵是 Redis 官方提供的高可用方案,一个独立的进程,核心做三件事:
监控:持续 ping 主从节点,检测健康状态。
自动故障转移:主库挂了,哨兵之间先选举出一个 Leader,然后由 Leader 从从库中选一个升级为主库,通知其他从库去复制新主库。
通知:故障转移完成后,通知客户端新的主库地址。
至少部署三个哨兵节点才能保证选举的可靠性。
Redis Cluster 用的是哈希槽方案,总共 16384 个槽。每个 key 经过 CRC16 哈希后对 16384 取模,就确定归属哪个槽,槽再分配给各个节点。
客户端请求时,如果请求的节点不负责这个槽,节点会返回 MOVED 指令告诉客户端正确的节点地址,客户端重定向过去。这也是为什么批量操作如果跨了槽,在 Cluster 模式下会受限。
一级缓存:默认开启,SqlSession 级别的,同一个 SqlSession 里重复查同一条 SQL 会命中。关不掉,因为它是 SqlSession 内部维护的。
二级缓存:需要手动开启,Mapper namespace 级别的,跨 SqlSession 共享。在 Mapper.xml 里加 标签就行,实体类要实现 Serializable。
实际开发里一级缓存默认用着就行,二级缓存要谨慎——因为别的 namespace 的更新可能导致缓存脏数据。
#{} 是预编译占位符,MyBatis 会把 SQL 中的 #{} 替换成 ?,参数通过 PreparedStatement 设进去,自动加引号、能防 SQL 注入,安全。
${} 是直接字符串拼接,不预编译,就纯粹拼 SQL。有注入风险,但动态表名、动态排序字段这种场景必须用它,因为占位符不能替换表名。这种场景一定要自己对参数做白名单校验。
常用的五个:
:条件判断,满足才拼 SQL。
:自动处理开头的 AND/OR,配合 用很常见。
:UPDATE 语句用,自动处理逗号。
:遍历集合,批量插入、IN 查询必用。
:多条件分支,相当于 switch-case。
反射就是程序在运行时能拿到类的元数据——类名、字段、方法、构造器——并且可以动态操作它们。核心入口是 Class 对象,不管是通过 类名.class、对象.getClass(),还是 Class.forName() 拿到 Class 对象,后续操作都一样。
实际应用场景:框架底层大量用反射——Spring 的依赖注入、MyBatis 的结果映射,还有动态代理。缺点是性能有损耗、破坏封装性,所以框架里一般会做缓存。
第一种:类名.class,编译时就确定了,最直接。
第二种:对象.getClass(),已经有实例了就用这个。
第三种:Class.forName(“全限定类名”),运行时动态加载,会触发类的静态初始化。JDBC 加载驱动就是这种。
四种:
new 关键字:最常用。
反射:Class.newInstance() 或者 Constructor.newInstance(),框架里大量用。
克隆:实现 Cloneable 接口,调用 clone(),浅拷贝。
反序列化:ObjectInputStream.readObject(),从流里恢复对象,不经过构造方法。
IOC 就是控制反转,把对象创建和依赖管理的控制权从业务代码交给容器。不用在自己代码里 new 对象,而是由 Spring 容器统一管理 Bean 的生命周期和依赖关系。
核心好处是解耦。传统方式——类 A 依赖类 B,A 得自己 new B,两者强耦合。用了 IOC 之后,A 只需要声明需要什么,Spring 帮你注入进来,A 根本不关心 B 怎么创建的。依赖注入是 IOC 的具体实现方式。
AOP 是面向切面编程,解决的是横切逻辑的问题。比如日志、事务、权限校验这些——它们在很多方法里都要用,跟核心业务无关但又不能少,散落得到处都是。
AOP 的思路就是把这些横切逻辑抽成一个切面,定义好在哪些方法(切点)的什么时机(通知)织入进去,核心业务代码完全不用改。
实际项目里,Spring 事务就是 AOP 的典型应用——加个 @Transactional,框架自动帮你开启、提交、回滚事务。
动态代理。Spring 会根据目标类的情况选代理方式:
如果目标类实现了接口,用 JDK 动态代理,基于接口生成代理对象,通过反射调用目标方法。
如果目标类没实现接口,用 CGLIB,通过继承目标类生成子类代理,重写方法来做增强。
Spring Boot 2.x 之后默认就用 CGLIB 了,因为不需要强制基于接口,更灵活一些。
核心的几个:
@Aspect:把类声明为切面类。
@Pointcut:定义切点表达式,指明增强哪些方法,比如 execution(* com.xxx.service.*.*(..))。
然后是五种通知类型的注解:
@Before:方法执行前。
@After:方法执行后,不管有没有异常。
@AfterReturning:正常返回后。
@AfterThrowing:抛异常后。
@Around:环绕,包裹整个方法,最灵活——可以控制是否执行目标方法,也能修改参数和返回值。
五种,按执行时机分:
前置通知(@Before):目标方法执行前。
后置通知(@After):目标方法执行后,相当于 finally,不管是否异常都会执行。
返回后通知(@AfterReturning):目标方法正常返回后,能拿到返回值。
异常通知(@AfterThrowing):目标方法抛异常后,能拿到异常信息。
环绕通知(@Around):最强大,方法执行前后都可以写逻辑,还能决定要不要执行目标方法。
同一方法被多个通知增强时,执行顺序是:
环绕通知的前半部分 → 前置通知 → 目标方法 → 返回后/异常通知 → 环绕通知的后半部分 → 后置通知。
不同切面之间,默认按切面类名的字母顺序排序,也可以用 @Order 注解指定优先级,数字越小越先执行。
三种:
构造器注入:通过构造方法注入依赖,Spring 官方推荐的方式。好处是依赖不可变、对象创建完就能用、方便单元测试 mock。
Setter 注入:通过 setter 方法注入,可选依赖适合用这种方式。
字段注入:直接在字段上加 @Autowired,用起来最方便,但不好测试,也不符合不可变原则。实际开发里用得最多但也最不推荐。
来源不同:@Autowired 是 Spring 的,@Resource 是 JDK 标准 javax.annotation 包里的。
匹配逻辑不同:@Autowired 默认按类型(byType)注入,同类型有多个 Bean 时配合 @Qualifier 指定名称。@Resource 默认按名称(byName),找不到同名的再降级按类型。
作用位置不同:@Autowired 可以用在构造器、方法、字段上;@Resource 不支持构造器注入。
常用的五个:
singleton:默认,容器里只有一个实例,全局共享。
prototype:每次获取都创建新实例,适合有状态的 Bean。
request:Web 环境下,每个 HTTP 请求一个实例。
session:Web 环境下,每个会话一个实例。
application:Web 环境下,整个 ServletContext 一个实例。
实际开发 95% 的场景都是单例,prototype 用得很少——要注意 prototype Bean 的生命周期管理,Spring 只管创建不管销毁。
整体分四步:实例化 → 属性注入 → 初始化 → 销毁。
展开说:
实例化——调用构造方法创建对象。
属性注入——@Autowired 这些依赖注入进来。
然后是一系列 Aware 回调:BeanNameAware → BeanFactoryAware → ApplicationContextAware。
接着是 BeanPostProcessor 的前置处理(postProcessBeforeInitialization)。
再是初始化阶段:@PostConstruct → InitializingBean 的 afterPropertiesSet() → 配置的 init-method。
BeanPostProcessor 的后置处理(postProcessAfterInitialization)。
这时候 Bean 就绪了,可以用了。
容器关闭时,调用 @PreDestroy → DisposableBean 的 destroy() → 配置的 destroy-method。
这道题和上一题是同一个问题,补充强调四个大阶段:实例化、属性填充、初始化、销毁。其中初始化阶段最复杂,包含了 Aware 回调、BeanPostProcessor 处理、自定义初始化方法等,面试时重点讲这个阶段就行。
Spring 靠三级缓存来解决,但只限于单例 Bean 的 setter 注入——构造器注入的循环依赖解决不了,会直接抛 BeanCurrentlyInCreationException。
三级缓存分别存什么:
一级缓存 singletonObjects:存成品 Bean,完全初始化好的。
二级缓存 earlySingletonObjects:存提前暴露的半成品 Bean,创建了但属性还没注入完。
三级缓存 singletonFactories:存能生成 Bean 的工厂,可以提前创建代理对象。
流程:A 和 B 互相依赖时,A 先实例化,把自己提前暴露到三级缓存,然后去填充属性,发现需要 B。B 创建时发现需要 A,就从三级缓存拿到 A 的半成品引用,B 初始化完成。然后回到 A,此时 B 已经有了,A 完成初始化。一圈走下来,两个都好了。
按使用场景说:
控制器层:@Controller、@RestController(= @Controller + @ResponseBody)。
请求映射:@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping。
参数绑定:@RequestParam(请求参数)、@PathVariable(路径变量)、@RequestBody(请求体 JSON)、@RequestHeader(请求头)。
其他:@ResponseBody、@ModelAttribute、@SessionAttributes、@CrossOrigin。
核心七个组件,按请求流程串起来:
DispatcherServlet:前端控制器,所有请求入口。
HandlerMapping:根据请求 URL 找到对应的 Handler(Controller 方法)。
HandlerAdapter:真正去执行 Handler。
Handler(Controller):业务逻辑处理。
ModelAndView:封装处理结果和视图信息。
ViewResolver:把视图名解析成具体的 View 对象。
View:渲染页面,返回给客户端。
按请求流转说:
请求先到 DispatcherServlet。它调 HandlerMapping 找到对应的 Handler,然后找能执行这个 Handler 的 HandlerAdapter,由 Adapter 调 Handler 方法。方法执行完返回 ModelAndView。DispatcherServlet 拿到 ModelAndView 后,调 ViewResolver 解析出具体 View,然后渲染视图返回给客户端。
整个流程的核心是 DispatcherServlet,所有调度都是它在做,其他组件各司其职。
三种:
byName:按 Bean 的名字匹配,属性名和 Bean 的 id 一致就能注入。
byType:按类型匹配,同类型只有一个可以,多个的话就报错了——除非配合 @Qualifier 或 @Primary。
构造器注入:根据构造器参数类型自动匹配,Spring 4.x 以后如果只有一个构造器,@Autowired 都可以省略。
列举几个最典型的:
工厂模式:BeanFactory、ApplicationContext 就是工厂,负责创建和管理 Bean。
单例模式:默认 Bean 就是单例的。
代理模式:AOP 的底层实现,JDK 动态代理和 CGLIB。
模板方法:JdbcTemplate、RestTemplate,固定流程 + 钩子方法。
观察者模式:ApplicationEvent 和 ApplicationListener,事件发布订阅。
适配器模式:HandlerAdapter,适配不同类型的 Controller。
不是。Spring 只保证 Bean 是单例的,不保证线程安全——线程安全得靠你自己。
如果你的单例 Bean 是无状态的——没有可变成员变量——那自然是线程安全的。但如果你在单例 Bean 里放了个可修改的成员变量,多线程并发访问一定出问题。
所以不要在 Service 或 Controller 里定义成员变量来存请求相关的数据,用局部变量或者 ThreadLocal。
两种:
声明式事务:基于 AOP,在方法上加 @Transactional 就行,最常用。灵活,改事务行为改注解就行。
编程式事务:通过 TransactionTemplate 或 PlatformTransactionManager 手动编码控制。更精细但代码侵入大,一般只在需要精确控制事务边界的场景用。
七种,最常用的是前四个:
REQUIRED(默认):有事务就加入,没有就新建。最常用的行为。
SUPPORTS:有事务就用,没有也无所谓,非事务执行。
MANDATORY:必须在事务里,没有就抛异常。
REQUIRES_NEW:无论有没有,都挂起当前事务,开个新的。内部事务和外部事务完全独立。
NOT_SUPPORTED:非事务执行,有事务就挂起。
NEVER:不能有事务,有就抛异常。
NESTED:嵌套事务,通过保存点机制,内部回滚不影响外部事务。
常见的六个坑:
非 public 方法:@Transactional 基于 AOP 动态代理,代理只能拦截 public 方法。
同类自调用:同一个类里 A 方法调 B(加了事务注解的),代理失效,事务不生效。解决方式是注入自己或者拆到另一个类里。
异常被 try-catch 吃了:异常被捕获没抛出去,AOP 感知不到,不回滚。
异常类型不对:默认只回滚 RuntimeException 和 Error,受检异常不回滚。可以配置 rollbackFor。
数据库引擎不支持事务:比如用了 MyISAM,加多少 @Transactional 都没用。
传播行为配错了:比如配了 NOT_SUPPORTED。
几个维度区分:
归属:过滤器是 Servlet 规范的,拦截器是 Spring 的。
执行时机:过滤器在请求到 DispatcherServlet 之前执行,拦截器在 DispatcherServlet 之后、Controller 前后执行。
作用范围:过滤器拦截所有请求(含静态资源),拦截器只对 Spring 管理的请求生效。
实现机制:过滤器基于方法回调,拦截器基于反射和动态代理。
典型用途:过滤器做编码设置、跨域、安全检查;拦截器做权限校验、日志记录。
CAP 是分布式系统的核心理论:一致性(C)、可用性(A)、分区容错性(P),三者不可兼得,只能同时满足两个。
但实际情况是 P 必选——网络分区一定会发生,你没法回避。所以实际是在 C 和 A 之间做权衡。
比如银行系统优先保证一致性,扣了钱就得准确,宁可短暂不可用。社交平台优先保证可用性,用户发的动态一时刷不到可以接受,但不能刷不出来。
Nacos 比较特殊,它在服务发现场景用 AP,配置管理场景用 CP,通过不同的协议来适配。
比较核心的几个:
服务注册发现:Nacos、Eureka。Nacos 同时支持 AP 和 CP。
负载均衡:Spring Cloud LoadBalancer,替代了 Ribbon。
服务调用:OpenFeign,声明式 RPC。
熔断降级:Sentinel 为主流,Hystrix 已经停更了。
网关:Spring Cloud Gateway,基于 WebFlux,性能比 Zuul 好。
配置中心:Nacos,支持动态刷新。
分层来说:
启动类:@SpringBootApplication,组合了 @Configuration、@EnableAutoConfiguration、@ComponentScan。
控制器层:@RestController(= @Controller + @ResponseBody)、@RequestMapping 系列。
依赖注入:@Autowired、@Qualifier、@Value。
分层标识:@Service、@Repository、@Component。
配置类:@Configuration + @Bean。
核心就是 @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 向容器注入组件。这就是“约定大于配置”的底层支撑。
Nacos 通过不同模块用不同协议来实现:
服务发现场景用 AP 模式,底层是自研的 Distro 协议,保证可用性和分区容错性,性能更高。
配置管理场景用 CP 模式,底层是 Raft 协议,保证数据强一致性——配置不能丢、不能错。
切换方式有两种:可以通过控制台或者 API 临时切换服务实例的模式。但一般没必要手动切,默认设计就已经区分开了。
都是通配符,区别在匹配数量:
(星号):匹配一个单词。比如 a. 匹配 a.b,但不能匹配 a.b.c 或者 a。
#(井号):匹配零个或多个单词。a.# 可以匹配 a、a.b、a.b.c 都行。
实际应用中,# 更常用,因为更灵活。
从消息的全程链路来看,三个环节都要保证:
生产者端:开启 Publisher Confirm 模式,消息发到 Broker 后等确认 ack,没收到就重试。消息还要设 deliveryMode=2 持久化。
Broker 端:队列声明为 durable 持久化,消息持久化到磁盘。集群场景配合镜像队列,多节点同步数据。
消费者端:关掉自动确认,消费者处理完业务逻辑后手动 basicAck。这样消费者挂了消息还在 Broker 那边,能重新投递。不过手动确认后要做好幂等——因为网络问题可能重复投递。
核心就是幂等性。几种常用方案:
唯一 ID 校验:每条消息带一个全局唯一 ID,消费前先去 Redis 或数据库查这个 ID 处理过没有,处理过就跳过。
数据库唯一约束:比如订单号建唯一索引,重复插入直接报唯一约束冲突,自然就幂等了。
状态机:业务数据加状态字段——待处理→处理中→已完成,重复消息来了发现状态已经是已完成,直接跳过。
RabbitMQ 本身不保证全局顺序,因为一个队列多个消费者并发消费,顺序一定乱。
解决思路:让需要顺序的消息进同一个队列,并且这个队列只有一个消费者。比如同一个订单 ID 的消息,通过路由键全部发到同一个队列,然后这个队列只配一个消费者。性能牺牲是有的,但能保证顺序。如果还要提升性能,消费者内部可以用内存队列二次排序,再批量处理。
同样是三个环节:
生产者:acks=all,等所有 ISR 副本都确认才返回;加上 retries 重试机制。
Broker:replication.factor 至少 3,min.insync.replicas 至少 2,确保消息真正写入了多个副本。合理设置 retention 策略,别让消息被提前删除。
消费者:enable.auto.commit 关掉,消费逻辑处理完后手动 commit offset。这样如果消费者挂了,offset 没提交,重启后能重新消费。
Kafka 的顺序保证在分区级别——单个分区内消息是有序的。所以思路就是:把需要顺序的消息发到同一个分区。
做法:生产者发送时指定相同的 key,Kafka 对 key 哈希后路由到固定分区。一个分区只被一个消费者消费,自然保证了顺序。
全局顺序的话只能一个分区,性能太差,基本不用。
分三步处理:
紧急止损:扩容消费者数量,快速提升消费能力。如果队列有上限先调大或者扩容集群。
排查根因:看消费者是不是有 bug 导致处理慢或卡死,还是上游消息量突然暴涨。通过监控和日志定位。
长期优化:修复消费者 bug,优化消费逻辑——比如批量处理提升吞吐,做好监控预警,在积压初期就发现问题。
三种:
acks=0:发送后不等确认就认为成功,吞吐最高但可能丢消息。
acks=1(默认):Leader 写入成功就确认。如果 Leader 刚写入还没同步到 Follower 就挂了,消息丢了。
acks=all 或 -1:等所有 ISR 副本都写入成功才确认,最可靠,吞吐最低。对一致性要求高的场景用这个。
三种方式:
直接指定分区号:生产者代码里写死 partition。
指定 key(最常用):对 key 做哈希映射到分区,保证同一个 key 的消息进同一个分区,这是实现有序消费的基础。
都不指定:默认轮询,均匀分布到所有分区。
倒排索引是 ES 快的核心。传统正排索引是从文档找词,倒排索引反过来——从词找文档。
流程:先对文档内容分词,拿到一系列词条(Term),然后建立“词条→文档列表”的映射。查询时直接按词条匹配,找到包含这个词的所有文档,不用全表扫描。
比如两篇文档“Java编程”和“Python编程”,倒排索引里“编程”会映射到两篇文档,“Java”只映射到第一篇。查“编程”时直接拿到两篇文档,非常快。
必考。核心区别是分不分词:
keyword:不分词,整个字符串存成一个完整的 term。用于精确匹配——term 查询、排序、聚合、过滤,比如邮箱、ID、标签、状态码。
text:会分词,经过分析器拆成词条存入索引。用于全文搜索——match 查询、相关性评分,比如文章内容、商品描述。
text 字段会自动生成一个 .keyword 子字段,需要精确匹配时可以用它。
分片:把索引数据水平拆分到多个分片上。主要作用是让数据量突破单机限制,同时把读写操作并行分布到各分片,提升吞吐。
副本:每个分片的拷贝。两个作用:一是高可用——主分片挂了副本能顶上;二是提升读吞吐——查询可以打到副本上分摊压力。
分片数设置是个重要决策,主分片数创建后不能改。建议:每个分片控制在 20-40GB,不要超过 50GB;分片数能被节点数整除,分布均匀;别贪多——分片越多开销越大。
默认的 from+size 方式深度分页性能很差——它要取前 N 条数据排序后只返回最后几条,数据量大了内存扛不住。
两种替代方案:
scroll:生成一个快照游标,后续按游标滚动取数,适合批量导出。缺点是非实时,占用资源。
search_after:用上一页最后一条的排序值做查询条件,类似游标但支持实时。需要指定唯一排序字段,适合实时分页场景。
另外业务上限一下分页深度也行——比如最多只让翻 100 页。