Elasticsearch赋能3D打印机任务统计分析

发布于:2025-09-03 ⋅ 阅读:(16) ⋅ 点赞:(0)

背景与挑战:打印任务统计的痛点

在3D打印机管理系统中,PrinterController负责提供各类打印任务统计接口,如打印时长分布、耗材用量趋势、任务成功率等关键指标。随着系统运行时间增长(假设已累计10万+打印任务),基于MySQL的传统统计方案逐渐面临严峻挑战:

  1. 统计查询性能瓶颈:复杂的GROUP BY和ORDER BY组合查询导致CPU利用率飙升,部分报表接口响应时间超过10秒
    如查看不同型号设备的月度打印时长TOP10
/* 耗时12.4s (10万条记录) */
SELECT 
    p.device_model,
    AVG(j.print_duration)/60 AS avg_hours,
    SUM(j.print_duration)/3600 AS total_hours
FROM PrinterJobStatus j
JOIN Printer p ON j.printer_id = p.id
WHERE j.print_start_time BETWEEN '2025-07-01' AND '2025-07-31'
GROUP BY p.device_model
ORDER BY total_hours DESC
LIMIT 10;

• 需要扫描PrinterJobStatus全表(10万+行)

• 关联Printer表时反复索引查询

• 排序前需生成临时表存储所有分组结果

• 生产影响​​:报表页面卡死,无法快速定位高负荷设备

2. 实时性要求难以满足:管理员需要实时查看当前打印任务状态和趋势,但MySQL不适合高频聚合分析
大屏监控系统需 ​​实时刷新当前车间任务成功率​​(5秒轮询)

/* 耗时3.2s (每秒触发20+次) */
SELECT 
    (SELECT COUNT(*) FROM PrinterJobStatus 
     WHERE status=1 AND end_time>NOW()-INTERVAL 1 HOUR) * 100.0 /
    (SELECT COUNT(*) FROM PrinterJobStatus 
     WHERE end_time>NOW()-INTERVAL 1 HOUR) AS success_rate;

• 每小时数据需扫描2万+条记录

• 高频触发导致MySQL QPS暴增(压测峰值CPU达85%)

• 生产影响​​:监控大屏数据延迟,故障响应滞后

  1. 数据关联复杂度高:统计分析往往需要关联Printer、PrinterJobStatus和PrinterStatistics等多张表
    统计各部门季度耗材成本​
/* 3表关联查询 耗时9.8s */
SELECT 
    d.name AS department,
    SUM(s.material_used * m.unit_price) AS cost
FROM PrinterStatistics s
JOIN Printer p ON s.printer_id = p.id
JOIN Department d ON p.department_id = d.id
JOIN Material m ON s.material_id = m.id
WHERE s.quarter = '2025-Q2'
GROUP BY d.id;

• 需要4次索引跳转(printer_id→department_id→material_id)

• 中间结果集膨胀至15万行

• 生产影响​​:月末结算延迟,成本核算误差达5%
4. 历史数据分析能力不足:难以高效地进行跨月、跨季度的长期趋势分析
分析 ​​2022全年各月打印失败原因分布​

/* 跨12个月分区 耗时22.6s */
SELECT 
    DATE_FORMAT(print_end_time,'%Y-%m') AS month,
    failure_reason,
    COUNT(*) AS count
FROM PrinterJobStatus
WHERE status=0 
  AND print_end_time BETWEEN '2022-01-01' AND '2022-12-31'
GROUP BY month, failure_reason;

• 需扫描全年12个分区(总计80万条记录)

• GROUP BY双字段分组产生大量中间数据

• 生产影响​​:无法快速识别喷头堵塞的季节性规律

技术方案:Elasticsearch的聚合分析优势

针对以上挑战,Elasticsearch凭借其强大的聚合分析能力提供了理想解决方案:
mermaid

数据模型设计:从关系型到文档型

1. 现有MySQL表结构分析

目前系统中与打印任务统计相关的核心表包括:

