【项目面试】

JWT 用户认证 + 拦截器 + ThreadLocal

JWT

JWT(JSON Web Token)是一种用于身份认证和信息安全传输的开放标准(RFC 7519)。它是一种基于 JSON 格式的令牌,用于在客户端与服务器之间安全地传递信息。

JWT 的结构

JWT 由三个部分组成,并用 . 连接:

  • Header(头部):包含令牌的元信息,如加密算法(如 HS256、RS256)。
  • Payload(负载):存储用户信息(如 userId、username),以及额外的声明(claims),如 exp(过期时间)。
  • Signature(签名):使用 Header 中指定的算法,对 Header 和 Payload 进行签名,确保 JWT 的完整性。
JWT 的使用

1. 登录时生成 JWT

  • 用户登录后,服务器验证用户名和密码。
  • 服务器生成 JWT 并返回给客户端。
  • 客户端存储 JWT(通常存放在 localStorageHTTP Only Cookie)。

2. 客户端携带 JWT 访问资源

  • 客户端在请求时将 JWT 放入 Authorization 头部:Authorization: Bearer <JWT_TOKEN>
  • 服务器接收请求,验证 JWT 的有效性(是否篡改、是否过期)。

3. 服务器验证 JWT 并返回响应

  • 服务器解码 JWT,验证签名和过期时间。
  • 若 JWT 有效,则返回数据;否则返回 401 Unauthorized
JWT 的优缺点

✅ 优点:

  • 无状态认证:不需要在服务器存储会话信息,适用于分布式系统。
  • 安全性:使用签名(HMAC、RSA)防止篡改。
  • 可扩展性:可以自定义 payload 内容,如角色权限等。

❌ 缺点:

  • 无法撤销:JWT 一旦签发,无法主动让其失效(除非使用黑名单机制)。
  • 体积较大:相比传统的 Session ID,JWT 包含较多信息,占用带宽。
JWT 令牌为什么能解决集群部署,什么是集群部署?

在传统的基于会话和Cookie的身份验证方式中,会话信息通常存储在服务器的内存或数据库中。但在集群部署中,不同服务器之间没有共享的会话信息,这会导致用户在不同服务器之间切换时需要重新登录,或者需要引入额外的共享机制(如Redis),增加了复杂性和性能开销。

而JWT令牌通过在令牌中包含所有必要的身份验证和会话信息,使得服务器无需存储会话信息,从而解决了集群部署中的身份验证和会话管理问题。当用户进行登录认证后,服务器将生成一个JWT令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。

由于JWT令牌是自包含的,服务器可以独立地对令牌进行验证,而不需要依赖其他服务器或共享存储。这使得集群中的每个服务器都可以独立处理请求,提高了系统的可伸缩性和容错性。

JWT 令牌如果泄露了,怎么解决,JWT是怎么做的?
  • 及时失效令牌:当检测到JWT令牌泄露或存在风险时,可以立即将令牌标记为失效状态。服务器在接收到带有失效标记的令牌时,会拒绝对其进行任何操作,从而保护用户的身份和数据安全。
  • 刷新令牌:JWT 令牌通常具有一定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄露时,可以主动刷新令牌,即重新生成一个新的令牌,并将旧令牌标记为失效状态。这样,即使泄露的令牌被恶意使用,也会很快失效,减少了被攻击者滥用的风险。
  • 使用黑名单:服务器可以维护一个令牌的黑名单,将泄露的令牌添加到黑名单中。在接收到令牌时,先检查令牌是否在黑名单中,如果在则拒绝操作。这种方法需要服务器维护黑名单的状态,对性能有一定的影响,但可以有效地保护泄露的令牌不被滥用。

TreadLocal

TreadLocal 有什么用

通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享。

ThreadLocal 类允许每个线程绑定自己的值,当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会拥有一个独立的副本。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而避免了线程安全问题。

ThreadLocal的底层原理实现

在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象。

  • 当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
  • 当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
  • 当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocal导致内存溢出问题

