多线程之ES同步数据

我们用到ES进行快速检索时,首先要做的就是将数据导入到ES中,或是在使用中定期地同步ES与本地数据进行同步。当数据量非常大时,这个操作就相当的耗时间。为了解决这个问题,我们可以采用多线程的方式提高执行的效率。下面我给出示例代码:

思想:首先要分析出这一过程中耗时间的步骤有哪些,既然要同步数据,就要先从数据库中查出最新的数据,然后再将它们更新或写入到ES中。显而易见涉及到IO操作的步骤最耗时间,首先是读操作,这个最容易优化,可以采用多个线程同时读的方式加载数据。写操作需要考虑顺序性和可共同编辑可能出现的问题,而在这个场景中ES会自动根据主键进行排序,所以写操作也可以使用多线程,但是为了示例代码的通用性,这里写操作仅使用一个线程执行,适配其他对顺序有要求有和不允许多个线程同时对一个区域进行编辑的场景(比如多线程导出)。

大致步骤如下

  1. 创建一个阻塞队列,用于从消费者和生产者传递信息
  2. 创建一个计数器标记任务完成状态,提前计算出总页数,初始值就是这个
  3. 单线程写:另开一个线程执行写入,不断从队列中拿数据
  4. 多线程读:已经提前计算出总页数,则发起总页数个数量的任务,查询对应页号的数据,放到队列中,然后将计数器减一
  5. 计数器为零时退出

代码实现:

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的任务,比如同步数据、导出数据、批量推送数据、下载资源、上传资源等等

相关推荐
地衣机房除尘6 小时前
科普漫画:机房数据中心防火小剧场
大数据·运维
海兰6 小时前
离线合同结构化提取与检索:LangExtract + 本地DeepSeek + Elasticsearch 9.x
大数据·elasticsearch·django
你住过的屋檐6 小时前
【Java】虚拟线程详解
java·开发语言
逍遥德6 小时前
Maven教程.02-基础-pom.xml 使用标签大全
java·后端·maven·软件构建
甲枫叶7 小时前
【claude热点资讯】Claude Code 更新:手机遥控电脑开发,Remote Control 功能上线
java·人工智能·智能手机·产品经理·ai编程
额,不知道写啥。7 小时前
P5354 [Ynoi Easy Round 2017] 由乃的 OJ
java·开发语言·算法
TDengine (老段)7 小时前
TDengine IDMP 数据可视化——散点图
大数据·数据库·物联网·信息可视化·时序数据库·tdengine·涛思数据
让我上个超影吧7 小时前
消息队列——RabbitMQ(高级)
java·rabbitmq
得物技术7 小时前
Sentinel Java客户端限流原理解析|得物技术
java·后端·架构
PM老周7 小时前
2026年软硬件一体化项目管理软件怎么选?多款工具对比测评
java·安全·硬件工程·团队开发·个人开发