- *PrinterJobStatus*:记录打印任务的执行状态和关键指标

- *Printer*:存储打印机基本信息

- *PrinterStatistics*:记录打印机的统计数据

2. Elasticsearch索引设计

为支持高效统计分析,我们设计了名为print_job_history的索引,采用宽表设计模式,将多个相关表的字段整合到单一文档中:

{

 "mappings": {

  "properties": {

   "jobId": { "type": "keyword" },

   "deviceSn": { "type": "keyword" },

   "deviceName": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },

   "deviceModel": { "type": "keyword" },

   "printStartTime": { "type": "date" },

   "printEndTime": { "type": "date" },

   "printDuration": { "type": "long" }, // 打印时长(秒)

   "materialUsage": { "type": "float" }, // 耗材用量(克)

   "jobStatus": { "type": "integer" }, // 0失败/1成功/2进行中

   "layerHeight": { "type": "float" },

   "nozzleTemp": { "type": "integer" },

   "bedTemp": { "type": "integer" },

   "materialType": { "type": "keyword" },

   "fileSize": { "type": "long" },

   "creator": { "type": "keyword" },

   "department": { "type": "keyword" },

   "successRate": { "type": "float" },

   "failureReason": { "type": "text", "analyzer": "ik_max_word" }

  }

 }

}

系统架构与数据流程

整体架构图


┌─────────────┐    ┌─────────────┐    ┌─────────────┐

│ 3D Printer  │─────>│ Spring Boot │─────>│  MySQL   │

└─────────────┘    └──────┬──────┘    └─────────────┘

		​                   │
		
		​          ┌────────┴────────┐
		
		​          ▼                 ▼

	​     ┌─────────────┐   ┌─────────────┐
	
	​      │  Canal   │────>│ Elasticsearch│
	
	​      └─────────────┘   └──────┬──────┘

				​                    │
				
				​                ┌──────┴──────┐
				
				​                │  Kibana   │
				
				​                └─────────────┘

核心数据流程

  1. *实时数据同步*
  • 通过AOP监听PrinterController中任务状态变更的方法

  • 任务完成时(状态变更为完成/失败),异步推送数据到Elasticsearch

  1. *历史数据迁移*
  • 使用Canal监听MySQL的binlog,实现存量数据的批量同步

  • 设计分片策略,按时间(每月/每季度)创建索引分片

  1. *查询服务重构*
  • 重构PrinterController中的统计接口,优先从ES获取数据

  • 对于ES中不存在的冷数据,设计自动回源MySQL的机制

核心功能实现:从代码层到业务层

完整案例

  1. 项目依赖 (pom.xml 示例)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>2.7.18</version>
       <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <groupId>com.example</groupId>
   <artifactId>printer-analytics</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>printer-analytics</name>
   <description>3D Printer Analytics with Elasticsearch</description>

   <properties>
       <java.version>17</java.version>
       <elasticsearch.version>7.17.2</elasticsearch.version>
       <spring-data-elasticsearch.version>4.4.0</spring-data-elasticsearch.version>
   </properties>

   <dependencies>
       <!-- Spring Boot Web Starter -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <!-- Spring Data Elasticsearch Starter -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
       </dependency>

       <!-- Elasticsearch High Level REST Client -->
       <dependency>
           <groupId>org.elasticsearch.client</groupId>
           <artifactId>elasticsearch-rest-high-level-client</artifactId>
           <version>${elasticsearch.version}</version>
       </dependency>

       <!-- Elasticsearch Core -->
       <dependency>
           <groupId>org.elasticsearch</groupId>
           <artifactId>elasticsearch</artifactId>
           <version>${elasticsearch.version}</version>
       </dependency>


       <!-- Spring Boot Test Starter -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>

       <dependency>
           <groupId>org.springframework.data</groupId>
           <artifactId>spring-data-elasticsearch</artifactId>
           <version>${spring-data-elasticsearch.version}</version>
       </dependency>
   </dependencies>

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>
</project>
  1. 配置文件 (application.properties)
