InsightFlow:基于 Spring Boot+Redis+Docker 的实时监控告警系统全流程开发与部署

从 0 到 1 实战:

本文为 Java 后端入门级实战项目,全程包含完整可运行代码、环境配置、踩坑记录、容器化部署全流程,跟着步骤操作即可 100% 跑通项目。项目覆盖 Spring Boot 核心开发、Redis 轻量级消息队列、3σ 异常检测算法、WebSocket 实时推送、Docker 容器化部署等核心技能,,也适合 Java 新手巩固后端技术栈。

【本文目录】

  1. 项目介绍与技术栈选型
  2. 前置环境准备
  3. 项目整体架构设计
  4. 核心代码全量开发(含逐行注释)
  5. 本地开发调试与踩坑全记录
  6. Docker 容器化一键部署全流程
  7. 项目进阶优化方向
  8. 简历优化与面试高频问题
  9. 项目总结

1. 项目介绍与技术栈选型

1.1 项目背景与功能

在后端开发、运维监控场景中,实时监控系统是最常见的实战项目,既能覆盖主流后端技术栈,又能解决实际业务问题。本项目从零开发一套轻量级服务器实时监控告警系统,核心功能如下:

  • 实时数据采集:每秒生成模拟的服务器 CPU、内存、磁盘使用率监控数据
  • 消息队列解耦:基于 Redis 实现轻量级消息队列,解耦数据生产与消费,实现削峰填谷
  • 智能异常检测:基于统计学 3σ 正态分布算法,自动识别监控数据中的异常波动
  • 实时前端推送:基于 WebSocket 实现服务端到前端的毫秒级数据推送,页面无刷新实时更新
  • 异常可视化告警:前端页面对异常数据标红高亮,直观展示监控状态
  • 容器化部署:基于 Docker+Docker Compose 实现项目一键打包部署,适配服务器环境

1.2 核心技术栈选型

本项目全部采用当前行业主流稳定版本:

表格

技术 / 工具 版本 / 选型 作用
开发框架 Spring Boot 3.2.x 后端核心开发框架,自动配置、简化开发
缓存 / 消息队列 Redis 7.x 实现轻量级消息队列、数据缓存
连接池 Lettuce Spring Data Redis 默认连接池,高性能异步非阻塞
实时推送 WebSocket + STOMP 协议 实现服务端到前端的双向实时通信
前端 HTML5 + 原生 JS + SockJS 轻量化前端页面,无需复杂框架,新手易上手
代码简化 Lombok 自动生成 get/set/ 构造器,减少冗余代码
部署工具 Docker + Docker Compose 实现项目容器化打包、一键部署、环境隔离
开发环境 IDEA 2023+、JDK 17、Maven 3.8+ 本地开发环境
服务器环境 AlmaLinux 9(CentOS 替代版) 部署服务器环境,兼容 RHEL 生态

1.3 项目最终效果

  • 项目启动后,控制台每秒输出「数据生成→队列写入→数据消费→异常检测」全流程日志
  • 浏览器访问服务地址,即可看到实时刷新的监控大屏,异常数据自动标红告警
  • 基于 Docker 实现一键部署,服务器上一条命令即可启动全套服务,无需复杂环境配置

2. 前置环境准备

2.1 本地开发环境配置

  1. JDK 17 安装 :Spring Boot 3.x 要求最低 JDK 17,Oracle 官网或 OpenJDK 下载安装,配置JAVA_HOME环境变量,cmd 执行java -version验证安装成功。
  2. IDEA 安装:下载 IntelliJ IDEA 社区版 / 旗舰版,安装后配置 Maven、JDK 环境。
  3. Maven 配置:下载 Maven 3.8+,配置阿里云镜像源,避免依赖下载缓慢。
  4. FinalShell 安装:用于连接 Linux 虚拟机,传输文件、执行服务器命令。
  5. 虚拟机安装:VMware 安装 AlmaLinux 9 虚拟机,配置网络为桥接模式,确保 Windows 能 ping 通虚拟机 IP。

2.2 服务器 Docker 环境配置

虚拟机中执行以下命令,安装 Docker 与 Docker Compose:

bash

运行

复制代码
# 1. 卸载旧版本Docker
yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine

# 2. 安装yum工具
yum install -y yum-utils

# 3. 配置阿里云Docker镜像源
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 4. 安装Docker CE
yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# 5. 启动Docker并设置开机自启
systemctl start docker
systemctl enable docker

# 6. 验证安装成功
docker --version
docker compose version

2.3 Redis 容器启动

虚拟机中执行以下命令,启动 Redis 容器,用于后续开发与部署:

bash

运行

复制代码
# 拉取Redis最新镜像
docker pull redis:latest

# 启动Redis容器,设置密码、关闭保护模式、允许外部访问
docker run -d --name if-redis -p 6379:6379 redis:latest redis-server --requirepass 123456aq@ --protected-mode no --bind 0.0.0.0

# 验证容器启动成功
docker ps | grep if-redis

3. 项目整体架构设计

本项目采用分层架构设计,各模块职责单一、解耦性强,符合后端开发规范,整体架构分为 5 层,数据流转全链路如下:

