🚀 引言:搭建方式决定了你后面踩坑的频率
很多人第一次用 ES,往往是直接去官网下一个压缩包,解压、改配置、启动------几分钟就能跑起来,感觉很顺利。但在实际项目里,这种方式埋下了无数隐患:
- 单节点没有副本,一旦宕机数据丢失;
- 环境配置和生产差异太大,开发没问题,上线一堆报错;
- ES 8.x 默认开启了安全认证,不了解就无从下手;
- 新旧 Java 客户端并存,稍不注意就用了已废弃的 API。
本篇的目标是用一套标准化、可复用的方式把 ES 环境搭起来,同时把 Spring Boot 整合到一个可以直接用于生产参考的状态。中间所有踩过的坑,都会明确标出来。
一、版本选型:ES 8.x 还是 7.x?
在动手之前,先把版本问题说清楚。目前(2024年)主流有两个大版本并存:
| 对比项 | ES 7.x | ES 8.x |
|---|---|---|
| 安全默认 | 默认关闭,需手动开启 | 默认开启(HTTPS + 用户认证) |
| Java 客户端 | RestHighLevelClient(官方维护) | ElasticsearchClient(新官方推荐) |
| 向量检索 | 有限支持 | 成熟支持(dense_vector + kNN) |
| 许可证 | Apache 2.0 | Elastic License 2.0(核心功能免费) |
| 生产推荐 | 老项目维护 | 新项目首选 |
结论:新项目一律用 ES 8.x。 本系列全程基于 ES 8.13.x(写作时的最新稳定版)。
唯一的心理负担是 ES 8.x 默认强制 HTTPS 和用户认证,初次上手会觉得麻烦。但这恰恰是生产环境的正确姿态,本篇会完整演示如何处理。
二、用 Docker Compose 搭建三节点集群
为什么用 Docker Compose 而不是直接安装二进制包?
原因很简单:环境一致性。Docker Compose 定义的集群配置是版本化的,可以 git 管理,可以一键销毁重建,开发、测试、CI 环境完全一致。直接安装二进制包的方式在多人协作时迟早会出现"我这里好的,你那里不行"的经典问题。
2.1 准备工作:Linux 内核参数
这是 ES 最常见的第一个报错,在启动之前就要处理好。ES 要求虚拟内存映射数量至少 262144,Linux 默认值只有 65530。
bash
# 临时生效(重启后失效)
sudo sysctl -w vm.max_map_count=262144
# 永久生效(重启后仍有效)
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
⚠️ 踩坑 :如果跳过这一步直接启动,ES 节点会立刻报错退出,日志里出现
max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]。在 Docker Desktop(Mac/Windows)下因为 Docker VM 内部已设置过,通常不会报这个错;但在 Linux 服务器上必须手动配置。
2.2 完整的 docker-compose.yml
下面是一个经过生产验证的三节点集群配置。先完整给出,再逐块解释关键点:
yaml
# docker-compose.yml
version: "3.9"
# 所有节点共享的公共配置,通过 YAML 锚点复用,避免重复
x-es-common: &es-common
image: elasticsearch:8.13.0
environment:
- cluster.name=es-saas-cluster # 集群名称,节点靠这个找到彼此
- discovery.seed_hosts=es-node1,es-node2,es-node3
- cluster.initial_master_nodes=es-node1,es-node2,es-node3
- bootstrap.memory_lock=true # 锁定内存,防止 ES 内存被 swap 到磁盘
- xpack.security.enabled=true # 开启安全认证(8.x 默认已开启,显式声明更清晰)
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.truststore.path=certs/es-node.p12
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
networks:
- es-net
services:
# ── 证书生成工具(一次性任务,生成后会自动退出)──────────────────
es-setup:
image: elasticsearch:8.13.0
command: >
bash -c '
if [ ! -f /certs/es-node.p12 ]; then
bin/elasticsearch-certutil cert \
--silent --pem=false \
--in /config/instances.yml \
--out /certs/es-node.zip \
--pass ""
cd /certs && unzip es-node.zip
bin/elasticsearch-certutil cert \
--silent \
--name es-node \
--dns es-node1,es-node2,es-node3,localhost \
--ip 127.0.0.1 \
--out /certs/es-node.p12 \
--pass ""
chmod 660 /certs/es-node.p12
echo "证书生成完毕"
else
echo "证书已存在,跳过生成"
fi
'
volumes:
- certs:/certs
networks:
- es-net
# ── 节点1:Master 候选 + Data ─────────────────────────────────────
es-node1:
<<: *es-common
container_name: es-node1
environment:
- node.name=es-node1
- ES_JAVA_OPTS=-Xms1g -Xmx1g # 开发环境 1g,生产按物理内存50%设置
# 继承公共环境变量(YAML 合并键)
- cluster.name=es-saas-cluster
- discovery.seed_hosts=es-node1,es-node2,es-node3
- cluster.initial_master_nodes=es-node1,es-node2,es-node3
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.truststore.path=certs/es-node.p12
volumes:
- es-data1:/usr/share/elasticsearch/data
- certs:/usr/share/elasticsearch/config/certs
ports:
- "9200:9200" # 对外暴露 HTTP 端口(仅 node1,通过 Coordinating 转发其他节点请求)
depends_on:
es-setup:
condition: service_completed_successfully
# ── 节点2:Master 候选 + Data ─────────────────────────────────────
es-node2:
<<: *es-common
container_name: es-node2
environment:
- node.name=es-node2
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- cluster.name=es-saas-cluster
- discovery.seed_hosts=es-node1,es-node2,es-node3
- cluster.initial_master_nodes=es-node1,es-node2,es-node3
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.truststore.path=certs/es-node.p12
volumes:
- es-data2:/usr/share/elasticsearch/data
- certs:/usr/share/elasticsearch/config/certs
# ── 节点3:Master 候选 + Data ─────────────────────────────────────
es-node3:
<<: *es-common
container_name: es-node3
environment:
- node.name=es-node3
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- cluster.name=es-saas-cluster
- discovery.seed_hosts=es-node1,es-node2,es-node3
- cluster.initial_master_nodes=es-node1,es-node2,es-node3
- bootstrap.memory_lock=true
- xpack.security.enabled=true
- xpack.security.http.ssl.enabled=true
- xpack.security.http.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.enabled=true
- xpack.security.transport.ssl.keystore.path=certs/es-node.p12
- xpack.security.transport.ssl.truststore.path=certs/es-node.p12
volumes:
- es-data3:/usr/share/elasticsearch/data
- certs:/usr/share/elasticsearch/config/certs
# ── Kibana:可视化管理界面 ─────────────────────────────────────────
kibana:
image: kibana:8.13.0 # 版本必须与 ES 完全一致,否则会启动失败
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=https://es-node1:9200
- ELASTICSEARCH_USERNAME=kibana_system
- ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD} # 从 .env 文件读取
- ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=/certs/ca/ca.crt
ports:
- "5601:5601"
volumes:
- certs:/certs
networks:
- es-net
depends_on:
- es-node1
networks:
es-net:
driver: bridge
volumes:
certs:
es-data1:
es-data2:
es-data3:
这个配置的核心逻辑,有几点值得解释:
cluster.initial_master_nodes 只用一次。 这个配置只在集群第一次启动时生效,用于告诉 ES "这些节点是初始 master 候选,不要等其他节点"。集群组建完成后,这个配置就没有意义了------但也不会造成问题,保留着就好。很多人看到"initial"误以为每次启动都要配,其实不是。
bootstrap.memory_lock=true 必须配合 ulimits。 锁内存可以防止操作系统把 ES 的堆内存 swap 到磁盘,一旦发生 swap,ES 的延迟会急剧上升甚至宕机。但光设置这个环境变量不够,还必须同时放开 memlock 的 ulimit 限制,两者缺一不可。
Kibana 版本必须与 ES 完全一致。 这是一个非常容易踩的坑。如果 Kibana 是 8.12,ES 是 8.13,Kibana 会一直报 Kibana server is not ready yet,检查日志才发现版本不匹配。永远保持两者版本号字符串完全相同。
2.3 启动集群
bash
# 第一步:生成 elastic 用户的初始密码(首次启动后执行)
docker compose up es-setup
docker compose up -d es-node1 es-node2 es-node3
# 等待集群健康(大约 30-60 秒)
# 然后重置 elastic 用户密码
docker exec -it es-node1 bin/elasticsearch-reset-password -u elastic
# 将密码写入 .env 文件(供 Kibana 和应用使用)
echo "ELASTIC_PASSWORD=你的密码" >> .env
echo "KIBANA_PASSWORD=kibana的密码" >> .env
# 启动 Kibana
docker compose up -d kibana
验证集群是否正常:
bash
curl -u elastic:你的密码 \
--cacert ./certs/ca/ca.crt \
https://localhost:9200/_cluster/health?pretty
期望看到 "status": "green",以及 "number_of_nodes": 3。
⚠️ 踩坑 :如果看到
"status": "yellow",不要慌,这通常是因为集群刚启动,.kibana等系统索引默认有 1 个副本,但副本还没分配完成。等 1-2 分钟自然变绿。如果长时间是 yellow,运行GET /_cluster/allocation/explain查看原因。
三、Java 客户端的历史包袱与正确选择
这是本篇另一个重点,也是很多团队在 ES 8.x 升级时卡住的地方。
ES 的 Java 客户端经历了几次大变迁:
bash
ES 2.x 时代:TransportClient(基于TCP,ES 7.x废弃,8.x彻底移除)
↓
ES 6.x 时代:RestHighLevelClient(基于HTTP,7.x主力,8.x标记废弃)
↓
ES 7.15+ :ElasticsearchClient(新官方Java客户端,强类型,8.x主力)
↓
Spring Data ES 4.4+:底层换成新客户端,提供更高层的 Repository 抽象
如果你的项目是新项目,直接用 ElasticsearchClient,跳过 RestHighLevelClient。 后者虽然在 8.x 里还能用(靠兼容层),但官方已经不再主动维护,迟早要迁移。
如果你维护的是老项目,大量使用了 RestHighLevelClient,也不用急着迁,等到下次大版本升级时一起处理。
四、Spring Boot 3.x 整合 ElasticsearchClient
4.1 依赖配置
xml
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot 3.x 整合 ES(底层使用新版 ElasticsearchClient)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Lombok,减少样板代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson,处理 JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
⚠️ 踩坑 :Spring Boot 3.x 对应 Spring Data ES 5.x,底层已经切换到新的
ElasticsearchClient。但如果你的项目是从 Spring Boot 2.x 升级过来的,老代码里如果有RestHighLevelClient的注入,会直接报 Bean 找不到的错误------因为新版 Spring Data ES 不再自动装配RestHighLevelClient,需要手动引入elasticsearch-rest-high-level-client依赖。这种兼容性问题在升级时很常见,建议升级前先读 Spring Data ES 的 Migration Guide。
4.2 application.yml 配置
yaml
# application.yml
spring:
elasticsearch:
uris: https://localhost:9200 # 注意:ES 8.x 默认 HTTPS,不要写 http
username: elastic
password: ${ELASTIC_PASSWORD} # 从环境变量读取,不要硬编码在配置文件里
socket-timeout: 30s
connection-timeout: 5s
# 如果是开发环境,可以临时信任所有证书(生产环境绝不能这样做!)
# 正确做法见下方 JavaConfig
仅靠 application.yml 还不够,因为 ES 8.x 的 HTTPS 证书是自签名的,Java 的 SSL 默认不信任自签名证书。有两种处理方式:
方式一(开发推荐):禁用 SSL 验证
这种方式只在本地开发环境使用,绝对不要带到生产:
java
// EsConfig.java
@Configuration
public class EsConfig {
@Value("${spring.elasticsearch.uris}")
private String esUri;
@Value("${spring.elasticsearch.username}")
private String username;
@Value("${spring.elasticsearch.password}")
private String password;
/**
* 开发环境:绕过 SSL 验证。
* 生产环境请使用方式二,加载真实证书。
*
* 为什么要这么做?
* ES 8.x 默认生成的是自签名证书,Java 的 TrustManager 默认只信任
* CA 签发的证书,自签名证书会导致 SSLHandshakeException。
* 在开发阶段,绕过 SSL 验证是最简单的解决方案。
*/
@Bean
@Profile("dev")
public ElasticsearchClient elasticsearchClientDev() throws Exception {
// 创建信任所有证书的 SSLContext(仅开发)
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(null, (chain, authType) -> true) // 信任所有证书
.build();
RestClient restClient = RestClient.builder(HttpHost.create(esUri))
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder
.setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.setDefaultCredentialsProvider(credentialsProvider())
)
.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
private CredentialsProvider credentialsProvider() {
BasicCredentialsProvider provider = new BasicCredentialsProvider();
provider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));
return provider;
}
}
方式二(生产推荐):加载 CA 证书
java
@Bean
@Profile("prod")
public ElasticsearchClient elasticsearchClientProd() throws Exception {
// 从 Docker volume 挂载路径加载 CA 证书
Path caCertPath = Paths.get("/app/certs/ca/ca.crt");
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(caCertPath.toFile(), null, TrustAllStrategy.INSTANCE)
.build();
// 或者使用 KeyStore 加载 p12 格式
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream is = Files.newInputStream(Paths.get("/app/certs/es-node.p12"))) {
keyStore.load(is, "".toCharArray()); // 我们生成证书时密码为空
}
RestClient restClient = RestClient.builder(HttpHost.create(esUri))
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder
.setSSLContext(sslContext)
.setDefaultCredentialsProvider(credentialsProvider())
)
.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
return new ElasticsearchClient(transport);
}
4.3 定义实体类
java
// Product.java
// 这是 ES 文档对应的 Java 实体类
// @Document 注解告诉 Spring Data ES 这个类对应哪个索引
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "products")
public class Product {
@Id
private String id;
// @Field 指定 ES 中的字段类型和分词器
// text 类型用于全文检索,keyword 子字段用于精确匹配和排序
@Field(type = FieldType.Text, analyzer = "standard",
searchAnalyzer = "standard")
private String name;
@Field(type = FieldType.Keyword)
private String brand;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Text)
private String description;
@Field(type = FieldType.Keyword)
private List<String> tags;
@Field(type = FieldType.Integer)
private Integer stock;
// 日期字段必须显式声明格式,否则 ES 和 Java 的日期格式可能不兼容
@Field(type = FieldType.Date,
format = DateFormat.date_hour_minute_second)
private LocalDateTime createdAt;
}
⚠️ 踩坑 :
LocalDateTime与 ES 的date类型序列化是个经典坑。ES 的日期字段默认接受 ISO 8601 格式,但 Jackson 默认会把LocalDateTime序列化成数组[2024,1,15,8,0,0],ES 无法识别。解决方案是在@Field注解里明确指定format,同时确保 Jackson 的JavaTimeModule已注册。Spring Boot 自动配置通常已经处理了这个问题,但如果自定义了ObjectMapper,要记得重新注册时间模块。
4.4 Repository 接口------最简单的 CRUD
java
// ProductRepository.java
// 继承 ElasticsearchRepository 即可得到基础 CRUD 和分页能力
// 这是 Spring Data ES 提供的高层抽象,底层调用 ElasticsearchClient
@Repository
public interface ProductRepository
extends ElasticsearchRepository<Product, String> {
// Spring Data 根据方法名自动生成查询,类似 JPA
// 这里会生成 term 查询(精确匹配 brand 字段)
List<Product> findByBrand(String brand);
// 价格范围查询
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
// 多条件组合(and 连接)
Page<Product> findByCategoryAndPriceLessThan(
String category, Double maxPrice, Pageable pageable);
}
Repository 方式虽然简洁,但有个明显局限:复杂的 bool 查询、聚合、高亮等都无法通过方法名表达。这时候需要直接使用 ElasticsearchClient。
4.5 直接使用 ElasticsearchClient------处理复杂场景
java
// ProductSearchService.java
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductSearchService {
private final ElasticsearchClient esClient;
/**
* 综合搜索接口:关键词全文检索 + 品牌过滤 + 价格范围
*
* 为什么要把 keyword 过滤放在 filter 而不是 must 里?
* filter 不参与相关性计算,结果会被缓存,性能比 must 好很多。
* must 用于需要影响评分的条件,filter 用于纯粹的过滤条件。
*/
public SearchResult<Product> search(ProductSearchRequest request) throws IOException {
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
// 分页
.from(request.getPage() * request.getSize())
.size(request.getSize())
// 查询体
.query(q -> q
.bool(b -> {
// must:全文检索(影响相关性评分)
if (StringUtils.hasText(request.getKeyword())) {
b.must(m -> m
.multiMatch(mm -> mm
.query(request.getKeyword())
.fields("name^3", "description", "brand^2")
// ^3 表示 name 字段权重是其他字段的3倍
// 这样相同关键词出现在商品名里的结果排在前面
)
);
}
// filter:精确过滤(不影响评分,走缓存)
if (StringUtils.hasText(request.getBrand())) {
b.filter(f -> f
.term(t -> t
.field("brand")
.value(request.getBrand())
)
);
}
if (request.getMinPrice() != null || request.getMaxPrice() != null) {
b.filter(f -> f
.range(r -> {
r.field("price");
if (request.getMinPrice() != null) {
r.gte(JsonData.of(request.getMinPrice()));
}
if (request.getMaxPrice() != null) {
r.lte(JsonData.of(request.getMaxPrice()));
}
return r;
})
);
}
// filter:只展示有库存的商品
b.filter(f -> f
.range(r -> r
.field("stock")
.gt(JsonData.of(0))
)
);
return b;
})
)
// 高亮:搜索结果中关键词飘红
.highlight(h -> h
.fields("name", hf -> hf
.preTags("<em class='highlight'>")
.postTags("</em>")
.numberOfFragments(0) // 0 表示返回整个字段,不截断
)
.fields("description", hf -> hf
.preTags("<em class='highlight'>")
.postTags("</em>")
.numberOfFragments(3)
.fragmentSize(150)
)
)
// 排序
.sort(sort -> sort
.field(f -> f
.field("_score")
.order(SortOrder.Desc)
)
),
Product.class
);
// 解析响应,把高亮内容合并到结果里
List<ProductVO> hits = response.hits().hits().stream()
.map(hit -> {
Product product = hit.source();
ProductVO vo = ProductVO.fromProduct(product);
// 如果有高亮,用高亮内容替换原始内容
if (hit.highlight().containsKey("name")) {
vo.setNameHighlight(hit.highlight().get("name").get(0));
}
return vo;
})
.collect(Collectors.toList());
long total = response.hits().total().value();
return new SearchResult<>(hits, total, request.getPage(), request.getSize());
}
/**
* 索引单个商品文档
*
* 注意:ES 的"更新"操作本质是删除旧文档 + 写入新文档。
* 如果你只需要更新部分字段,用 updateProduct 方法(内部调用 _update API),
* 比整个文档重新写入要高效得多,因为不需要重新索引所有字段。
*/
public String indexProduct(Product product) throws IOException {
IndexResponse response = esClient.index(i -> i
.index("products")
.id(product.getId())
.document(product)
);
log.info("商品 [{}] 索引完成,版本号: {}", product.getId(), response.version());
return response.id();
}
/**
* 部分更新:只更新价格和库存
*
* 为什么要用 _update 而不是整个文档重写?
* 因为商品的 name、description 等字段需要分词索引,重建成本高。
* _update 只更新指定字段,Lucene 层面依然是删除+重写,
* 但网络传输的数据量小很多,对实时性要求高的场景(如库存更新)更合适。
*/
public void updateProductPrice(String productId, Double newPrice) throws IOException {
esClient.update(u -> u
.index("products")
.id(productId)
.doc(Map.of("price", newPrice)),
Product.class
);
}
}
4.6 集群健康检查接口
这是上线前必须做的基础设施验证,建议加入应用的 Actuator 或者 HealthCheck 接口:
java
// EsHealthController.java
@RestController
@RequestMapping("/actuator/es")
@RequiredArgsConstructor
public class EsHealthController {
private final ElasticsearchClient esClient;
@GetMapping("/health")
public ResponseEntity<Map<String, Object>> health() {
try {
HealthResponse health = esClient.cluster().health(h -> h);
Map<String, Object> result = new HashMap<>();
result.put("status", health.status().jsonValue()); // green/yellow/red
result.put("numberOfNodes", health.numberOfNodes());
result.put("numberOfDataNodes", health.numberOfDataNodes());
result.put("activePrimaryShards", health.activePrimaryShards());
result.put("unassignedShards", health.unassignedShards());
// 如果集群是 red,返回 503
if (health.status() == HealthStatus.Red) {
return ResponseEntity.status(503).body(result);
}
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.status(503)
.body(Map.of("error", "无法连接到 ES 集群: " + e.getMessage()));
}
}
}
五、SaaS 视角:多租户场景下的客户端设计
在 SaaS 应用里,所有租户共用同一个 ES 集群(通常情况下),客户端设计需要考虑以下问题:
是否需要多个 ElasticsearchClient 实例?
一般情况下,一个 ElasticsearchClient 实例就够了。它内部维护了一个连接池,线程安全,可以并发使用。只有在以下场景才需要多实例:
- 不同租户对应不同的 ES 集群(强隔离方案)
- 需要用不同的 ES 账号访问(行级权限场景)
连接池与超时配置:
java
RestClient restClient = RestClient.builder(
HttpHost.create("https://es-node1:9200"),
HttpHost.create("https://es-node2:9200"),
HttpHost.create("https://es-node3:9200") // 多节点,自动负载均衡
)
.setHttpClientConfigCallback(httpClientBuilder ->
httpClientBuilder
.setMaxConnTotal(200) // 连接池最大连接数
.setMaxConnPerRoute(50) // 每个节点最大连接数
.setDefaultCredentialsProvider(credentialsProvider())
// 连接超时:建立 TCP 连接的最大等待时间
.setDefaultIOReactorConfig(
IOReactorConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(5))
.build()
)
)
.setRequestConfigCallback(requestConfigBuilder ->
requestConfigBuilder
.setSocketTimeout(30000) // 等待响应的最大时间(ms),慢查询要适当放大
.setConnectTimeout(5000)
)
.build();
六、常见问题 FAQ
Q: 为什么 Spring Boot 启动时报 SSLHandshakeException?
A: ES 8.x 使用自签名证书,Java 默认不信任。解决方案有三种:(1)按本文 4.2 节加载 CA 证书;(2)开发环境使用 TrustAllStrategy 绕过验证;(3)将 ES 的 HTTP SSL 关闭(仅用于内网开发,xpack.security.http.ssl.enabled=false,但 Transport 层 SSL 仍建议保留)。
Q: RestHighLevelClient 和 ElasticsearchClient 能同时存在吗?
A: 可以。在升级过渡期,可以通过手动引入 elasticsearch-rest-high-level-client 依赖来保留老客户端,同时使用新客户端开发新功能。但要注意两者的 Bean 名称可能冲突,需要手动命名区分。
Q: ES 的版本和 Spring Boot 的版本如何对应?
A: Spring Boot 3.2.x → Spring Data ES 5.2.x → 支持 ES 8.x。Spring Boot 2.7.x → Spring Data ES 4.4.x → 支持 ES 7.x/8.x(有限)。建议查阅 Spring Data ES 官方兼容矩阵。
Q: 本地开发时,能不能直接关闭 ES 8.x 的安全认证?
A: 可以,但不推荐。关闭方式是设置 xpack.security.enabled=false。不推荐的原因是这样你的开发代码与生产环境配置差异太大,容易在上线时出现认证相关的问题。更好的做法是接受学习曲线,把安全配置从一开始就做好。
📋 本篇小结
| 知识点 | 关键结论 |
|---|---|
| 版本选型 | 新项目用 ES 8.x,拥抱默认安全配置 |
| Docker Compose | 标准化环境,version 必须全组件一致 |
| 内核参数 | vm.max_map_count=262144 必须提前设置 |
| Java 客户端 | 新项目用 ElasticsearchClient,弃用 RestHighLevelClient |
| SSL 处理 | 开发绕过验证,生产加载 CA 证书 |
| 连接池 | 单实例足够,配置超时和最大连接数 |
| 查询 vs 过滤 | 精确条件放 filter,全文检索放 must |
🔗 上一篇
第01篇 :《Elasticsearch 核心概念与架构全解析------写给每一个想搞懂搜索引擎的开发者》
🔗 下一篇
第03篇 :《深入 Mapping 与数据类型设计------ES Schema 设计避坑指南》
Mapping 设计是 ES 项目中最容易"一错定终身"的环节。text 和 keyword 到底有什么区别?object 和 nested 什么时候该用哪个?主分片数设错了怎么不停机迁移?下一篇全部讲透。
💬 遇到启动报错? 欢迎在评论区贴出错误日志,大概率是证书或内核参数问题,一起排查。