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

相关推荐
indexsunny3 分钟前
互联网大厂Java面试实战:从Spring Boot到微服务与Kafka的深度探讨
java·spring boot·junit·kafka·mybatis·hibernate·microservices
星辰_mya8 分钟前
三级缓存破局:Spring 如何优雅解决循环依赖?
java·spring·缓存·面试
BUG胡汉三9 分钟前
Java内网代理访问HTTPS接口SSL证书不匹配
java·https·ssl
洛邙10 分钟前
互联网大厂Java求职面试实录:Spring Boot与微服务实战解析
java·spring boot·缓存·微服务·面试·分布式事务·电商
java1234_小锋10 分钟前
Java高频面试题:Spring框架中的单例bean是线程安全的吗?
java·数据库·spring
代码探秘者10 分钟前
【大模型应用】5.深入理解向量数据库
java·数据库·后端·python·spring·面试
小王不爱笑13211 分钟前
Java 代理模式与 AOP 底层
java·开发语言·代理模式
weixin_4041576812 分钟前
Java高级面试与工程实践问题集(二)
java·开发语言·面试
杨超越luckly14 分钟前
AI Agent应用指南 :自动化构建品牌数据库:提示词 + API + 结构化输出
大数据·数据库·人工智能·自动化·ai agent
渔民小镇15 分钟前
不止 request/response —— ionet 的 4 种通信模型选型指南
java·服务器·游戏