plaintext

复制代码
数据采集层(定时任务生成监控数据)
    ↓
消息队列层(Redis List实现轻量级队列,解耦生产消费)
    ↓
异常检测层(消费者消费数据,3σ算法检测异常)
    ↓
实时推送层(WebSocket将结果实时推送到前端)
    ↓
前端展示层(页面实时渲染数据,异常标红告警)

各层核心职责说明:

  1. 数据采集层:基于 Spring Schedule 定时任务,每秒生成模拟的服务器监控数据,序列化为 JSON 后写入 Redis 消息队列。
  2. 消息队列层:基于 Redis List 数据结构实现轻量级消息队列,采用「左进右出」模式,通过阻塞读实现消息实时消费,解耦生产与消费逻辑,避免生产速度与消费速度不匹配导致的数据丢失。
  3. 异常检测层:项目启动后自动开启异步消费线程,阻塞读取队列中的监控数据,基于 3σ 正态分布算法判断数据是否异常,维护历史数据滑动窗口,保证检测准确性。
  4. 实时推送层:基于 WebSocket+STOMP 协议实现发布 - 订阅模式,异常检测完成后,将结果推送到指定主题,前端订阅该主题即可实时接收数据,无需轮询请求。
  5. 前端展示层:轻量化 HTML 页面,通过 SockJS+STOMP 客户端连接 WebSocket,实时接收服务端推送的数据,渲染到页面中,异常数据自动标红高亮。

4. 核心代码全量开发(含逐行注释)

4.1 项目初始化与 POM 依赖

打开 IDEA,通过 Spring Initializr 创建项目,项目名称insightflow-lite,包名com.example.insightflow_lite,选择对应依赖,最终pom.xml完整内容如下:

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 https://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>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>insightflow-lite</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>insightflow-lite</name>
    <description>轻量级实时监控告警系统</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Spring Web核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Data Redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- WebSocket依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- Lombok代码简化 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Jackson JSON序列化 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 项目启动类

Spring Boot 项目入口,无需额外配置,自动扫描同包下的所有组件:

java

运行

复制代码
package com.example.insightflow_lite;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class InsightFlowLiteApplication {
    public static void main(String[] args) {
        SpringApplication.run(InsightFlowLiteApplication.class, args);
    }
}

4.3 核心实体类开发

4.3.1 监控数据实体类 MonitorData.java

用于封装服务器监控数据,路径src/main/java/com/example/insightflow_lite/model/MonitorData.java

java

运行

