1. 为什么需要连接多个ElasticSearch数据源?
在微服务架构盛行的今天,一个典型的业务系统往往需要同时对接多个数据存储。就拿我最近接手的电商平台改造项目来说,商品数据存放在AWS上的ElasticSearch集群,用户行为日志存储在本地IDC的ES实例,而风控数据则使用阿里云的ES服务。这种多ES实例并存的场景越来越普遍,主要源于以下实际需求:
业务隔离需求:不同业务域的数据可能需要独立的ES集群。例如核心交易数据要求SSD存储和高规格节点,而日志类数据用普通HDD存储即可。分开部署既能优化成本,又能避免相互干扰。
合规与安全要求:金融、医疗等行业常要求不同安全等级的数据物理隔离。比如用户隐私数据必须存放在内网集群,而公开商品信息可以放在公有云。
技术演进中的过渡方案:在系统迁移过程中,新旧集群往往需要并行运行一段时间。我们去年做ES版本升级时,就经历了长达两个月的双集群并行期。
多租户场景:SaaS类产品通常需要为每个大客户单独部署ES集群,保证性能和数据的独立性。
提示:当你的查询需要跨多个ES集群聚合数据时,应用层整合往往比跨集群查询更可靠。ES官方的CCR(跨集群复制)功能对网络稳定性要求较高,在复杂网络环境下容易出问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实战:SpringBoot多ES数据源配置全流程
2.1 基础环境准备
先看依赖配置。除了标准的SpringBoot Starter外,需要特别注意ElasticSearch客户端的版本匹配:
xml复制<!-- 必须与ES服务端主版本一致 -->
<elasticsearch.version>7.17.3</elasticsearch.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
我强烈建议在pom.xml中显式指定ES版本,避免SpringBoot自动依赖管理带来的版本冲突。曾经有个生产事故就是因为测试环境用7.9.x而生产环境是7.17.x,导致滚动查询API不兼容。
2.2 多数据源配置核心代码
下面是连接两个ES集群的典型配置类。关键点在于:
- 为每个数据源创建独立的RestHighLevelClient
- 使用@Qualifier区分不同的ElasticsearchOperations
java复制@Configuration
public class MultiESConfig {
// 主集群配置
@Bean(name = "primaryClient")
public RestHighLevelClient primaryClient() {
ClientConfiguration config = ClientConfiguration.builder()
.connectedTo("primary.es.com:9200")
.withBasicAuth("user", "password")
.withConnectTimeout(Duration.ofSeconds(5))
.withSocketTimeout(Duration.ofSeconds(30))
.build();
return RestClients.create(config).rest();
}
// 从集群配置
@Bean(name = "secondaryClient")
public RestHighLevelClient secondaryClient() {
ClientConfiguration config = ClientConfiguration.builder()
.connectedTo("secondary.es.com:9200")
.usingSsl() // 启用SSL加密
.withBasicAuth("user", "password")
.withConnectTimeout(Duration.ofSeconds(5))
.build();
return RestClients.create(config).rest();
}
@Bean(name = "primaryTemplate")
public ElasticsearchOperations primaryTemplate(
@Qualifier("primaryClient") RestHighLevelClient client) {
return new ElasticsearchRestTemplate(client);
}
@Bean(name = "secondaryTemplate")
public ElasticsearchOperations secondaryTemplate(
@Qualifier("secondaryClient") RestHighLevelClient client) {
return new ElasticsearchRestTemplate(client);
}
}
2.3 实际使用中的注入方式
在Repository或Service层使用时,通过@Qualifier指定要操作的数据源:
java复制@Repository
public class ProductRepository {
private final ElasticsearchOperations primaryOps;
private final ElasticsearchOperations secondaryOps;
public ProductRepository(
@Qualifier("primaryTemplate") ElasticsearchOperations primaryOps,
@Qualifier("secondaryTemplate") ElasticsearchOperations secondaryOps) {
this.primaryOps = primaryOps;
this.secondaryOps = secondaryOps;
}
public void saveToBoth(Product product) {
primaryOps.save(product);
secondaryOps.save(product); // 双写场景
}
}
3. 生产环境中的关键问题与解决方案
3.1 连接池优化配置
ES客户端默认的连接池配置可能无法满足高并发需求,需要针对性调整:
java复制@Bean(name = "primaryClient")
public RestHighLevelClient primaryClient() {
RestClientBuilder builder = RestClient.builder(
new HttpHost("primary.es.com", 9200))
.setHttpClientConfigCallback(httpClientBuilder -> {
// 关键连接池参数
httpClientBuilder.setMaxConnTotal(100); // 最大连接数
httpClientBuilder.setMaxConnPerRoute(50); // 每路由最大连接数
httpClientBuilder.setDefaultIOReactorConfig(
IOReactorConfig.custom()
.setIoThreadCount(Runtime.getRuntime().availableProcessors())
.build());
return httpClientBuilder;
});
return new RestHighLevelClient(builder);
}
注意:MaxConnPerRoute应该根据实际ES节点数量调整。如果是3节点集群,建议设置为总连接数的1/3左右,避免单个节点过载。
3.2 跨集群事务一致性处理
在双写场景下,如何保证数据一致性是个难题。我们采用的解决方案是:
- 本地事务表+定时补偿:
java复制@Transactional
public void saveProduct(Product product) {
// 先保存到主集群
primaryOps.save(product);
// 记录到本地事务表
transactionLogRepository.save(
new EsSyncLog(product.getId(), "product", "CREATE"));
}
// 定时任务补偿
@Scheduled(fixedDelay = 60000)
public void syncToSecondary() {
List<EsSyncLog> logs = transactionLogRepository.findUnsynced();
logs.forEach(log -> {
try {
Object entity = getEntity(log.getEntityType(), log.getEntityId());
secondaryOps.save(entity);
log.markAsSynced();
} catch (Exception e) {
log.retry();
}
});
transactionLogRepository.saveAll(logs);
}
- 最终一致性监控:通过Elasticsearch的_seq_no和_primary_term字段比对两个集群的数据差异,开发了专门的校验工具。
4. 高级应用场景与性能优化
4.1 读写分离实现
对于读多写少的场景,可以采用主写从读的模式:
java复制public class ProductService {
@Qualifier("primaryTemplate")
private ElasticsearchOperations writeTemplate;
@Qualifier("secondaryTemplate")
private ElasticsearchOperations readTemplate;
public Product getProduct(String id) {
// 从只读集群查询
return readTemplate.get(id, Product.class);
}
public void saveProduct(Product product) {
// 只写入主集群
writeTemplate.save(product);
}
}
4.2 多集群数据聚合查询
当需要合并多个集群的查询结果时,可以采用并行查询+内存聚合的方式:
java复制public SearchHits aggregateFromClusters(Query query) {
// 并行查询两个集群
CompletableFuture<SearchHits> primaryFuture = CompletableFuture.supplyAsync(
() -> primaryTemplate.search(query, Product.class));
CompletableFuture<SearchHits> secondaryFuture = CompletableFuture.supplyAsync(
() -> secondaryTemplate.search(query, Product.class));
// 合并结果
return CompletableFuture.allOf(primaryFuture, secondaryFuture)
.thenApply(v -> {
SearchHits combined = new SearchHits(
Stream.concat(
Arrays.stream(primaryFuture.join().getSearchHits()),
Arrays.stream(secondaryFuture.join().getSearchHits())
).toArray(SearchHit[]::new),
primaryFuture.join().getTotalHits() + secondaryFuture.join().getTotalHits(),
primaryFuture.join().getMaxScore()
);
return combined;
}).join();
}
4.3 连接健康监测与熔断
引入Resilience4j实现自动熔断:
java复制@CircuitBreaker(name = "esPrimary", fallbackMethod = "fallbackSearch")
public SearchHits searchPrimary(Query query) {
return primaryTemplate.search(query, Product.class);
}
private SearchHits fallbackSearch(Query query, Exception e) {
// 降级到从集群查询
return secondaryTemplate.search(query, Product.class);
}
对应的配置:
yaml复制resilience4j.circuitbreaker:
instances:
esPrimary:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 50
minimumNumberOfCalls: 10
permittedNumberOfCallsInHalfOpenState: 5
waitDurationInOpenState: 10s
failureRateThreshold: 50
5. 监控与运维实践
5.1 关键指标监控
建议监控以下ES客户端指标:
-
连接池状态:
- active_connections
- idle_connections
- total_connections
-
请求指标:
- request_count
- request_timeouts
- request_failures
-
线程阻塞情况:
- thread_pool_queue_size
- thread_pool_active_threads
我们使用Micrometer对接Prometheus的配置示例:
java复制@Bean
public MeterRegistryCustomizer<PrometheusMeterRegistry> esMetrics() {
return registry -> {
new ElasticsearchMetricsMonitor(primaryClient().getLowLevelClient())
.bindTo(registry);
new ElasticsearchMetricsMonitor(secondaryClient().getLowLevelClient())
.bindTo(registry);
};
}
5.2 日志排查技巧
ES客户端日志需要特别关注以下模式:
- 连接失败:
code复制org.elasticsearch.client.RestClient - request [GET http://es:9200/_cluster/health] failed
java.net.ConnectException: Connection refused
- 慢查询警告:
code复制took [2156ms] took_millis [2156] ...
建议的日志配置:
yaml复制logging:
level:
org.elasticsearch.client: DEBUG
org.apache.http: WARN
org.apache.http.wire: ERROR # 生产环境建议关闭wire日志
6. 迁移与升级策略
当需要切换ES集群时,采用双写+流量渐进的方案:
- 阶段一:新集群只接收写入,不参与查询
- 阶段二:逐步将只读流量切换到新集群
- 阶段三:全量切换写入和查询
- 阶段四:旧集群作为灾备保留一段时间
对应的版本回滚方案:
java复制public void saveProduct(Product product) {
try {
newClusterOps.save(product); // 先写入新集群
oldClusterOps.save(product); // 再写入旧集群
} catch (Exception e) {
metrics.counter("write.failure").increment();
throw e;
}
}
我在实际迁移中发现,数据量超过1TB时,最好配合Snapshot API进行全量迁移,再通过上述双写方案同步增量数据。