ThreadLocalMap 的 key 和 value 引用机制:

  • key 是弱引用:ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用 (WeakReference<ThreadLocal<?>>)。 这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。
  • value 是强引用:ThreadLocalMap 中的 value 是强引用。 即使 key 被回收(变为 null),value 仍然存在于 ThreadLocalMap 中,被强引用,不会被回收。

当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,就会造成内存泄漏。

也就是说,内存泄漏的发生需要同时满足两个条件:

  • ThreadLocal 实例不再被强引用;
  • 线程持续存活,导致 ThreadLocalMap 长期存在。

因此,在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。

无状态的用户登录认证流程

在 Spring Boot 中,我们可以基于 JWT + 拦截器 + ThreadLocal 实现 无状态的用户登录认证,主要流程如下:

  • 用户登录:提交用户名、密码,验证成功后,服务器生成 JWT 并返回给前端。
  • 前端携带 JWT 访问 API:后续请求在 Authorization 头中携带 Bearer Token
  • 拦截器拦截请求:
    • 解析 JWT,校验签名与过期时间。
    • 从 JWT 中提取 用户信息,存入 ThreadLocal,供当前请求使用。
  • 请求结束后清理 ThreadLocal,防止内存泄漏。

对比:

方案优点缺点
JWT1️⃣ 无状态,可跨服务、集群扩展方便
2️⃣ 解析快,直接从 Token 获取用户信息
1️⃣ Token 无法撤销,必须等到过期
2️⃣ 需要客户端存储 Token
Session + Redis1️⃣ 可随时注销 Session,安全性更高
2️⃣ 可存储更多用户信息
1️⃣ 需要 Redis 依赖,有额外存储开销
2️⃣ 跨域复杂,Session 需要 Sticky Session 或 Redis 共享

试卷导出场景的工厂模式 + 策略模式实现

1. 需求分析与设计背景

首先,我分析了试卷导出的需求,我们需要支持多种文件格式(如 PDF、Word、Excel)进行试卷导出。每种格式的导出逻辑不同,但它们都遵循相同的基本流程,因此我决定结合使用 工厂模式策略模式 来管理不同的导出策略。

  • 策略模式:用于封装不同的导出格式(如 PDF、Word、Excel),使每个导出格式的逻辑独立,易于扩展。
  • 工厂模式:用于根据用户的需求(如 PDF、Word、Excel)动态选择相应的导出策略,使得系统具有更高的灵活性。

2. 策略模式实现

我首先定义了一个 ExportStrategy 接口,所有导出格式都需要实现该接口,并提供具体的 export() 方法。每个实现类负责实现相应格式的导出逻辑。

public interface ExportStrategy {
    void export(ExamPaper examPaper);
}

然后,我为每种导出格式(如 PDF、Word、Excel)创建了具体的策略类,实现了 ExportStrategy 接口。例如:

@Component
public class PdfExportStrategy implements ExportStrategy {
    @Override
    public void export(ExamPaper examPaper) {
        System.out.println("导出试卷为 PDF 格式");
        // 具体的 PDF 导出逻辑
    }
}

@Component
public class WordExportStrategy implements ExportStrategy {
    @Override
    public void export(ExamPaper examPaper) {
        System.out.println("导出试卷为 Word 格式");
        // 具体的 Word 导出逻辑
    }
}

@Component
public class ExcelExportStrategy implements ExportStrategy {
    @Override
    public void export(ExamPaper examPaper) {
        System.out.println("导出试卷为 Excel 格式");
        // 具体的 Excel 导出逻辑
    }
}

3. 工厂模式实现

为了根据用户选择的导出格式动态选择相应的策略,我实现了一个 工厂类,该类从配置文件中加载支持的格式类型,并根据选择的格式返回对应的策略对象。

@Component
public class ExportFactory implements ApplicationContextAware {

    private static Map<String, ExportStrategy> strategyPool = new ConcurrentHashMap<>();