复制代码
package com.example.insightflow_lite.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 服务器监控数据实体类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MonitorData {
    /**
     * 服务器ID
     */
    private String serverId = "server-01";

    /**
     * 数据采集时间
     */
    private String collectTime;

    /**
     * CPU使用率(百分比)
     */
    private double cpuUsage;

    /**
     * 内存使用率(百分比)
     */
    private double memUsage;

    /**
     * 磁盘使用率(百分比)
     */
    private double diskUsage;

    /**
     * 构造器:自动生成采集时间
     */
    public MonitorData(double cpuUsage, double memUsage, double diskUsage) {
        this.cpuUsage = Math.round(cpuUsage * 100) / 100.0;
        this.memUsage = Math.round(memUsage * 100) / 100.0;
        this.diskUsage = Math.round(diskUsage * 100) / 100.0;
        // 格式化采集时间
        this.collectTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}
4.3.2 异常检测结果实体类

在异常检测服务中定义内部类,用于封装异常检测结果,后续详细代码见 4.6 节。

4.4 数据采集生成器开发

基于 Spring Schedule 定时任务实现,每秒生成监控数据并写入 Redis 队列,路径src/main/java/com/example/insightflow_lite/task/MonitorDataGenerator.java

java

运行

复制代码
package com.example.insightflow_lite.task;

import com.example.insightflow_lite.model.MonitorData;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 * 监控数据生成器:定时生成模拟监控数据,写入Redis消息队列
 */
@Slf4j
@Component
@EnableScheduling // 开启定时任务
public class MonitorDataGenerator {
    // Redis消息队列的Key,必须和消费者保持一致
    private static final String QUEUE_KEY = "monitor:queue:server-data";
    // 随机数生成器,模拟监控数据波动
    private final Random random = new Random();
    // JSON序列化工具
    private final ObjectMapper objectMapper = new ObjectMapper();
    // Redis操作模板,构造器注入(消除字段注入警告)
    private final StringRedisTemplate redisTemplate;

    /**
     * 构造器注入:Spring推荐的注入方式,避免字段注入的不规范问题
     */
    public MonitorDataGenerator(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 定时任务:fixedRate=1000 表示每秒执行一次
     * 生成模拟监控数据,序列化为JSON后写入Redis List队列
     */
    @Scheduled(fixedRate = 1000)
    public void generateMonitorData() {
        try {
            // 生成模拟监控数据:模拟正常波动范围
            // CPU:20%-80%随机波动
            double cpu = 20 + random.nextDouble() * 60;
            // 内存:60%-90%随机波动
            double mem = 60 + random.nextDouble() * 30;
            // 磁盘:70%-95%随机波动
            double disk = 70 + random.nextDouble() * 25;

            // 封装为监控数据实体
            MonitorData data = new MonitorData(cpu, mem, disk);
            // 序列化为JSON字符串
            String dataJson = objectMapper.writeValueAsString(data);

            // 写入Redis List队列:leftPush从左侧入队,消费者从右侧出队,实现先进先出
            redisTemplate.opsForList().leftPush(QUEUE_KEY, dataJson);

            log.info("生成监控数据并写入队列:{}", dataJson);
        } catch (JsonProcessingException e) {
            log.error("监控数据JSON序列化失败", e);
        } catch (Exception e) {
            log.error("生成监控数据失败", e);
        }
    }
}

4.5 消息消费者开发

项目启动后自动开启异步线程,阻塞读取 Redis 队列中的数据,交给异常检测服务处理,路径src/main/java/com/example/insightflow_lite/consumer/MonitorDataConsumer.java

java

运行

复制代码
package com.example.insightflow_lite.consumer;

import com.example.insightflow_lite.model.MonitorData;
import com.example.insightflow_lite.service.AnomalyDetectionService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import jakarta.annotation.PostConstruct;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * 监控数据消费者:阻塞读取Redis队列中的数据,进行异常检测处理
 */
@Slf4j
@Component
public class MonitorDataConsumer {
    // 队列Key,必须和生成器保持一致
    private static final String QUEUE_KEY = "monitor:queue:server-data";
    // 单线程池:异步消费,不阻塞Spring主线程启动
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    // JSON反序列化工具
    private final ObjectMapper objectMapper = new ObjectMapper();
    // 构造器注入
    private final StringRedisTemplate redisTemplate;
    private final AnomalyDetectionService anomalyDetectionService;

    /**
     * 构造器注入
     */
    public MonitorDataConsumer(StringRedisTemplate redisTemplate, AnomalyDetectionService anomalyDetectionService) {
        this.redisTemplate = redisTemplate;
        this.anomalyDetectionService = anomalyDetectionService;
    }

    /**
     * @PostConstruct 注解:Spring Bean初始化完成后自动执行该方法
     * 作用:项目启动后自动开启消费线程
     */
    @PostConstruct
    public void startConsume() {
        executor.submit(this::consumeLoop);
        log.info("监控数据消费者已启动,开始监听队列:{}", QUEUE_KEY);
    }

    /**
     * 核心消费循环:阻塞式读取队列新消息
     * 采用Redis BRPOP命令,阻塞5秒等待新消息,无数据则返回null,避免CPU空轮询
     */
    private void consumeLoop() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 阻塞读取队列:从队列右侧弹出数据,超时时间5秒
                // 有新数据立即返回,无数据则阻塞等待,超时返回null
                String dataJson = redisTemplate.opsForList()
                        .rightPop(QUEUE_KEY, 5, TimeUnit.SECONDS);

                // 只有读取到数据才进行处理
                if (dataJson != null) {
                    // 反序列化为监控数据实体
                    MonitorData monitorData = objectMapper.readValue(dataJson, MonitorData.class);
                    // 交给异常检测服务处理
                    anomalyDetectionService.processMonitorData(monitorData);
                    log.info("成功消费监控数据:{}", monitorData);
                }
            } catch (JsonProcessingException e) {
                log.error("监控数据JSON解析失败", e);
            } catch (Exception e) {
                log.error("消费消息异常", e);
            }
        }
    }
}

4.6 异常检测服务开发

基于 3σ 正态分布算法实现异常检测,核心原理:在正态分布中,99.73% 的数据会落在均值 ±3 倍标准差的范围内,超出该范围的数据可判定为异常波动,非常适合监控数据的异常识别。

路径src/main/java/com/example/insightflow_lite/service/AnomalyDetectionService.java

java

运行

复制代码
package com.example.insightflow_lite.service;

import com.example.insightflow_lite.model.MonitorData;
import com.example.insightflow_lite.websocket.WebSocketPushService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 异常检测服务:基于3σ正态分布算法实现监控数据异常检测
 */
@Slf4j
@Service
public class AnomalyDetectionService {
    /**
     * 滑动窗口大小:保留最近50条历史数据,用于计算均值和标准差
     */
    private static final int WINDOW_SIZE = 50;
    /**
     * 历史数据存储:key为指标类型,value为历史数据列表
     */
    private final Map<String, List<Double>> historyData = new HashMap<>();
    /**
     * WebSocket推送服务
     */
    private final WebSocketPushService webSocketPushService;

    /**
     * 构造器注入
     */
    public AnomalyDetectionService(WebSocketPushService webSocketPushService) {
        this.webSocketPushService = webSocketPushService;
        // 初始化历史数据容器
        historyData.put("cpu", new ArrayList<>());
        historyData.put("mem", new ArrayList<>());
        historyData.put("disk", new ArrayList<>());
    }