# Elasticsearch Configuration
# 确保你的 ES 实例运行在 9200 端口
spring.elasticsearch.uris=http://localhost:9200
# 如果需要认证,添加用户名和密码
# spring.elasticsearch.username=your_username
# spring.elasticsearch.password=your_password

# 应用端口
server.port=8080

# 日志级别 (可选,用于调试)
logging.level.org.elasticsearch.client=DEBUG
  1. Elasticsearch 配置类
package com.grant.code.esdemo.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

    @Value("${spring.elasticsearch.uris}")
    private String elasticsearchUri;

    @Bean(destroyMethod = "close")
    public RestHighLevelClient elasticsearchClient() {
        // 解析 URI (假设是 http://host:port 格式)
        String[] parts = elasticsearchUri.replace("http://", "").split(":");
        String hostname = parts[0];
        int port = Integer.parseInt(parts[1]);

        // 创建 RestHighLevelClient [[7]]
        return new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost(hostname, port, "http")
                )
        );
    }
}
  1. 实体类 (映射 ES 文档)
package com.grant.code.esdemo.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.util.Date;

import java.util.Date;

@Document(indexName = "print_job_history") // 指定索引名
public class PrintJobDocument {

    @Id // 标识文档ID
    private String jobId;

    @Field(type = FieldType.Keyword)
    private String deviceSn;

    @MultiField(
            mainField = @Field(type = FieldType.Text),
            otherFields = {
                    @InnerField(suffix = "keyword", type = FieldType.Keyword)
            }
    )
    private String deviceName;

    @Field(type = FieldType.Keyword)
    private String deviceModel;

    @Field(type = FieldType.Date, format = DateFormat.date_time) // 日期时间格式
    private Date printStartTime;

    @Field(type = FieldType.Date, format = DateFormat.date_time)
    private Date printEndTime;

    @Field(type = FieldType.Long) // 打印时长(秒)
    private Long printDuration;

    @Field(type = FieldType.Float) // 耗材用量(克)
    private Float materialUsage;

    @Field(type = FieldType.Integer) // 0失败/1成功/2进行中
    private Integer jobStatus;

    @Field(type = FieldType.Float)
    private Float layerHeight;

    @Field(type = FieldType.Integer)
    private Integer nozzleTemp;

    @Field(type = FieldType.Integer)
    private Integer bedTemp;

    @Field(type = FieldType.Keyword)
    private String materialType;

    @Field(type = FieldType.Long)
    private Long fileSize;

    @Field(type = FieldType.Keyword)
    private String creator;

    @Field(type = FieldType.Keyword)
    private String department;

    @Field(type = FieldType.Float)
    private Float successRate;

    @Field(type = FieldType.Text)
    private String failureReason;

    // Getters and Setters
    public String getJobId() { return jobId; }
    public void setJobId(String jobId) { this.jobId = jobId; }

    public String getDeviceSn() { return deviceSn; }
    public void setDeviceSn(String deviceSn) { this.deviceSn = deviceSn; }

    public String getDeviceName() { return deviceName; }
    public void setDeviceName(String deviceName) { this.deviceName = deviceName; }

    public String getDeviceModel() { return deviceModel; }
    public void setDeviceModel(String deviceModel) { this.deviceModel = deviceModel; }

    public Date getPrintStartTime() { return printStartTime; }
    public void setPrintStartTime(Date printStartTime) { this.printStartTime = printStartTime; }

    public Date getPrintEndTime() { return printEndTime; }
    public void setPrintEndTime(Date printEndTime) { this.printEndTime = printEndTime; }

    public Long getPrintDuration() { return printDuration; }
    public void setPrintDuration(Long printDuration) { this.printDuration = printDuration; }

    public Float getMaterialUsage() { return materialUsage; }
    public void setMaterialUsage(Float materialUsage) { this.materialUsage = materialUsage; }

    public Integer getJobStatus() { return jobStatus; }
    public void setJobStatus(Integer jobStatus) { this.jobStatus = jobStatus; }

