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

相关推荐
tianyuanwo2 小时前
Jenkins节点编码环境深度解析:从配置到Java Web连接原理
java·jenkins·语言编码
A-刘晨阳2 小时前
2026年时序数据库选型指南:从大数据视角深度解析Apache IoTDB的技术优势与实践路径
大数据·apache·时序数据库
CodeToGym2 小时前
【全栈进阶】Spring Boot 整合 WebSocket 实战:从实时告警到金融行情推送
java·后端·spring
张3蜂2 小时前
java springboot2.0 api ;.netcore8 api ;python GunicornAPI ,哪种更强?请从多个维度,对比分析
java·python·.netcore
ba_pi2 小时前
每天写点什么2026-02-04(2.2)新一代技术
大数据·软考
市场部需要一个软件开发岗位2 小时前
一个无人机平台+算法监督平台的离线部署指南
java·python·算法·bash·无人机·持续部署
凤山老林2 小时前
SpringBoot + MyBatis-Plus 如何高效实现数据变更记录
java·spring boot·mybatis
黎阳之光2 小时前
黎阳之光:以科技之力赋能城市更新,共筑高品质示范之城
大数据·人工智能·科技
AI营销前沿2 小时前
原圈科技AI市场分析榜单:2026年如何打破数据孤岛,实现营销增长300%?
大数据·人工智能