    /**
     * 核心处理方法:处理监控数据,执行异常检测,推送结果到前端
     */
    public void processMonitorData(MonitorData data) {
        // 1. 收集历史数据,维护滑动窗口
        collectHistoryData("cpu", data.getCpuUsage());
        collectHistoryData("mem", data.getMemUsage());
        collectHistoryData("disk", data.getDiskUsage());

        // 2. 对每个指标执行异常检测
        AnomalyResult cpuResult = detectAnomaly("cpu", data.getCpuUsage());
        AnomalyResult memResult = detectAnomaly("mem", data.getMemUsage());
        AnomalyResult diskResult = detectAnomaly("disk", data.getDiskUsage());

        // 3. 封装检测结果
        MonitorAnomaly anomaly = new MonitorAnomaly(data, cpuResult, memResult, diskResult);

        // 4. 通过WebSocket推送到前端
        webSocketPushService.pushMonitorData(anomaly);

        // 5. 异常数据打印错误日志
        if (anomaly.isAnyAnomaly()) {
            log.error("检测到监控数据异常:{}", anomaly);
        }
    }

    /**
     * 收集历史数据,维护滑动窗口:超过窗口大小则删除最旧的数据
     */
    private void collectHistoryData(String key, double value) {
        List<Double> list = historyData.get(key);
        list.add(value);
        // 滑动窗口:超过最大长度,删除最旧的数据
        if (list.size() > WINDOW_SIZE) {
            list.remove(0);
        }
    }

    /**
     * 3σ异常检测核心算法
     * @param key 指标类型
     * @param currentValue 当前检测值
     * @return 异常检测结果
     */
    private AnomalyResult detectAnomaly(String key, double currentValue) {
        List<Double> dataList = historyData.get(key);
        // 历史数据不足10条时,不进行异常检测,避免冷启动误差
        if (dataList.size() < 10) {
            return new AnomalyResult(false, 0.0, 0.0);
        }

        // 计算历史数据的均值
        double avg = dataList.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
        // 计算历史数据的标准差
        double std = calculateStandardDeviation(dataList, avg);
        // 计算3σ上下限
        double lowerBound = avg - 3 * std;
        double upperBound = avg + 3 * std;

        // 超出上下限则判定为异常
        boolean isAnomaly = currentValue < lowerBound || currentValue > upperBound;
        return new AnomalyResult(isAnomaly, avg, std);
    }

    /**
     * 计算标准差
     */
    private double calculateStandardDeviation(List<Double> dataList, double avg) {
        double sum = 0.0;
        for (double value : dataList) {
            sum += Math.pow(value - avg, 2);
        }
        return Math.sqrt(sum / dataList.size());
    }

    /**
     * 异常检测结果实体类
     */
    @Data
    @AllArgsConstructor
    public static class AnomalyResult {
        /**
         * 是否异常
         */
        private boolean isAnomaly;
        /**
         * 历史数据均值
         */
        private double avg;
        /**
         * 历史数据标准差
         */
        private double std;
    }

    /**
     * 监控异常结果封装类
     */
    @Data
    @AllArgsConstructor
    public static class MonitorAnomaly {
        /**
         * 原始监控数据
         */
        private MonitorData rawData;
        /**
         * CPU异常检测结果
         */
        private AnomalyResult cpuResult;
        /**
         * 内存异常检测结果
         */
        private AnomalyResult memResult;
        /**
         * 磁盘异常检测结果
         */
        private AnomalyResult diskResult;

        /**
         * 判断是否有任意指标异常
         */
        public boolean isAnyAnomaly() {
            return cpuResult.isAnomaly() || memResult.isAnomaly() || diskResult.isAnomaly();
        }
    }
}

4.7 WebSocket 配置与推送服务开发

4.7.1 WebSocket 配置类

注册 STOMP 端点,配置消息代理,路径src/main/java/com/example/insightflow_lite/websocket/WebSocketConfig.java

java

运行

复制代码
package com.example.insightflow_lite.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * WebSocket配置类:启用STOMP消息代理
 */
@Configuration
@EnableWebSocketMessageBroker // 启用WebSocket消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 配置消息代理
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单内存消息代理,推送消息的前缀为/topic
        config.enableSimpleBroker("/topic");
        // 客户端发送消息的前缀,这里我们只做服务端推送,无需配置
        config.setApplicationDestinationPrefixes("/app");
    }

    /**
     * 注册STOMP端点,客户端通过该端点连接WebSocket
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册端点/ws/monitor,允许SockJS降级(浏览器不支持WebSocket时用HTTP模拟)
        registry.addEndpoint("/ws/monitor").withSockJS();
    }
}
4.7.2 WebSocket 推送服务

将异常检测结果推送到前端订阅的主题,路径src/main/java/com/example/insightflow_lite/websocket/WebSocketPushService.java

java

运行

复制代码
package com.example.insightflow_lite.websocket;

import com.example.insightflow_lite.service.AnomalyDetectionService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

/**
 * WebSocket推送服务:向客户端推送监控数据
 */