    public Float getLayerHeight() { return layerHeight; }
    public void setLayerHeight(Float layerHeight) { this.layerHeight = layerHeight; }

    public Integer getNozzleTemp() { return nozzleTemp; }
    public void setNozzleTemp(Integer nozzleTemp) { this.nozzleTemp = nozzleTemp; }

    public Integer getBedTemp() { return bedTemp; }
    public void setBedTemp(Integer bedTemp) { this.bedTemp = bedTemp; }

    public String getMaterialType() { return materialType; }
    public void setMaterialType(String materialType) { this.materialType = materialType; }

    public Long getFileSize() { return fileSize; }
    public void setFileSize(Long fileSize) { this.fileSize = fileSize; }

    public String getCreator() { return creator; }
    public void setCreator(String creator) { this.creator = creator; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }

    public Float getSuccessRate() { return successRate; }
    public void setSuccessRate(Float successRate) { this.successRate = successRate; }

    public String getFailureReason() { return failureReason; }
    public void setFailureReason(String failureReason) { this.failureReason = failureReason; }

    @Override
    public String toString() {
        return "PrintJobDocument{" +
                "jobId='" + jobId + '\'' +
                ", deviceSn='" + deviceSn + '\'' +
                ", deviceName='" + deviceName + '\'' +
                ", deviceModel='" + deviceModel + '\'' +
                ", printStartTime=" + printStartTime +
                ", printEndTime=" + printEndTime +
                ", printDuration=" + printDuration +
                ", materialUsage=" + materialUsage +
                ", jobStatus=" + jobStatus +
                ", layerHeight=" + layerHeight +
                ", nozzleTemp=" + nozzleTemp +
                ", bedTemp=" + bedTemp +
                ", materialType='" + materialType + '\'' +
                ", fileSize=" + fileSize +
                ", creator='" + creator + '\'' +
                ", department='" + department + '\'' +
                ", successRate=" + successRate +
                ", failureReason='" + failureReason + '\'' +
                '}';
    }
}
  1. Repository (数据访问层)
package com.example.printeranalytics.repository;

import com.example.printeranalytics.model.PrintJobDocument;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

// 使用 Spring Data Elasticsearch Repository [[31]]
@Repository
public interface PrintJobRepository extends ElasticsearchRepository<PrintJobDocument, String> {

    // Spring Data 自动根据方法名生成查询 [[38]]
    List<PrintJobDocument> findByDeviceModel(String deviceModel);

    // 可以添加更多自定义查询方法,例如基于注解 [[36]]
    // @Query("{\"bool\": {\"must\": [{\"match\": {\"jobStatus\": \"?0\"}}]}}")
    // List<PrintJobDocument> findByStatus(int status);
}
  1. Service (业务逻辑层)
package com.grant.code.esdemo.service;

import com.grant.code.esdemo.repository.PrintJobRepository;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilters;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.ParsedAvg;
import org.elasticsearch.search.aggregations.metrics.ParsedSum;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Service
public class AnalyticsService {

    @Autowired
    private PrintJobRepository printJobRepository; // 使用 Repository 进行基础操作

    @Autowired
    private RestHighLevelClient elasticsearchClient; // 使用 High Level Client 进行复杂聚合查询 [[7]]

