SpringBoot集成Elasticsearch 7.2.0:配置、查询与避坑

发布时间:2026/10/5 2:41:17
SpringBoot集成Elasticsearch 7.2.0:配置、查询与避坑
简介这是一份面向Spring Boot开发者的技术笔记重点解决Spring Boot与Elasticsearch 7.2.0整合时版本不匹配的常见问题资源以单个PDF文件提供压缩包大小仅58KB内容紧凑方便快速查阅。文档首先指出Spring Boot 2.1.X自带的spring-boot-starter-data-elasticsearch仅支持Elasticsearch 2.X因此需要改而使用Spring Data Elasticsearch才能兼容7.2.X随后对比transport与rest两种连接方式说明官方建议采用rest方式。核心部分给出完整的Maven依赖列表、application.yml中elasticsearch.ip配置项以及基于RestHighLevelClient的客户端连接配置类包含连接超时、读取超时等关键参数均设为5分钟可帮助读者直接套用搭建最小可用环境。对于需要升级ES版本或快速整合搜索能力的初中级Java工程师这份笔记提供了清晰的依赖选型与配置思路能减少踩坑。已有6769人学习下载适合作为项目起步的参考资料。1. 先把话说清楚SpringBoot 和 Elasticsearch 7.2.0 到底该怎么对齐如果你接手一个老项目突然要在 SpringBoot 服务里把文档搜索、日志检索或业务数据聚合接进 Elasticsearch 7.2.0第一反应往往是去 Maven 上搜spring-boot-starter-data-elasticsearch。结果就是SpringBoot 版本稍微高一点启动直接给你抛 NoSuchMethodError低版本呢又会发现它自动帮你创建的客户端和服务端版本对不上请求发出去就报version mismatch。Elasticsearch 7.2.0 本身是个非常稳的版本但它真正的友善之处在于官方的elasticsearch-rest-high-level-client可以直接和 SpringBoot 解耦版本自己锁定配置自己注入不依赖那套容易被版本绑架的自动配置。这篇文章是给正在做搜索、日志采集、后台统计方案的人看的我把依赖、索引、查询、批量写入一条线给你走通最后列几个我真实环境里翻车过的坑。2. 搭建最小可运行工程依赖、配置和第一个连接2.1 依赖坐标怎么选放弃 spring-boot-starter-data-elasticsearch如果你只是想快速把数据写进去、查出来我建议第一步就放弃spring-boot-starter-data-elasticsearch。不是说它不能用而是它的版本映射关系太曲折了SpringBoot 2.2 对应 Spring Data Elasticsearch 3.2.x这个 3.2.x 才能匹配 ES 7.2SpringBoot 2.7 直接带的是 4.4.x它面对 ES 7.17 是合适的但拿到 7.2.0 的节点上就会出兼容问题。自动装配原理再完美也架不住服务端和客户端版本差异太大。我一般用最直接的一套依赖把 ES 的版本锁死在 POM 里properties java.version1.8/java.version elasticsearch.version7.2.0/elasticsearch.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId version${elasticsearch.version}/version /dependency dependency groupIdorg.elasticsearch/groupId artifactIdelasticsearch/artifactId version${elasticsearch.version}/version /dependency /dependencies注意elasticsearch-rest-high-level-client本身会传递依赖elasticsearch-rest-client但elasticsearch核心库并不保证一定被正确带起来所以我习惯把org.elasticsearch:elasticsearch也显式加上。为什么不用 starter因为 starter 会触发 Spring Data Elasticsearch 的自动配置它会在容器里帮你再建一个RestHighLevelClient而那个客户端的版本号是由 Spring Data 决定的不是由你pom.xml里的elasticsearch.version决定的。等你连上一个 7.2.0 的集群客户端却是 7.17很容易出现请求头里 version 不对的诡异错误。直接引入官方 client整个对象的创建权都在你手里升级时只需要改一个 version 属性。2.2 application.yml 里的关键配置与第一个连接自检工程里的配置文件很简单不需要让 SpringBoot 去自动识别 ES 前缀我们打算用Value手动绑定。这样做的另一个好处是当你的 SpringBoot 版本高到不认某些配置路径时这个 Bean 还是能自己独立建起来。spring: application: name: es-demo jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 elasticsearch: hosts: 192.168.1.10:9200,192.168.1.11:9200 max-conn-total: 100 max-conn-per-route: 50接下来写一个配置类把 hosts 串拆开构造出HttpHost数组再交给RestClientBuilderConfiguration public class EsConfig { Value(${elasticsearch.hosts}) private String hosts; Value(${elasticsearch.max-conn-total:100}) private Integer maxConnTotal; Value(${elasticsearch.max-conn-per-route:50}) private Integer maxConnPerRoute; Bean public RestHighLevelClient restHighLevelClient() { String[] hostArr hosts.split(,); HttpHost[] httpHosts new HttpHost[hostArr.length]; for (int i 0; i hostArr.length; i) { String host hostArr[i].split(:)[0]; int port Integer.parseInt(hostArr[i].split(:)[1]); httpHosts[i] new HttpHost(host, port, http); } RestClientBuilder builder RestClient.builder(httpHosts); builder.setMaxRetryTimeoutMillis(30000); builder.setRequestConfigCallback(config - config .setConnectTimeout(5000) .setSocketTimeout(10000) .setConnectionRequestTimeout(5000)); builder.setHttpClientConfigCallback(clientBuilder - clientBuilder .setMaxConnTotal(maxConnTotal) .setMaxConnPerRoute(maxConnPerRoute)); return new RestHighLevelClient(builder); } }这里有几个参数一定要讲清楚。setMaxRetryTimeoutMillis不是单个请求的超时时间而是客户端在同一个请求上允许重试的累计时间上限ES 节点暂时不可用时它很有用。requestConfigCallback里那三个超时分别控制连接建立、Socket 读等待、从连接池获取连接的时间。后两个在高并发下特别关键如果连接池被打满connectionRequestTimeout太短会误报超时太长又会让线程堆积。maxConnTotal和maxConnPerRoute就是 Apache HttpClient 的链接池大小前者是所有节点的总连接数后者是单个节点的并发连接上限。连接一建好先写一个启动自检是很稳的做法。我通常在ApplicationRunner里调用client.ping()和client.info()Component public class EsHealthCheck implements ApplicationRunner { private final RestHighLevelClient client; public EsHealthCheck(RestHighLevelClient client) { this.client client; } Override public void run(ApplicationArguments args) throws Exception { boolean ping client.ping(RequestOptions.DEFAULT); if (!ping) { throw new RuntimeException(Elasticsearch is not reachable); } MainResponse response client.info(RequestOptions.DEFAULT); System.out.println(cluster: response.getClusterName()); System.out.println(version: response.getVersion()); } }ping()实际上就是请求了一下/路径开销极小适合启动时确认基础网络通不通。client.info()能拿到集群名和节点版本号连的是不是 7.2.0 一眼就能看出来。如果你不想因为 ES 故障把整个 SpringBoot 应用启动卡死可以把检查逻辑从run()里拿掉改成监听ApplicationReadyEvent只打印 WARN 日志而不是抛异常。这个选择没有对错完全看你的运维习惯。3. 把索引和文档操作写成代码RestHighLevelClient 的标准姿势3.1 索引的创建、判断和删除索引在 ES 里就是数据的容器7.x 开始一个索引下不再建议分多个 type直接用_doc就够。创建索引时最关键的其实是 mapping它决定了哪些字段能被精确匹配、哪些字段能分词、哪些字段能参与排序。public void createIndex(String indexName) throws IOException { CreateIndexRequest request new CreateIndexRequest(indexName); request.settings(Settings.builder() .put(index.number_of_shards, 5) .put(index.number_of_replicas, 1) .put(index.refresh_interval, 10s)); String mappingJson {\properties\:{ \id\:{\type\:\keyword\}, \title\:{\type\:\text\,\analyzer\:\ik_max_word\}, \createTime\:{\type\:\date\,\format\:\yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis\}, \status\:{\type\:\integer\} }}; request.mapping(_doc, mappingJson, XContentType.JSON); CreateIndexResponse response client.indices().create(request, RequestOptions.DEFAULT); if (!response.isAcknowledged()) { throw new RuntimeException(create index failed: indexName); } }参数说明里有三个点要特别注意。number_of_shards是主分片数创建后不能改数据量超过几个 GB 前最好先规划好number_of_replicas是副本数这个以后用UpdateSettingsRequest还能动态调。refresh_interval是刷新间隔默认 1 秒写成 10s 可以明显降低写入时的段合并压力但代价是数据写入后要等 10 秒才可查询适合日志类场景。mapping 里用ik_max_word是 IK 分词器的最大切分方式如果你没装 IK 插件这里会报失败没插件就改成standard。判断索引是否存在和删除索引用.indices()下的方法就行GetIndexRequest getRequest new GetIndexRequest(indexName); boolean exists client.indices().exists(getRequest, RequestOptions.DEFAULT); if (exists) { DeleteIndexRequest deleteRequest new DeleteIndexRequest(indexName); AcknowledgedResponse deleteResponse client.indices().delete(deleteRequest, RequestOptions.DEFAULT); System.out.println(delete acknowledged: deleteResponse.isAcknowledged()); }exists()和get()不同exists只关心返回状态码不会把全部 index 元的描述拉回来所以在分支逻辑里优先用它。删除索引等于删除底下所有数据生产环境我一般会对索引名做白名单校验比如只允许删除以tmp_开头的索引避免手滑把线上索引删掉。3.2 文档的增删改与批量写入有了索引就可以往里写文档了。单条写入直接用IndexRequest把业务对象转成Map或 JSON 字符串都行public void indexDoc(String indexName, String id, MapString, Object source) throws IOException { IndexRequest request new IndexRequest(indexName) .id(id) .source(source, XContentType.JSON); IndexResponse response client.index(request, RequestOptions.DEFAULT); System.out.println(response.getResult()); }一定要传业务 id别让 ES 自动生成。自动生成 id 在数据重放、幂等更新时非常痛苦你没法用同一条记录去覆盖之前的数据。index操作在 id 已存在时默认是覆盖整条文档。部分更新用UpdateRequestUpdateRequest request new UpdateRequest(indexName, id); MapString, Object doc new HashMap(); doc.put(status, 2); doc.put(updateTime, LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); request.doc(doc); UpdateResponse response client.update(request, RequestOptions.DEFAULT); System.out.println(response.getResult());这里要提一个被坑过很多次的行为request.doc(doc)是合并字段不是覆盖。如果 doc 里只写了status那这条文档里其他字段都还在。setUpsert(doc)可以让文档不存在时自动创建存在时按 doc 更新这个在增量同步场景里很实用。删除单条文档就更简单了DeleteRequest request new DeleteRequest(indexName, id); DeleteResponse response client.delete(request, RequestOptions.DEFAULT);真正到了生产写入基本不会一条条来而是用BulkRequest。比如从数据库批次同步 10 万条记录绝对不能写一个 for 循环挨个index()那样集群的连接和线程会被瞬间打满。正确姿势是分批批量写public void bulkIndex(String indexName, ListMapString, Object rows) throws IOException { BulkRequest bulkRequest new BulkRequest(); int batchSize 5000; for (int i 0; i rows.size(); i) { MapString, Object row rows.get(i); IndexRequest request new IndexRequest(indexName) .id(String.valueOf(row.get(id))) .source(row, XContentType.JSON); bulkRequest.add(request); if ((i 1) % batchSize 0 || i rows.size() - 1) { BulkResponse response client.bulk(bulkRequest, RequestOptions.DEFAULT); handleBulkResponse(response, rows, i); bulkRequest new BulkRequest(); } } }batchSize一般取 30005000 比较稳。太小了HTTP 请求往返次数多太大单次请求体可能超过 ES 默认的 100MB http.max_content_length会直接收到Content is too large。每次bulk()之后要检查hasFailures()不能只看总耗时。失败项里有item.getFailure().getCause()通常能拿到明确的异常信息。我习惯把失败的数据捞出来单独记录到日志后续补推而不是让它静默丢失。4. 查询才是重头戏从 term 到 aggregations 的落地写法4.1 组合查询bool term match range查询是最能体现 ES 设计思想的部分。如果只是按 id 主键查询那和关系型数据库没区别真正厉害的是把分词匹配、精确过滤、范围筛选组合在一个请求里。假设你要查文章索引条件是这样的标题包含springboot状态必须为 1创建时间在 2024 年按点击量倒序。用 Java 客户端这么写SearchRequest searchRequest new SearchRequest(article); SearchSourceBuilder sourceBuilder new SearchSourceBuilder(); BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.matchQuery(title, springboot)); boolQuery.filter(QueryBuilders.termQuery(status, 1)); boolQuery.filter(QueryBuilders.rangeQuery(createTime) .gte(2024-01-01 00:00:00) .lte(2024-12-31 23:59:59)); sourceBuilder.query(boolQuery); sourceBuilder.from(0).size(20); sourceBuilder.sort(clickCount, SortOrder.DESC); SearchResponse response client.search(searchRequest, RequestOptions.DEFAULT);matchQuery会对查询文本分词然后从倒排索引里找匹配termQuery是精确匹配不做任何分词适合 status、id 这类字段。rangeQuery只能用在 Date、Integer 或 Keyword 类型上如果字段是 text即便内容是日期也没法比较。注意filter里的条件不参与相关度评分所以它的执行性能比must好很多能过滤掉的记录越早后面算分的代价就越小。from和size不能超过 index.max_result_window默认 10000超过就得换search_after。查询结果解析也不复杂SearchHits hits response.getHits(); long total hits.getTotalHits().value; for (SearchHit hit : hits.getHits()) { MapString, Object source hit.getSourceAsMap(); System.out.println(source.get(id) - source.get(title)); }getSourceAsMap()返回的是整个_source字段的 Map。如果你在 mapping 里设置了_source: {enabled: false}这一步就拿不到原始文档了只能从hit.getFields()里取。7.2 里getTotalHits()返回的TotalHits对象有个relation属性当总数超过 10000 时可能是GREATER_THAN_OR_EQUAL_TO注意别把它当精确值展示给用户。想做标题高亮在SearchSourceBuilder上加高亮器HighlightBuilder highlightBuilder new HighlightBuilder(); highlightBuilder.field(title) .preTags(span classhighlight) .postTags(/span); sourceBuilder.highlighter(highlightBuilder);解析时可以从hit.getHighlightFields()里拿HighlightField再取 fragments 拼接。这在搜索系统里基本是标配功能但很多人漏了高亮字段也需要单独申请开发量。4.2 聚合统计按时间、按分类的 bucket 写法聚合是 ES 比普通数据库更有价值的地方尤其是时间序列统计。先看一个最常用的按天统计文档数量。DateHistogramAggregationBuilder dateHist AggregationBuilders .dateHistogram(by_date) .field(createTime) .calendarInterval(DateHistogramInterval.DAY) .format(yyyy-MM-dd); sourceBuilder.aggregation(dateHist); SearchResponse response client.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggregations response.getAggregations(); ParsedDateHistogram parsed aggregations.get(by_date); for (Histogram.Bucket bucket : parsed.getBuckets()) { System.out.println(bucket.getKeyAsString() / bucket.getDocCount()); }这里必须提醒在 7.2.0 里dateHistogram的.interval(1d)已经废弃继续用会抛异常或不生效。正确做法是用.calendarInterval()或.fixedInterval()。calendarInterval会按自然日、自然月来对齐比如DAY就是从零点开始fixedInterval不做日历对齐适合做固定时间窗的监控统计。解析时ParsedDateHistogram是Histogram.Aggregation的解析类拿到buckets后逐个取keyAsString和docCount就好。按状态分类统计数量用termsAggregationTermsAggregationBuilder termsAgg AggregationBuilders .terms(by_status) .field(status) .size(10); sourceBuilder.aggregation(termsAgg);如果你要对一个 text 字段做 terms 聚合直接field(title)会报Fielddata is disabled on text fields。常见做法是改成title.keyword前提是你映射里开了 keyword 子字段。这个关键字子字段在 7.2 的动态映射里默认就有但如果你手工只定义了 text 类型那就没有 keyword聚合时只能另想办法。聚合可以嵌套而且这才是真正的统计价值。比如按天分桶后再统计每天点击量总和DateHistogramAggregationBuilder dateHist AggregationBuilders .dateHistogram(by_date) .field(createTime) .calendarInterval(DateHistogramInterval.DAY); dateHist.subAggregation(AggregationBuilders.sum(total_clicks).field(clickCount)); sourceBuilder.aggregation(dateHist);解析子聚合时先拿到ParsedDateHistogram再在每一个 bucket 里取子聚合for (Histogram.Bucket bucket : parsed.getBuckets()) { ParsedSum sum bucket.getAggregations().get(total_clicks); System.out.println(bucket.getKeyAsString() - sum.getValue()); }子聚合的类型有很多sum、avg、cardinality、percentiles都行。cardinality可以统计去重数量但默认精度有限精确去重需要换composite或做二次查询。聚合这类东西写出来容易跑得慢也容易关键是看你的字段类型和数据量是否匹配。5. 这些坑我替你踩过了版本冲突、连接泄漏、类型转换和其它玄学5.1 SpringBoot 版本太高导致自动配置客户端不对现象项目用的是 SpringBoot 2.7.x引入spring-boot-starter-data-elasticsearch后应用能启动但一旦调用 repository 查询就报NoNodeAvailableException或者ElasticsearchStatusException看底层日志发现客户端版本和服务端 7.2.0 对不上。原因SpringBoot 的自动装配原理决定了它会为ElasticsearchRestTemplate和RestHighLevelClient注入一套版本固定的依赖。SpringBoot 2.7 对应 Spring Data Elasticsearch 4.4这个版本内部用的是 ES 7.17 的 client。虽然它也能通过 REST 协议访问 7.2 的节点但序列化、mapping 处理上有差异某些 API 会直接失败。解决最省心的是放弃这个 starter回到第 2 章那种手动创建RestHighLevelClient的方式。如果你只是因为项目历史包袱必须保留 Spring Data 的 Repository 抽象那就手动改依赖版本并排除 Boot 自动配置dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId exclusions exclusion groupIdorg.springframework.data/groupId artifactIdspring-data-elasticsearch/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.data/groupId artifactIdspring-data-elasticsearch/artifactId version3.2.0/version /dependency就算这样还是建议在启动自检里把 ES 版本打出来确认一遍。版本玄学这东西光靠猜不靠谱。5.2 连接不关闭导致连接池耗尽现象服务运行一段时间后所有 ES 请求一直卡住日志里出现Timeout waiting for connection from pool或Connection pool shut down。原因代码里每次调用都new RestHighLevelClient(...)用完不close()。RestHighLevelClient底层的RestClient持有连接池不关闭就是不可回收的泄漏。更隐蔽的是有人把它写成了方法局部变量却想当然地放在try-with-resources外面释放结果连接没有被归还。解决把它声明为 Spring 的Bean单例注入到各个 Service 里。如果确实要在方法级别创建必须在finally里client.close()但高频调用这样做连接反复建立销毁性能会很难看。生产上我还会在连接池回调里设置空闲 keep-alive 策略避免 ES 端空闲连接被回收之后客户端还拿着已经失效的连接继续发请求builder.setHttpClientConfigCallback(clientBuilder - { clientBuilder.setMaxConnTotal(maxConnTotal) .setMaxConnPerRoute(maxConnPerRoute) .setKeepAliveStrategy((response, context) - 5 * 1000); return clientBuilder; });5.3 返回结果转 Java Bean 时 LocalDateTime 反序列化报错现象ES 里存的是date类型getSourceAsMap()之后字段值是一个String比如2024-05-20 08:00:00。用它转LocalDateTime直接抛InvalidFormatException或者得到的值是Timestamp格式和预期不一致。原因ES 返回的 JSON 里日期就是字符串Java 侧需要自己控制解析格式。SpringBoot 的ObjectMapper如果不注册JavaTimeModule就无法把字符串转成LocalDateTime。解决在配置类里定义一个自定义ObjectMapperBean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); return mapper; }如果你不从 source 转为 Bean而是直接getSourceAsMap()自己取值那就要手动LocalDateTime.parse(value, FORMATTER)。别依赖 ES 自动转成 Java 时间对象REST 接口没这么智能。5.4 Windows 下启动 Elasticsearch 总是自动退出现象Win11 上解压了 elasticsearch-7.2.0双击elasticsearch.bat或直接执行命令CMD 窗口闪一下就没服务起不来。原因最常见的是环境变量里没有配JAVA_HOMEES 脚本找不到 JDK其次是jvm.options里的-Xms1g -Xmx1g对开发机内存不足的情况直接触发启动失败还有一个容易被忽略的就是下载的压缩包路径里带了空格或中文脚本解析路径出错。解决先确认 JDK 版本是 1.8设好JAVA_HOME。然后打开config/jvm.options把堆内存改成 512m-Xms512m -Xmx512m在命令行里手动执行.\bin\elasticsearch.bat不要双击这样能看到真正的报错日志。如果报max virtual memory areas vm.max_map_count之类那是 Linux Kernel 参数的问题Windows 一般不会遇到真遇到了去config/elasticsearch.yml里把bootstrap.memory_lock: false确认一下。还有一次我遇到的情况是之前有个后台进程已经占用了 9200 端口新实例起不来用netstat -ano | findstr 9200查一下就能定位。5.5 索引已有数据后修改 mapping 不生效现象给一个已经写入了三个月数据的索引新增一个 keyword 子字段执行 PUT mapping 返回成功但查询新字段时发现类型还是老的甚至有些字段查出来是 null。原因ES 的 mapping 建立之后已有字段的类型不能改只能新增字段。即便只是把text改成text加一个fields.keyword老字段的映射也不会自动补上。解决正确做法是重建索引。先创建新索引并写好完整 mapping然后用_reindex把老索引数据搬过去ReindexRequest reindexRequest new ReindexRequest(); reindexRequest.setSourceIndices(article_v1); reindexRequest.setDestIndex(article_v2); BulkByScrollResponse response client.reindex(reindexRequest, RequestOptions.DEFAULT); System.out.println(reindex total: response.getTotal());数据量大时reindex是个长任务client.reindex()默认会同步等待超时时间一定要给足更好的方式是用setConflicts(proceed)避免文档版本冲突导致任务中断。老索引确认没问题后再用 alias 把业务读请求切到新索引上这种切换对上游服务基本无感。这也是ES 恢复数据或者迁移索引字段时最稳妥的一条路。6. 收尾的硬功夫连接池调优与写入性能验证6.1 集群连接池三个参数怎么调第 2 章里设置了maxConnTotal和maxConnPerRoute这里再补一个容易被忽略的策略。ES 服务端自己也有 keep-alive默认是 30 秒左右。如果 HttpClient 这边不告诉它这个连接能活多久客户端会一直握着结果服务端已关闭下一次请求才知道连接坏了。所以我在setHttpClientConfigCallback里设置一个 5 秒的 keep-alive比服务端短确保每次请求拿到的都是健康连接。builder.setHttpClientConfigCallback(clientBuilder - { clientBuilder.setKeepAliveStrategy((response, context) - 5 * 1000); return clientBuilder; });maxConnTotal和maxConnPerRoute不是越大越好节点的 CPU 和内存是有限的开太大反而让 ES 线程池排队。常见起步值是总连接 100单路由 50如果你的业务同时只有少量线程在访问那么 30/10 就够。6.2 写入性能的快速验证方法我每接一个和 ES 相关的项目第一件事不是写业务代码而是先做一次 10 分钟的写入压测。原因很简单我吃过一次亏凌晨日志量翻倍结果一次性 bulk 太大ES 集群直接 CPU 拉满业务查询全被拖慢。那种看着没问题一上线就炸的情况基本都是没在真实压力下验证过。压测方法很简单准备 1 万条 Map 数据按 5000 条一批次 bulk 写入分别测默认刷新和refreshwait_for两种策略。默认情况 ES 每 1 秒刷新一次写入吞吐高wait_for会等 refresh 完成再返回写入变慢但数据写入后立刻可见。代码里设置bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.NONE);压测时要关注的不是单次 bulk 耗时而是整体耗时是否线性增长。如果第二批比第一批明显慢可能是段合并或者磁盘 IO 成了瓶颈这时候不是调客户端而是要调 ES 的分片数或refresh_interval。数据量小的时候5 个分片和 1 个分片区别不大数据量上来分片少会拖慢并发。做完这些验证我才敢说这个 SpringBoot 整合 Elasticsearch 7.2.0 的方案能扛住业务压力。连接池的每个参数、批量大小、刷新策略都要拿到真实数据后再定不能靠感觉。希望这篇文章能帮你少走几圈弯路也欢迎你在自己的环境里先把最小例子跑通再往里填业务逻辑。本文还有配套的精品资源点击获取