@Slf4j
@Service
public class WebSocketPushService {
    /**
     * 前端订阅的主题地址
     */
    private static final String DESTINATION = "/topic/monitor/data";
    /**
     * Spring提供的WebSocket消息发送模板
     */
    private final SimpMessagingTemplate messagingTemplate;
    /**
     * JSON序列化工具
     */
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 构造器注入
     */
    public WebSocketPushService(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    /**
     * 推送监控异常数据到前端
     */
    public void pushMonitorData(AnomalyDetectionService.MonitorAnomaly anomaly) {
        try {
            // 序列化为JSON字符串,推送到指定主题
            String json = objectMapper.writeValueAsString(anomaly);
            messagingTemplate.convertAndSend(DESTINATION, json);
            log.debug("WebSocket推送数据成功:{}", json);
        } catch (JsonProcessingException e) {
            log.error("WebSocket推送数据序列化失败", e);
        } catch (Exception e) {
            log.error("WebSocket推送数据失败", e);
        }
    }
}

4.8 控制器开发

提供健康检查接口、根路径重定向,路径src/main/java/com/example/insightflow_lite/controller/HealthController.java

java

运行

复制代码
package com.example.insightflow_lite.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 健康检查与页面跳转控制器
 */
@Controller
public class HealthController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 根路径:重定向到监控大屏页面
     */
    @GetMapping("/")
    public String index() {
        return "redirect:/index.html";
    }

    /**
     * 健康检查接口:用于验证项目是否启动成功
     */
    @GetMapping("/health")
    @ResponseBody
    public String health() {
        return "✅ InsightFlow_lite 项目启动成功!当前运行环境: "
                + System.getProperty("spring.profiles.active", "local");
    }

    /**
     * Redis连通性测试接口:验证Redis连接是否正常
     */
    @GetMapping("/test/redis")
    @ResponseBody
    public String testRedis() {
        try {
            redisTemplate.opsForValue().set("project:test:key", "InsightFlow连接成功");
            String value = redisTemplate.opsForValue().get("project:test:key");
            return "✅ Redis 连接成功!写入并读取到数据: " + value;
        } catch (Exception e) {
            return "❌ Redis 连接失败!错误信息: " + e.getMessage();
        }
    }
}

4.9 前端监控大屏开发

路径src/main/resources/static/index.html,实现实时数据渲染、异常标红告警:

html

预览

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>InsightFlow 实时监控大屏</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft YaHei", Arial, sans-serif;
        }
        body {
            background-color: #f5f7fa;
            padding: 20px;
        }
        .header {
            text-align: center;
            margin-bottom: 30px;
            color: #2c3e50;
        }
        .monitor-card {
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
            padding: 30px;
            max-width: 800px;
            margin: 0 auto;
        }
        .server-title {
            font-size: 22px;
            font-weight: bold;
            color: #2c3e50;
            margin-bottom: 20px;
            border-bottom: 2px solid #e4e7ed;
            padding-bottom: 10px;
        }
        .data-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px 0;
            border-bottom: 1px solid #f0f0f0;
            font-size: 18px;
        }
        .data-label {
            color: #606266;
            font-weight: 500;
        }
        .data-value {
            color: #303133;
            font-weight: bold;
        }
        .anomaly {
            color: #f56c6c;
            font-weight: bold;
        }
        .normal {
            color: #67c23a;
        }
    </style>
    <!-- 引入SockJS和STOMP客户端,用于连接WebSocket -->
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
</head>
<body>
    <div class="header">
        <h1>InsightFlow 实时监控大屏</h1>
    </div>

    <div class="monitor-card">
        <div class="server-title">服务器:server-01</div>
        <div class="data-item">
            <span class="data-label">采集时间</span>
            <span class="data-value" id="collectTime">--</span>
        </div>
        <div class="data-item">
            <span class="data-label">CPU使用率</span>
            <div>
                <span class="data-value" id="cpuUsage">--</span>%
                <span id="cpuAnomaly" class="normal">(正常)</span>
            </div>
        </div>
        <div class="data-item">
            <span class="data-label">内存使用率</span>
            <div>
                <span class="data-value" id="memUsage">--</span>%
                <span id="memAnomaly" class="normal">(正常)</span>
            </div>
        </div>
        <div class="data-item">
            <span class="data-label">磁盘使用率</span>
            <div>
                <span class="data-value" id="diskUsage">--</span>%
                <span id="diskAnomaly" class="normal">(正常)</span>
            </div>
        </div>
    </div>

    <script>
        // 连接WebSocket端点
        const socket = new SockJS('/ws/monitor');
        // 创建STOMP客户端
        const stompClient = Stomp.over(socket);

        // 连接成功后的回调
        stompClient.connect({}, function (frame) {
            console.log('WebSocket连接成功:' + frame);
            // 订阅监控数据主题
            stompClient.subscribe('/topic/monitor/data', function (message) {
                // 解析服务端推送的JSON数据
                const data = JSON.parse(message.body);
                console.log('收到监控数据:', data);

                // 渲染数据到页面
                document.getElementById('collectTime').textContent = data.rawData.collectTime;
                document.getElementById('cpuUsage').textContent = data.rawData.cpuUsage;
                document.getElementById('memUsage').textContent = data.rawData.memUsage;
                document.getElementById('diskUsage').textContent = data.rawData.diskUsage;

                // 更新异常状态
                updateAnomalyStatus('cpuAnomaly', data.cpuResult.isAnomaly);
                updateAnomalyStatus('memAnomaly', data.memResult.isAnomaly);
                updateAnomalyStatus('diskAnomaly', data.diskResult.isAnomaly);
            });
        });

        /**
         * 更新异常状态显示
         * @param {string} elementId 元素ID
         * @param {boolean} isAnomaly 是否异常
         */
        function updateAnomalyStatus(elementId, isAnomaly) {
            const element = document.getElementById(elementId);
            if (isAnomaly) {
                element.textContent = '(异常!)';
                element.className = 'anomaly';
            } else {
                element.textContent = '(正常)';
                element.className = 'normal';
            }
        }

        // 页面关闭时断开WebSocket连接
        window.onbeforeunload = function() {
            stompClient.disconnect();
        };
    </script>