    @Autowired
    private ExportTypeConfig exportTypeConfig;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        exportTypeConfig.getTypes().forEach((k, v) -> {
            strategyPool.put(k, (ExportStrategy) applicationContext.getBean(v));
        });
    }

    public ExportStrategy getExportStrategy(String exportType) {
        return strategyPool.get(exportType);
    }
}

在这个工厂类中,我使用了 Spring 的 ApplicationContext 来获取不同的导出策略(例如 pdfExportStrategywordExportStrategyexcelExportStrategy),并将它们存储在 strategyPool 中。通过传入的格式类型(如 pdf、word、excel),工厂类返回对应的策略对象。

4. 配置文件与动态配置

为了方便后期扩展,我将导出格式及对应的策略类名配置在 application.yml 文件中,使得导出格式可以灵活配置,而无需修改代码。

export:
  types:
    pdf: pdfExportStrategy
    word: wordExportStrategy
    excel: excelExportStrategy

ExportTypeConfig 类读取 application.yml 文件中的配置,并将不同的策略类映射到配置文件中的格式(如 PDF、Word、Excel)。

@Configuration
@ConfigurationProperties(prefix = "export")
public class ExportTypeConfig {

    private Map<String, String> types;

    public Map<String, String> getTypes() {
        return types;
    }

    public void setTypes(Map<String, String> types) {
        this.types = types;
    }
}

@Configuration:标识该类是一个 Spring 配置类,用于定义 Bean。
@ConfigurationProperties(prefix = "export"):自动将配置文件中以 export 为前缀的属性注入到类的字段中,简化了配置管理。

5. 使用工厂类与策略类进行导出操作

在导出试卷的业务类中,我通过工厂类 ExportFactory 获取具体的导出策略,并调用 export() 方法来执行导出操作。

@Component
public class ExamExportService {

    @Autowired
    private ExportFactory exportFactory;

    public void exportExamPaper(ExamPaper examPaper, String exportType) {
        ExportStrategy exportStrategy = exportFactory.getExportStrategy(exportType);
        if (exportStrategy == null) {
            throw new IllegalArgumentException("Unsupported export type: " + exportType);
        }
        exportStrategy.export(examPaper);
    }
}

优化计算场景的多线程计算实现

1. 完整方案

第一步:站流量分配优化,使用类似 fmincon 的优化方法。
第二步:使用 ThreadPoolExecutor 并行计算每个站的井流量分配。
第三步:汇总各站井流量分配方案并得到总能耗。

2. 采用线性规划实现站流量分配优化

import org.apache.commons.math3.optimization.GoalType;
import org.apache.commons.math3.optimization.linear.*;

import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

// 站流量分配优化类
public class FlowOptimization {

    /**
     * 优化站流量分配
     * 通过线性规划求解最小能耗的流量分配方案
     */
    public void optimizeFlow(double[] initialGuess) {
        // 定义目标函数:能耗最小化,系数根据能耗计算公式设置
        // 这里使用简化的线性目标函数,实际应用中需要根据具体能耗模型定义
        LinearObjectiveFunction objectiveFunction = new LinearObjectiveFunction(initialGuess, 0);  // 系数根据你的能耗公式定义

        // 定义约束条件:例如流量限制,流量总和等
        LinearConstraintSet constraints = new LinearConstraintSet();
        constraints.add(new LinearConstraint(initialGuess, Relationship.LEQ, 100));  // 示例约束条件:流量 ≤ 100

        // 创建线性优化求解器
        SimplexSolver solver = new SimplexSolver();

        // 求解优化问题,GoalType.MINIMIZE 表示最小化目标函数
        PointValuePair solution = solver.optimize(objectiveFunction, constraints, GoalType.MINIMIZE, new NonNegativeConstraint(true));

        // 输出优化结果
        double[] optimizedFlow = solution.getPoint();
        System.out.println("Optimized Flow: " + Arrays.toString(optimizedFlow));
    }
}

3. 自定义线程池计算井的流量分配