    /**
     * 获取指定月份内各设备型号的打印时长统计 (TOP 10)
     * 对应原文中的第一个挑战查询
     */
    public List<Map<String, Object>> getDeviceModelPrintDurationStats(String yearMonth) throws IOException {
        // 构建日期范围 (假设 yearMonth 格式为 "yyyy-MM")
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        LocalDateTime start = LocalDateTime.parse(yearMonth + "-01T00:00:00");
        LocalDateTime end = start.plusMonths(1);

        SearchRequest searchRequest = new SearchRequest("print_job_history"); // 指定索引名
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // 添加日期范围和聚合查询
        searchSourceBuilder.query(QueryBuilders.rangeQuery("printStartTime")
                .gte(start)
                .lt(end)
                .format("strict_date_optional_time||epoch_millis")); // 设置日期格式

        // 添加聚合查询
        TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_device_model") // 聚合名称
                .field("deviceModel") // 聚合字段
                .subAggregation(AggregationBuilders.avg("avg_hours").field("printDuration")) // 平均时长
                .subAggregation(AggregationBuilders.sum("total_hours").field("printDuration")) // 总时长
                .size(10) // TOP 10
                .order(BucketOrder.aggregation("total_hours", false)); // 按总时长降序

        searchSourceBuilder.aggregation(aggregation);
        searchSourceBuilder.size(0); // 不返回文档内容,只返回聚合结果

        searchRequest.source(searchSourceBuilder); // 设置查询源

        SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT);

        // 解析聚合结果
        List<Map<String, Object>> results = new ArrayList<>();
        ParsedStringTerms modelAgg = searchResponse.getAggregations().get("by_device_model"); // 根据聚合名称获取聚合结果
        for (Terms.Bucket bucket : modelAgg.getBuckets()) {
            Map<String, Object> result = new HashMap<>();
            result.put("deviceModel", bucket.getKeyAsString());

            // 处理可能为null的情况
            ParsedAvg avgHours = bucket.getAggregations().get("avg_hours");
            result.put("avgHours", avgHours != null ? avgHours.getValue() / 3600.0 : 0.0); // 转换为小时

            ParsedSum totalHours = bucket.getAggregations().get("total_hours");
            result.put("totalHours", totalHours != null ? totalHours.getValue() / 3600.0 : 0.0); // 转换为小时

            results.add(result);
        }
        return results;
    }

    /**
     * 获取过去一小时的任务成功率
     * 对应原文中的第二个挑战查询
     */
    public double getRecentSuccessRate() throws IOException {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime oneHourAgo = now.minusHours(1);

        SearchRequest searchRequest = new SearchRequest("print_job_history");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // 查询过去一小时结束的任务
        searchSourceBuilder.query(QueryBuilders.rangeQuery("printEndTime")
                .gte(oneHourAgo)
                .lte(now)
                .format("strict_date_optional_time||epoch_millis"));

        // 使用单一聚合计算成功率
        searchSourceBuilder.aggregation(
                AggregationBuilders.filters("status_stats",
                        new FiltersAggregator.KeyedFilter("success", QueryBuilders.termQuery("jobStatus", 1)),
                        new FiltersAggregator.KeyedFilter("failed", QueryBuilders.termQuery("jobStatus", 0))
                )
        );

        searchSourceBuilder.size(0); // 不返回文档内容
        searchRequest.source(searchSourceBuilder);

        SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT);

        // 解析聚合结果
        ParsedFilters filters = searchResponse.getAggregations().get("status_stats");
        long successCount = filters.getBucketByKey("success").getDocCount();
        long totalCount = successCount + filters.getBucketByKey("failed").getDocCount();

        if (totalCount == 0) {
            return 0.0;
        }
        return (double) successCount / totalCount * 100;
    }


    // 可以添加更多统计方法...
}
  1. Controller (API 接口层)
package com.grant.code.esdemo.controller;

import com.grant.code.esdemo.service.AnalyticsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/analytics")
public class AnalyticsController {

    @Autowired
    private AnalyticsService analyticsService;

    /**
     * 获取指定月份的打印设备型号统计
     * @param yearMonth
     * @return
     */
    @GetMapping("/device-model-stats/{yearMonth}")
    public List<Map<String, Object>> getDeviceModelStats(@PathVariable String yearMonth) {
        try {
            return analyticsService.getDeviceModelPrintDurationStats(yearMonth);
        } catch (IOException e) {
             // 处理异常,例如返回 500 错误或记录日志
            e.printStackTrace();
            throw new RuntimeException("Failed to fetch device model stats", e);
        }
    }