</body>
</html>

4.10 多环境配置文件开发

Spring Boot 多环境配置,分为本地开发环境和 Docker 部署环境,避免硬编码配置,实现环境隔离。

4.10.1 主配置文件 application.yml

路径src/main/resources/application.yml,仅负责环境激活,不写具体业务配置:

yaml

复制代码
# 项目全局主配置:仅负责激活环境,不写具体业务配置
spring:
  # 环境激活开关:local(本地开发) / docker(虚拟机部署)
  profiles:
    active: local
  # 项目名称
  application:
    name: InsightFlowLite

# 日志配置:简化启动日志,方便排查问题
logging:
  level:
    # 只打印项目包下的日志,屏蔽无关依赖日志
    com.example.insightflow_lite: INFO
    # Redis连接日志(调试时开启,上线后关闭)
    org.springframework.data.redis: WARN
4.10.2 本地开发环境配置 application-local.yml

路径src/main/resources/application-local.yml,适配 Windows 本地开发,连接虚拟机 Redis:

yaml

复制代码
# 本地开发环境:连接虚拟机的Redis,使用内置Tomcat 8080端口
server:
  port: 8080
  # 关闭Tomcat访问日志(本地开发无需记录)
  tomcat:
    accesslog:
      enabled: false

# Redis配置:对接虚拟机中运行的Redis容器
spring:
  data:
    redis:
      host: 192.168.242.128  # 替换为你的虚拟机真实IP
      port: 6379
      # 匹配虚拟机Redis启动时设置的密码
      password: 123456aq@
      # 连接超时时间(防止网络波动导致连接失败)
      timeout: 5000ms
      # Redis连接池配置(性能优化)
      lettuce:
        pool:
          max-active: 8    # 最大连接数
          max-idle: 8      # 最大空闲连接
          min-idle: 2      # 最小空闲连接
4.10.3 Docker 部署环境配置 application-docker.yml

路径src/main/resources/application-docker.yml,适配容器化部署,容器间通过服务名访问 Redis:

yaml

复制代码
# Docker部署环境:通过容器名连接Redis
server:
  port: 8080
  address: 0.0.0.0  # 容器内必须监听所有IP,否则外部无法访问
  # 开启Tomcat访问日志(部署后便于排查访问问题)
  tomcat:
    accesslog:
      enabled: true
      directory: /app/logs
      prefix: access_log
      suffix: .log

# Redis配置:对接Docker网络中的redis容器
spring:
  data:
    redis:
      host: if-redis  # Docker中Redis容器名,容器间通过名称互通
      port: 6379
      password: 123456aq@
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 2

# 日志配置:容器内日志输出到控制台,便于docker logs查看
logging:
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"

5. 本地开发调试与踩坑全记录

5.1 本地调试步骤

  1. 虚拟机启动 Redis 容器 :确保虚拟机中if-redis容器处于运行状态,防火墙放行 6379 端口。
  2. 配置本地环境 :修改application-local.yml中的host为你的虚拟机真实 IP,application.ymlprofiles.active设置为local
  3. 启动项目 :IDEA 中运行InsightFlowLiteApplication启动类,控制台无报错,看到「监控数据消费者已启动」日志即为启动成功。
  4. 功能验证
    • 控制台每秒输出「生成监控数据」「成功消费监控数据」日志;
    • 浏览器访问http://localhost:8080,自动跳转到监控大屏,数据每秒刷新;
    • 访问http://localhost:8080/health,查看项目运行环境;
    • 访问http://localhost:8080/test/redis,验证 Redis 连接是否正常。

5.2 本地开发踩坑全记录(重点!新手必看)

坑 1:RedisConnectionFailureException 无法连接 Redis

报错信息org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis问题原因

  1. 虚拟机防火墙未放行 6379 端口;
  2. Redis 容器启动时未关闭保护模式,仅允许本地访问;
  3. 虚拟机 IP 配置错误,Windows 无法 ping 通虚拟机;
  4. Redis 密码配置不匹配。