public class StationEnergyOptimization {

    private ThreadPoolExecutor executorService;

    public StationEnergyOptimization(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
        // 创建一个自定义线程池
        executorService = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, unit,
                new LinkedBlockingQueue<>(10),
                new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:调用者线程执行
        );
    }

    /**
     * 执行多线程任务,对每个站点进行井流量分配计算
     * @param stationFlow 每个站点的流量分配
     */
    public void calculateEnergyDistribution(double[] stationFlow) {
        List<Future<Double>> futures = new ArrayList<>();

        // 提交每个井的能耗计算任务到线程池
        for (double flow : stationFlow) {
            futures.add(executorService.submit(() -> calculateEnergyForWell(flow)));
        }

        // 获取每个任务的计算结果
        for (Future<Double> future : futures) {
            try {
                System.out.println("Energy for well: " + future.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        // 关闭线程池,释放资源
        executorService.shutdown();
    }

    /**
     * 计算单个井的能耗
     * @param flow 该井的流量
     * @return 该井的能耗
     */
    private double calculateEnergyForWell(double flow) {
        // 假设能耗计算公式为 flow^2 * costFactor
        double costFactor = 1.5;  // 示例成本因子
        return Math.pow(flow, 2) * costFactor;  // 能耗计算公式
    }
}

Future 是 Java 并发编程中的一个接口,用于 表示一个异步计算的结果。它提供了一些方法,可以用来查询任务的状态、等待任务执行完毕并获取执行结果等。通常,Future 是通过提交任务给线程池时获取的,线程池会返回一个 Future 对象,表示任务执行的状态和结果。

主要方法:

  • get():阻塞方法,用于获取异步计算的结果。如果任务还没完成,调用 get() 会一直等待直到任务执行完毕。
  • get(long timeout, TimeUnit unit):带超时参数的 get 方法,如果任务在指定时间内未完成,将抛出 TimeoutException
  • cancel(boolean mayInterruptIfRunning):尝试取消任务的执行,mayInterruptIfRunning 参数指定是否中断正在执行的任务。
  • isDone():判断任务是否已经完成。
  • isCancelled():判断任务是否已被取消。

Spring AOP 记录日志

1. 介绍 OpenTracing 和 Spring AOP

  • OpenTracing:OpenTracing 是一个跨语言的标准,用于提供分布式追踪功能,能够帮助我们追踪一个请求在微服务架构中的流转过程。它主要通过生成 TraceId 来标识一个请求的生命周期,跟踪请求在各个服务间的执行情况。

  • Spring AOP:Spring AOP 是 Spring 框架提供的面向切面编程(Aspect-Oriented Programming)功能,它允许你在不修改核心业务代码的情况下,动态地在方法执行前后增加横切逻辑(如日志记录、安全检查等)。

2. 为什么将 OpenTracing 与 Spring AOP 结合使用

OpenTracingSpring AOP 配合使用,主要是为了在方法执行的过程中 自动生成 TraceId,并在方法调用时记录相关的跟踪信息。通过 AOP,我们能够在服务方法调用的开始和结束时自动生成和传递 Trace 信息,避免手动在每个方法中加入跟踪逻辑。

3. 实现步骤

步骤 1: 添加依赖
首先,在 pom.xml 中添加所需的 OpenTracing 和 Spring AOP 相关的依赖。

步骤 2: 配置 OpenTracing
在 Spring 项目中配置 OpenTracing。这里以 Jaeger 为例,你可以在配置类中设置 Jaeger 的 Tracer:

import io.jaegertracing.Configuration;
import io.opentracing.Tracer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TracingConfig {

    @Bean
    public Tracer tracer() {
        // 创建并返回 Jaeger Tracer
        return new Configuration("co2-pipeline")  // 服务名
                .getTracer();
    }
}

步骤 3: 创建 AOP 切面类
使用 Spring AOP 创建一个切面类,负责在方法执行时启动和关闭 OpenTracing Span,并将 TraceId 自动传播到日志中。

import io.opentracing.Span;
import io.opentracing.Tracer;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class TracingAspect {

    private static final Logger logger = LoggerFactory.getLogger(TracingAspect.class);

    @Autowired
    private Tracer tracer;

    // 前置通知:方法执行前
    @Before("execution(* com.example.co2.service.*.*(..))")
    public void startTrace(ProceedingJoinPoint joinPoint) {
        // 创建 Span
        Span span = tracer.buildSpan(joinPoint.getSignature().getName()).start();
        // 将 TraceId 注入到日志上下文中
        tracer.activateSpan(span);
        logger.info("Started tracing method: {}", joinPoint.getSignature().getName());
    }

    // 后置通知:方法执行后
    @After("execution(* com.example.co2.service.*.*(..))")
    public void endTrace(ProceedingJoinPoint joinPoint) {
        Span span = tracer.activeSpan();
        if (span != null) {
            span.finish();  // 结束 Span,记录 Trace 信息
            logger.info("Ended tracing method: {}", joinPoint.getSignature().getName());
        }
    }

    // 环绕通知:记录方法执行的时间
    @Around("execution(* com.example.co2.service.*.*(..))")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        // 执行目标方法
        Object result = joinPoint.proceed();

        long endTime = System.currentTimeMillis();
        logger.info("Method {} executed in {}ms", joinPoint.getSignature().getName(), (endTime - startTime));

        return result;
    }
}

步骤 4: 配置 TraceId 注入到日志中
通过 MDC(Mapped Diagnostic Context),你可以将 TraceId 注入到日志中,便于在分布式系统中查看请求的完整轨迹。

import org.slf4j.MDC;

@Before("execution(* com.example.co2.service.*.*(..))")
public void startTrace(ProceedingJoinPoint joinPoint) {
    // 获取方法签名
    Signature signature = joinPoint.getSignature();
    Span span = tracer.buildSpan(signature.getName()).start();
    
    // 将 TraceId 注入到 MDC,方便在日志中打印
    MDC.put("traceId", span.context().toString());
    tracer.activateSpan(span);
    
    logger.info("Started tracing method: {}", signature.getName());
}

@After("execution(* com.example.co2.service.*.*(..))")
public void endTrace(ProceedingJoinPoint joinPoint) {
    Span span = tracer.activeSpan();
    if (span != null) {
        span.finish();
        MDC.remove("traceId");  // 结束后移除 TraceId
        logger.info("Ended tracing method: {}", joinPoint.getSignature().getName());
    }
}

4. 配置日志框架

在日志配置(例如 logback.xml)中,可以通过 MDC 输出 TraceId:

<pattern>
  %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg %n
  %X{traceId}
</pattern>

5. 使用 TraceId

当业务方法(例如 CO2Service 中的 calculateEnergyConsumption)被调用时,Spring AOP 会自动记录 TraceId 和请求的执行信息:

@Service
public class CO2Service {

    public void calculateEnergyConsumption(String pipelineId) {
        // 业务逻辑
        System.out.println("Calculating energy consumption for pipeline " + pipelineId);
    }

    public void updateFlowStatus(String wellId, double flowRate) {
        // 更新井流量状态
        System.out.println("Updating flow status for well " + wellId + " with flow rate " + flowRate);
    }
}

6. 总结

Spring AOPOpenTracing 配合使用 的过程中,您可以通过以下步骤完成 TraceId 的自动生成与记录:

  • 配置 OpenTracing:设置 Tracer,如使用 Jaeger 来生成 TraceId。
  • 创建 AOP 切面类:通过切面(@Aspect)定义日志记录方法,在方法执行前后生成 TraceId,记录日志。
  • 将 TraceId 注入日志中:通过 MDC 把 TraceId 注入到日志上下文,确保在日志中能够记录 TraceId 信息。
  • 可视化追踪:结合 OpenTracing 的分布式追踪工具(如 Jaeger、Zipkin)来查看请求在各个服务中的完整执行路径,帮助排查性能瓶颈和错误。

通过这种方式,你可以轻松地将 分布式追踪日志记录 融合到业务逻辑中,帮助你在微服务架构中有效追踪请求,提升系统可观察性和可维护性。

Echarts 渲染优化

1. 数据分片

数据分片的主要目的是将大规模的数据集拆分成多个小块,这样可以减少一次渲染的数据量,提高渲染速度。

实现思路:

  • 将数据切分成多个小块:对于大规模的时间序列或散点数据,可以将其分成多个小段,在每次渲染时只加载当前显示区域的数据。
  • 动态加载和卸载数据:随着用户缩放或滚动,动态加载或卸载数据块。只加载当前视图范围内的数据,避免一次性加载过多数据。

实现步骤:

  • 计算视图区域范围:通过监听 ECharts 的 dataZoomroam 事件,获取当前视图的显示范围(如 X 轴和 Y 轴的显示区域)。
  • 切割数据:根据视图的范围,选择性地加载或展示数据。例如,选择 X 轴范围内的数据进行渲染。
  • 动态更新数据:当用户缩放或拖动图表时,动态更新当前视图范围内的数据,卸载不再显示的数据。
// ECharts 配置中使用数据分片
var chart = echarts.init(document.getElementById('main'));

var data = [...];  // 假设这是大量的数据

var chunkSize = 1000;  // 每次渲染的最大数据量
var currentChunkIndex = 0;

function getCurrentData() {
    var start = currentChunkIndex * chunkSize;
    var end = Math.min((currentChunkIndex + 1) * chunkSize, data.length);
    return data.slice(start, end);
}

chart.setOption({
    xAxis: {
        type: 'category',
        data: getCurrentData().map(item => item.x)  // 提取 X 轴数据
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: getCurrentData().map(item => item.y),  // 提取 Y 轴数据
        type: 'line'
    }]
});