    /**
     * 获取最近一个月的打印成功率
     * @return
     */
    @GetMapping("/success-rate")
    public Map<String, Object> getSuccessRate() {
         try {
            double rate = analyticsService.getRecentSuccessRate();
            return Map.of("successRate", rate);
        } catch (IOException e) {
             // 处理异常
            e.printStackTrace();
            throw new RuntimeException("Failed to fetch success rate", e);
        }
    }

    // 添加更多 API 端点...
}
  1. 初始化索引和映射
package com.grant.code.esdemo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;

@Component
public class IndexInitializer {

    private static final String INDEX_NAME = "print_job_history";
    private static final String MAPPING_FILE = "mapping/print_job_history_mapping.json";

    @Autowired
    private RestHighLevelClient elasticsearchClient;

    @Autowired
    private ObjectMapper objectMapper;

    @EventListener(ApplicationReadyEvent.class)
    public void initIndices() {
        try {
            // 添加重试机制
            int retryCount = 0;
            while (retryCount < 3) {
                try {
                    if (!indexExists(INDEX_NAME)) {
                        createIndexWithMapping(INDEX_NAME, MAPPING_FILE);
                    } else {
                        System.out.println("索引 " + INDEX_NAME + " 已存在");
                    }
                    break; // 成功则退出循环
                } catch (IOException e) {
                    retryCount++;
                    if (retryCount >= 3) {
                        throw e; // 重试3次后抛出异常
                    }
                    System.err.println("索引初始化失败,正在重试 (" + retryCount + "/3)...");
                    TimeUnit.SECONDS.sleep(2); // 等待2秒后重试
                }
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("### 索引初始化严重错误 ###");
            e.printStackTrace();
            // 这里可以添加通知逻辑,如发送邮件或短信
        }
    }

    private boolean indexExists(String indexName) throws IOException {
        // 正确创建 GetIndexRequest 并指定索引名称
        GetIndexRequest request = new GetIndexRequest().indices(indexName);
        return elasticsearchClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    private void createIndexWithMapping(String indexName, String mappingFilePath) throws IOException {
        // 1. 创建索引请求
        CreateIndexRequest request = new CreateIndexRequest(indexName);

        // 2. 设置索引设置(可选)
        request.settings(Settings.builder()
                .put("index.number_of_shards", 3)
                .put("index.number_of_replicas", 1)
                .put("index.refresh_interval", "1s")
                .build());

        // 3. 加载映射文件
        ClassPathResource resource = new ClassPathResource(mappingFilePath);
        if (!resource.exists()) {
            throw new IOException("映射文件未找到: " + mappingFilePath);
        }

        String mappingSource = new String(Files.readAllBytes(Paths.get(resource.getURI())), StandardCharsets.UTF_8);
        request.mapping(mappingSource, XContentType.JSON);

        // 4. 创建索引
        CreateIndexResponse createIndexResponse = elasticsearchClient.indices().create(request, RequestOptions.DEFAULT);

        // 5. 处理响应
        if (createIndexResponse.isAcknowledged()) {
            System.out.println("索引 " + indexName + " 创建成功");
        } else {
            System.err.println("索引创建请求未被确认");
            System.err.println("响应信息: " + createIndexResponse);
        }
    }
}

对应的 src/main/resources/mapping/print_job_history_mapping.json 文件:

{
  "mappings": {
    "properties": {
      "jobId": { "type": "keyword" },
      "deviceSn": { "type": "keyword" },
      "deviceName": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
      "deviceModel": { "type": "keyword" },
      "printStartTime": { "type": "date", "format": "strict_date_optional_time||epoch_millis" },
      "printEndTime": { "type": "date", "format": "strict_date_optional_time||epoch_millis" },
      "printDuration": { "type": "long" },
      "materialUsage": { "type": "float" },
      "jobStatus": { "type": "integer" },
      "layerHeight": { "type": "float" },
      "nozzleTemp": { "type": "integer" },
      "bedTemp": { "type": "integer" },
      "materialType": { "type": "keyword" },
      "fileSize": { "type": "long" },
      "creator": { "type": "keyword" },
      "department": { "type": "keyword" },
      "successRate": { "type": "float" },
      "failureReason": { "type": "text" }
    }
  }
}
  1. 启动程序
    在这里插入图片描述
    在这里插入图片描述

  2. 测试端点
    在这里插入图片描述
    在这里插入图片描述

生成大量模拟数据

使用 Java Faker 库
添加依赖 (pom.xml)

<!-- Java Faker for generating fake data -->
<dependency>
    <groupId>com.github.javafaker</groupId>
    <artifactId>javafaker</artifactId>
    <version>1.0.2</version> <!-- 使用较新版本 -->
</dependency>

创建数据生成服务

package com.grant.code.esdemo.controller;

import com.grant.code.esdemo.service.DataGenerationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/data")
public class DataGenerationController {

    @Autowired
    private DataGenerationService dataGenerationService;

    @PostMapping("/generate")
    public String generateData(@RequestParam(defaultValue = "10000") int count) {
        try {
            dataGenerationService.generateAndSaveData(count);
            return "Successfully generated and saved " + count + " data entries.";
        } catch (Exception e) {
            e.printStackTrace();
            return "Error generating data: " + e.getMessage();
        }
    }
}

创建 Controller 端点触发数据生成

package com.grant.code.esdemo.controller;

import com.grant.code.esdemo.service.DataGenerationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/data")
public class DataGenerationController {

    @Autowired
    private DataGenerationService dataGenerationService;

    @PostMapping("/generate")
    public String generateData(@RequestParam(defaultValue = "10000") int count) {
        try {
            dataGenerationService.generateAndSaveData(count);
            return "Successfully generated and saved " + count + " data entries.";
        } catch (Exception e) {
            e.printStackTrace();
            return "Error generating data: " + e.getMessage();
        }
    }
}

通过postman发送请求,生成50000条测试数据
在这里插入图片描述
观察控制台输出
在这里插入图片描述
查看kibana
在这里插入图片描述

测试

在这里插入图片描述
测试查询 1:设备型号月度打印时长 TOP 10
假设查询 2023 年 10 月的数据

POST /print_job_history/_search
{
  "size": 0,
  "query": {
    "range": {
      "printStartTime": {
        "gte": "2023-10-01T00:00:00",
        "lt": "2023-11-01T00:00:00",
        "format": "strict_date_optional_time"
      }
    }
  },
  "aggs": {
    "by_device_model": {
      "terms": {
        "field": "deviceModel",
        "size": 10,
        "order": { "total_hours": "desc" }
      },
      "aggs": {
        "avg_hours": {
          "avg": {
            "field": "printDuration"
          }
        },
        "total_hours": {
          "sum": {
            "field": "printDuration"
          }
        },
        "total_hours_in_hours": {
          "bucket_script": {
            "buckets_path": {
              "totalSeconds": "total_hours"
            },
            "script": "params.totalSeconds / 3600"
          }
        },
        "avg_hours_in_hours": {
          "bucket_script": {
            "buckets_path": {
              "avgSeconds": "avg_hours"
            },
            "script": "params.avgSeconds / 3600"
          }
        }
      }
    }
  }
}

在这里插入图片描述

查看返回结果的 aggregations 部分,确认是否得到了预期的设备型号及其总时长和平均时长。特别注意 took 字段,它表示 ES 执行查询所花费的时间(毫秒),应该远小于原始 MySQL 的 12.4 秒。 这里5万条数据仅需 154毫秒!!!!

测试查询 2:近期任务成功率
查询过去一小时的成功率。
这里直接测试接口
在这里插入图片描述
可以发现速度还是飞快的!!!
在这里插入图片描述

总结与展望

通过引入Elasticsearch,3D打印机管理系统的打印任务统计分析功能实现了质的飞跃。不仅解决了传统MySQL方案的性能瓶颈,还为系统带来了更丰富的数据分析能力和更好的用户体验。


网站公告

今日签到

点亮在社区的每一天
去签到