解决方案

  1. 虚拟机放行 6379 端口: bash

    运行

    复制代码
    firewall-cmd --add-port=6379/tcp --permanent
    firewall-cmd --reload
  2. 重新启动 Redis 容器,关闭保护模式: bash

    运行

    复制代码
    docker stop if-redis && docker rm if-redis
    docker run -d --name if-redis -p 6379:6379 redis:latest redis-server --requirepass 123456aq@ --protected-mode no --bind 0.0.0.0
  3. 虚拟机执行ip addr确认真实 IP,修改application-local.yml中的host配置;

  4. 核对 Redis 密码,确保配置文件与启动命令中的密码完全一致。

坑 2:Spring Data Redis Stream API 无法解析方法

报错信息无法解析方法 'read(StreamOffset<String>, Duration)'问题原因 :Spring Boot 3.x 版本中,Spring Data Redis 的 Stream API 发生了破坏性变更,不同小版本的方法签名不一致,导致版本兼容问题。解决方案:放弃 Redis Stream,改用 Redis List 实现轻量级消息队列。List 是 Redis 最基础的数据结构,API 十年未变,全版本兼容,且完全满足本项目的消息队列需求,代码更简单,无兼容问题。

坑 3:消费者报「消费消息异常」,无详细日志

报错信息消费消息异常,无具体异常栈问题原因 :数据生成器写入 Redis Stream,消费者读取 Redis List,数据结构完全不匹配,导致消费失败;同时日志中仅打印了异常信息,未打印异常栈,无法定位问题。解决方案

  1. 统一数据结构:生成器和消费者都使用 Redis List,队列 Key 完全一致;
  2. 完善日志打印:log.error("消费消息异常", e),必须传入异常对象e,才能打印完整异常栈。
坑 4:切换profiles.active=docker后项目启动报错

报错信息 :Redis 连接失败,配置未加载问题原因 :缺少application-docker.yml配置文件,或配置文件中 Redis host 配置错误。解决方案 :补全application-docker.yml配置文件,容器内访问 Redis 不能用虚拟机 IP,必须用 Redis 容器名,或使用host网络模式。

坑 5:前端页面无法连接 WebSocket

报错信息 :浏览器控制台报错SockJS connection failed问题原因 :WebSocket 端点配置错误,或前端引入的 SockJS/STOMP 库地址失效。解决方案

  1. 核对 WebSocket 配置类中的端点/ws/monitor与前端连接地址一致;
  2. 使用 jsdelivr 的 CDN 地址,确保库能正常加载;
  3. 检查 Spring Boot 是否有拦截器拦截了 WebSocket 端点。

6. Docker 容器化一键部署全流程

6.1 部署前准备

  1. 修改环境配置 :将application.yml中的profiles.active改为docker

  2. 本地打包项目 :IDEA 终端执行打包命令,生成可执行 JAR 包:

    bash

    运行

    复制代码
    mvn clean package -DskipTests

    打包成功后,在项目target目录下会生成insightflow-lite-0.0.1-SNAPSHOT.jar

6.2 编写 Docker 相关配置文件

6.2.1 Dockerfile

在项目根目录新建Dockerfile,用于构建项目镜像:

dockerfile

复制代码
# 基础镜像:轻量的OpenJDK 17,匹配Spring Boot 3.x
FROM openjdk:17-jdk-slim

# 容器内工作目录
WORKDIR /app

# 复制本地打包好的JAR包到容器内,重命名为insightflow.jar
# 注意:JAR名称必须和本地target目录下的名称完全一致
COPY target/insightflow-lite-0.0.1-SNAPSHOT.jar /app/insightflow.jar

# 暴露容器内的8080端口
EXPOSE 8080

# 容器启动命令:运行JAR包,激活docker环境
ENTRYPOINT ["java", "-jar", "insightflow.jar", "--spring.profiles.active=docker"]
6.2.2 docker-compose.yml

在项目根目录新建docker-compose.yml,实现应用 + Redis 一键启动:

yaml

复制代码
version: '3.8'
services:
  # 监控应用服务
  insightflow-app:
    build: .  # 基于当前目录的Dockerfile构建镜像
    image: insightflow:v1.0
    container_name: insightflow-app
    ports:
      - "8080:8080"  # 端口映射:宿主机8080 → 容器8080
    volumes:
      - ./app-logs:/app/logs  # 挂载日志目录到宿主机,避免日志丢失
    depends_on:
      - if-redis  # 依赖Redis服务,先启动Redis再启动应用
    restart: always  # 异常崩溃自动重启
    networks:
      - insightflow-network

  # Redis服务,和本地开发用同一个容器名
  if-redis:
    image: redis:7-alpine
    container_name: if-redis
    ports:
      - "6379:6379"
    # 启动命令:设置密码、关闭保护模式、允许所有IP访问
    command: redis-server --requirepass 123456aq@ --protected-mode no --bind 0.0.0.0
    volumes:
      - redis-data:/data  # 数据持久化,重启容器不丢失数据
    restart: always
    networks:
      - insightflow-network

# 自定义网络:容器间通过网络互通
networks:
  insightflow-network:
    driver: bridge

# 数据卷:持久化Redis数据
volumes:
  redis-data:

