
我们用到ES进行快速检索时,首先要做的就是将数据导入到ES中,或是在使用中定期地同步ES与本地数据进行同步。当数据量非常大时,这个操作就相当的耗时间。为了解决这个问题,我们可以采用多线程的方式提高执行的效率。下面我给出示例代码:
思想:首先要分析出这一过程中耗时间的步骤有哪些,既然要同步数据,就要先从数据库中查出最新的数据,然后再将它们更新或写入到ES中。显而易见涉及到IO操作的步骤最耗时间,首先是读操作,这个最容易优化,可以采用多个线程同时读的方式加载数据。写操作需要考虑顺序性和可共同编辑可能出现的问题,而在这个场景中ES会自动根据主键进行排序,所以写操作也可以使用多线程,但是为了示例代码的通用性,这里写操作仅使用一个线程执行,适配其他对顺序有要求有和不允许多个线程同时对一个区域进行编辑的场景(比如多线程导出)。
大致步骤如下:
- 创建一个阻塞队列,用于从消费者和生产者传递信息
- 创建一个计数器标记任务完成状态,提前计算出总页数,初始值就是这个
- 单线程写:另开一个线程执行写入,不断从队列中拿数据
- 多线程读:已经提前计算出总页数,则发起总页数个数量的任务,查询对应页号的数据,放到队列中,然后将计数器减一
- 计数器为零时退出
代码实现:
java
public AjaxResult syncES() throws IOException, InterruptedException {
// 同步ES;
// 1. 参数设置
int pageSize = 50; // 每页大小
Long total = postMapper.selectCount(new LambdaQueryWrapper<>(Posts.class).eq(Posts::getStatus, PostStatus.PUBLISHED.getCode()));
long totalPages = (total + pageSize - 1) / pageSize; // 计算总页数
// 结果
StringBuffer processLog = new StringBuffer();
// 初始化线程间通信组件
// 有界队列,用于生产者和消费者之间传递数据
BlockingQueue<List<ESPost>> dataQueue = new LinkedBlockingQueue<>((int) totalPages);
// 计数器,用于等待所有数据查询任务完成
CountDownLatch countDownLatch = new CountDownLatch((int) totalPages);
Thread syncThread = new Thread(() -> {
try {
long handleCount = 0;
while (handleCount < totalPages) {
BulkRequest bulkRequest = new BulkRequest();
List<ESPost> docs = dataQueue.poll(2, TimeUnit.SECONDS);
for (ESPost doc : docs) {
IndexRequest request = new IndexRequest("posts_index")
.id(doc.getId().toString())
.source(objectMapper.writeValueAsString(doc), XContentType.JSON);
bulkRequest.add(request);
}
BulkResponse response = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
handleCount++;
if (response.hasFailures()) {
log.warn("批量同步部分失败:{}", response.buildFailureMessage());
processLog.append("批量同步部分失败:").append(response.buildFailureMessage()).append("\n");
} else {
log.info("批次批量同步成功,共:{}", docs.size());
processLog.append("批次批量同步成功,共:").append(docs.size()).append("\n");
}
}
} catch (Exception e) {
log.error("数据同步出现错误", e);
processLog.append("数据同步出现错误").append(e.getMessage()).append("\n");
}
});
syncThread.start();
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
final int currentPage = pageNum;
exportTaskExecutor.execute(() -> {
try {
int offset = (currentPage - 1) * pageSize;
log.info("开始查询第 {} 页数据, offset: {}", currentPage, offset);
Page<ESPost> page = new Page<>(currentPage, pageSize);
List<ESPost> pageData = postMapper.selectAllPostWithContent(page).getRecords();
// 处理正文
// posts.forEach(post -> post.setContent(HtmlUtil.removeHtmlTag(post.getContent())));
// 等级赋值
pageData.forEach(post -> {
post.setAuthorLevel(getUserLevel(post.getExp()));
});
// 将查询到的数据放入队列
dataQueue.put(pageData);
log.info("第 {} 页数据查询完成,共 {} 条", currentPage, pageData.size());
} catch (Exception e) {
log.error("查询第 {} 页数据时发生异常", currentPage, e);
} finally {
// 无论成功与否,都需要计数减一
countDownLatch.countDown();
}
});
}
countDownLatch.await();
// 等待同步线程完成
syncThread.join();
return AjaxResult.success("同步完成", processLog.toString());
}
线程池配置类
java
@Configuration
public class ThreadPoolConfig {
@Bean("exportTaskExecutor")
public ThreadPoolTaskExecutor exportTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数,根据你的服务器CPU核心数调整(通常建议为核心数*2)
executor.setCorePoolSize(4);
// 最大线程数,不宜过高,需考虑数据库连接池大小
executor.setMaxPoolSize(8);
// 队列容量,用于缓冲
executor.setQueueCapacity(100);
// 线程空闲存活时间
executor.setKeepAliveSeconds(30);
executor.setThreadNamePrefix("export-thread-");
// 拒绝策略:由调用者线程执行该任务,避免任务丢失
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
这样执行速度可以提升至单线程执行时的n倍以上,n取决于你的最大线程数和你的cpu核心数。
同样的这套架子也可以应用在其他许多场景中,只要涉及到 长时间IO的任务,比如同步数据、导出数据、批量推送数据、下载资源、上传资源等等