// 监听 zoom 或 roam 事件,更新数据
chart.on('datazoom', function () {
    var newRange = chart.getOption().dataZoom[0];
    var start = Math.floor(newRange.start * data.length / 100);
    var end = Math.floor(newRange.end * data.length / 100);
    currentChunkIndex = Math.floor(start / chunkSize);
    chart.setOption({
        xAxis: {
            data: data.slice(start, end).map(item => item.x)
        },
        series: [{
            data: data.slice(start, end).map(item => item.y)
        }]
    });
});

2. 降采样算法

降采样算法用于减少渲染时需要处理的数据点数量,通过减少数据量来提高渲染速度。常见的降采样算法包括 随机采样、平均采样、最大/最小值采样、平滑采样等。

实现思路:

  • 选择性抽取数据点: 通过选择一个固定的采样率,减少数据点的数量。
  • 在保持数据趋势的情况下减少点数: 在图表展示时,通过一定的算法只保留重要的数据点,去除无关紧要的部分。

降采样算法示例(使用 平均采样):

  1. 计算每一段数据的平均值:将数据按一定的时间间隔(或数据点间隔)分段,并计算每段的平均值(或其他统计量)。
  2. 替换原始数据:将计算后的平均值用于绘图。
// 简单的降采样:按时间间隔计算平均值
function downSampleData(data, interval) {
    var sampledData = [];
    for (var i = 0; i < data.length; i += interval) {
        var chunk = data.slice(i, i + interval);
        var averageX = chunk.reduce(function (sum, item) { return sum + item.x; }, 0) / chunk.length;
        var averageY = chunk.reduce(function (sum, item) { return sum + item.y; }, 0) / chunk.length;
        sampledData.push({ x: averageX, y: averageY });
    }
    return sampledData;
}

var downsampledData = downSampleData(data, 5);  // 每隔5个数据点取平均

chart.setOption({
    xAxis: {
        type: 'category',
        data: downsampledData.map(item => item.x)
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: downsampledData.map(item => item.y),
        type: 'line'
    }]
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值