6.3 服务器部署步骤

  1. 传输文件到服务器 :通过 FinalShell 将以下文件传输到虚拟机的/root/insightflow目录:

    • 本地target目录下的 JAR 包;
    • 项目根目录的Dockerfile
    • 项目根目录的docker-compose.yml
  2. 服务器创建目录

    bash

    运行

    复制代码
    mkdir -p /root/insightflow
    cd /root/insightflow
  3. 构建并启动服务

    bash

    运行

    复制代码
    # 构建镜像并启动所有服务,-d表示后台运行
    docker compose up -d --build
  4. 验证容器启动状态

    bash

    运行

    复制代码
    # 查看容器运行状态,确保两个容器都是Up状态
    docker compose ps
  5. 查看应用启动日志

    bash

    运行

    复制代码
    # 实时查看应用日志,确认启动成功无报错
    docker compose logs -f insightflow-app

    日志中看到Started InsightFlowLiteApplication in X seconds,且无 Redis 连接报错,即为部署成功。

6.4 部署验证

  1. 服务器内验证

    bash

    运行

    复制代码
    # 访问健康检查接口
    curl http://localhost:8080/health
    # 正常返回:✅ InsightFlow_lite 项目启动成功!当前运行环境: docker
  2. 外部访问验证 :浏览器访问http://虚拟机IP:8080,即可看到实时监控大屏,数据每秒刷新,异常自动标红。

  3. Redis 数据验证

    bash

    运行

    复制代码
    # 进入Redis容器
    docker exec -it if-redis redis-cli -a 123456aq@
    # 查看队列长度,正常应接近0,说明生产消费速度匹配
    LLEN monitor:queue:server-data

6.5 常用运维命令

bash

运行

复制代码
# 停止所有服务
docker compose down

# 重启应用服务
docker compose restart insightflow-app

# 查看实时日志
docker compose logs -f insightflow-app

# 删除镜像(如需重新构建)
docker rmi insightflow:v1.0

7. 项目进阶优化方向

7.1 可视化升级

引入 ECharts 可视化库,将监控数据从简单的数值展示升级为折线图、仪表盘、柱状图,展示 CPU / 内存 / 磁盘的历史趋势,提升页面的专业度和可视化效果。

7.2 多服务器监控支持

修改数据生成器,支持多服务器实例的数据采集,前端页面通过多卡片的形式展示不同服务器的监控状态,实现集群监控能力。

7.3 异常告警通知

新增告警服务,集成钉钉 / 企业微信 / 邮件机器人,当检测到异常数据时,自动推送告警消息到手机 / 企业微信,实现真正的监控告警能力。

7.4 数据持久化与历史查询

引入 MySQL/InfluxDB 时序数据库,将监控数据持久化存储,新增历史数据查询接口,前端页面支持按时间范围查询历史监控数据,生成趋势报表。

7.5 权限管理与用户系统

引入 Spring Security + JWT,实现用户登录、权限管理,不同用户只能查看对应权限的服务器监控数据,提升系统的安全性。

7.6 集群化与高可用

实现 Redis 集群部署,避免单点故障;应用服务多实例部署,通过 Nginx 实现负载均衡,提升系统的并发能力和可用性。


8. 项目总结

本项目从零到一完成了一套完整的实时监控告警系统,覆盖了 Spring Boot 后端开发、Redis 中间件使用、WebSocket 实时通信、Docker 容器化部署等后端开发核心技能,全程包含完整的代码实现、踩坑记录、部署流程,非常适合 Java 后端新手入门实战

通过本项目的开发,你可以掌握:

  1. Spring Boot 项目的分层架构设计、规范开发流程;
  2. Redis 的核心使用场景,消息队列的实现原理;
  3. 实时通信场景的技术选型,WebSocket 的开发与使用;
  4. 统计学算法在业务场景中的落地应用;
  5. Docker 容器化开发、部署的全流程;
  6. 项目开发中的问题排查、踩坑解决能力。

后续可以基于本项目继续扩展优化,不断提升项目的复杂度和技术深度,逐步掌握企业级后端开发的核心能力。如果在项目开发过程中遇到任何问题,欢迎在评论区留言交流。

相关推荐
二月夜2 小时前
记SpringBoot升级Tomcat引发的两类典型问题及解决方案
spring boot·后端·tomcat
李白的粉2 小时前
基于springboot的来访管理系统
java·spring boot·毕业设计·课程设计·源代码·来访管理系统
乄bluefox2 小时前
Redis Pipeline 实战:Spring Data Redis 批量写入最佳实践
java·redis·spring
05大叔2 小时前
Docker
运维·docker·容器
天才梦浪2 小时前
wsl的网络导致springboot启动提示端口占用
网络·spring boot·后端
此方ls2 小时前
Redis源码研读八——listpack.c 1080-1528行
c语言·数据库·redis
袁煦丞 cpolar内网穿透实验室2 小时前
Portainer可视化玩转 Docker 全流程。cpolar 内网穿透实验室第 737 个成功挑战
运维·docker·容器·远程工作·内网穿透·cpolar
wellc2 小时前
redis连接服务
数据库·redis·bootstrap
Chan162 小时前
LeetCode 热题 100 | 链表
java·数据结构·spring boot·算法·leetcode·链表·java-ee