游乐游手机版
首页/编程语言/文章详情

SpringBoot+Vue实时设备告警系统全链路实现方案

时间:2026-07-22 19:19
通过边沿检测与状态机实现告警去重与生命周期管理,以数据库未闭合记录作为状态源确保幂等性,结合REST接口与前端轮询或推送,构成从采集、判定、持久化到展示闭环的实时设备告警系统全链路方案。

实时监控告警功能看似简单,但在实际落地过程中往往会遇到诸多挑战——重复报警、僵尸告警、前端刷屏等问题层出不穷。本文将从数据采集、告警判定、持久化、接口暴露、前端展示、闭环处理到兜底补偿,完整梳理整个实时告警系统的实现链路。每个环节的关键设计要点和通用代码示例都会给出,并尽量与具体业务解耦,方便直接参考复用。

基于SpringBoot和Vue的实时设备告警系统全链路实现方案

一、整体架构与数据流

一个典型的实时告警系统,拆解来看,主要包含以下五个核心环节:

┌──────────┐   采集    ┌──────────┐  判定/去重  ┌──────────┐
│ 数据源    │ ───────▶ │ 采集任务  │ ─────────▶ │  告警表   │
│(设备/API)│  周期轮询  │(定时/流) │  生成/闭环  │ (DB)     │
└──────────┘           └──────────┘            └────┬─────┘
                                                     │ REST
                                              ┌──────▼─────┐   推送/轮询  ┌─────────┐
                                              │ 后端接口层  │ ──────────▶ │ 前端界面 │
                                              └────────────┘             │ +提醒    │
                                                                         └─────────┘

设计时,需要提前确立几个关键原则:

  • 采集与判定解耦:采集层只负责获取原始状态数据,判定逻辑独立成模块,这样后续新增告警类型时无需修改采集代码。
  • 状态化而非事件流:告警落库应当是一条有生命周期的记录(包含 start/stop 时间),而非无休止的日志流。这样才能高效查询“当前未恢复告警”,避免每次扫描全量数据。
  • 前端只读 + 幂等操作:前端不参与告警的产生逻辑,只负责展示和“确认/处理”等幂等状态流转。这样设计简单可靠,减少运维复杂度。

二、告警生成层:边沿检测与状态机

2.1 电平 vs 边沿

采集到的原始状态通常是电平信号——布尔量,故障=true,正常=false。如果每个采集周期只要 true 就插入一条告警,2秒一采集,一分钟就能产生30条,一天下来表里全是重复数据。

正确的做法是采用边沿检测(Edge Detection):仅在状态发生跳变时触发动作。

边沿上一次本次动作
上升沿falsetrue生成新告警(记录 start_time)
下降沿truefalse关闭告警(记录 stop_time、时长)
保持相同相同什么都不做

2.2 通用示例:边沿检测器

/**
 * 通用布尔量边沿检测器,线程安全
 * key 通常是 "设备ID:信号名"
 */
public class EdgeDetector {

    private final Map lastState = new ConcurrentHashMap<>();

    public enum Edge { RISING, FALLING, NONE }

    public Edge detect(String key, boolean current) {
        Boolean prev = lastState.put(key, current);
        if (prev == null) {
            // 首次采集:把 true 视为上升沿,false 视为无事件
            return current ? Edge.RISING : Edge.NONE;
        }
        if (!prev && current) return Edge.RISING;
        if (prev && !current) return Edge.FALLING;
        return Edge.NONE;
    }
}

2.3 在采集周期中使用

EdgeDetector detector = new EdgeDetector();

void onSample(String deviceId, String signal, boolean faultBit, Date now) {
    String key = deviceId + ":" + signal;
    switch (detector.detect(key, faultBit)) {
        case RISING:
            alarmService.open(deviceId, signal, now);   // 生成告警
            break;
        case FALLING:
            alarmService.close(deviceId, signal, now);  // 闭合告警
            break;
        default:
            // 状态未变,忽略
    }
}

状态存哪里? 单机场景可以使用内存 Map;多实例或需要重启后不丢失状态,可以将“上一次状态”存放在 Redis 中,或者直接以数据库中“是否存在未闭合告警”作为判断依据(见第3节,这种方式最稳定,无需额外状态存储)。

三、告警去重与生命周期

相比内存边沿检测,更可靠的方案是:以数据库中“是否存在未闭合记录”作为状态源。这种方式天然幂等、重启不丢失、多实例也安全。

3.1 生命周期模型

一条告警记录包含三个关键时间点和两个派生字段:

start_time ──────(持续中)──────▶ stop_time
                                  total_time = stop - start(秒)
handle_status: 待确认 → 已确认 → 已处理(或 超时未确认)

3.2 开告警(带去重)

public void open(Long deviceId, String alarmName, Date startTime) {
    // 去重:若已存在该设备该类型"未闭合"告警,则不再新建
    if (mapper.selectUnfinished(deviceId, alarmName) != null) {
        return;
    }
    Alarm a = new Alarm();
    a.setDeviceId(deviceId);
    a.setAlarmName(alarmName);
    a.setStartTime(startTime);
    a.setAlarmLevel(resolveLevel(alarmName));   // 名称→等级映射
    a.setHandleStatus(STATUS_PENDING);          // 默认待确认
    mapper.insert(a);
}

3.3 关告警(计算时长)

public boolean close(Long deviceId, String alarmName, Date stopTime) {
    Alarm a = mapper.selectUnfinished(deviceId, alarmName);
    if (a == null) return false;                // 没有未闭合记录,忽略

    long seconds = (stopTime.getTime() - a.getStartTime().getTime()) / 1000;
    return mapper.updateStop(a.getId(), stopTime, seconds) > 0;
}

3.4 名称 → 等级映射(可配置化)

将“告警名称对应严重等级”抽成映射表,后续新增告警只需修改配置,无需改动逻辑:

public class AlarmLevel {
    public static final int INFO = 1, WARN = 2, SERIOUS = 3, FATAL = 4;

    private static final Map MAP = new HashMap<>();
    static {
        MAP.put("急停", FATAL);
        MAP.put("高压报警", SERIOUS);
        MAP.put("低流量报警", WARN);
        // ...
    }
    /** 未命中默认按严重处理,保证"漏配也不漏报" */
    public static int of(String name) {
        return MAP.getOrDefault(name, SERIOUS);
    }
}

更进一步,可以将映射表放到数据库字典表(sys_dict_data)或独立阈值配置表中,实现运营可视化配置。

四、数据库层设计

4.1 建表 DDL(通用示例)

CREATE TABLE device_alarm (
    id            BIGINT       NOT NULL AUTO_INCREMENT COMMENT '主键',
    device_id     BIGINT       NOT NULL               COMMENT '设备ID',
    alarm_name    VARCHAR(64)  NOT NULL               COMMENT '告警名称/类型',
    alarm_level   TINYINT      NOT NULL DEFAULT 3      COMMENT '等级 1提示2预警3严重4致命',
    start_time    DATETIME     NOT NULL               COMMENT '开始时间',
    stop_time     DATETIME     NULL                   COMMENT '结束时间(NULL=未恢复)',
    total_time    INT          NULL                   COMMENT '持续秒数',
    handle_status TINYINT      NOT NULL DEFAULT 0      COMMENT '0待确认1已确认2超时3已处理',
    handle_user   VARCHAR(64)  NULL                   COMMENT '处理人',
    handle_time   DATETIME     NULL                   COMMENT '处理时间',
    handle_remark VARCHAR(500) NULL                   COMMENT '处理备注',
    create_time   DATETIME     NOT NULL               COMMENT '创建时间',
    PRIMARY KEY (id),
    KEY idx_device_unfinished (device_id, alarm_name, stop_time),
    KEY idx_stop_time (stop_time),
    KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备告警表';

4.2 索引设计要点

查询场景索引说明
“该设备该类型是否有未闭合告警”(去重、闭合)(device_id, alarm_name, stop_time)最高频写路径,联合索引命中
“当前所有未恢复告警”(大屏轮询)idx_stop_timeWHERE stop_time IS NULL
历史分页/导出idx_create_time按时间倒序翻页

stop_time IS NULL 表示未恢复——使用 NULL 而非额外的 is_finished 布尔字段,语义清晰且能与时间字段复用索引。

4.3 关键 SQL

-- 查未闭合(去重判断)
SELECT * FROM device_alarm
WHERE device_id = #{deviceId} AND alarm_name = #{alarmName}
  AND stop_time IS NULL LIMIT 1;
-- 闭合告警
UPDATE device_alarm
SET stop_time = #{stopTime}, total_time = #{seconds}
WHERE id = #{id} AND stop_time IS NULL;   -- 加 stop_time IS NULL 防并发重复闭合
-- 当前未恢复列表
SELECT a.*, d.device_name
FROM device_alarm a LEFT JOIN device d ON a.device_id = d.id
WHERE a.stop_time IS NULL
ORDER BY a.alarm_level DESC, a.start_time DESC;

并发防护UPDATE ... WHERE id=? AND stop_time IS NULL 利用行锁 + 条件,天然防止两个线程重复闭合同一告警(第二个 UPDATE 影响 0 行)。

五、后端接口层:REST API 设计

5.1 接口清单(RESTful + 语义动作)

方法路径用途
GET/alarm/list分页历史查询
GET/alarm/current当前未恢复告警(前端轮询)
GET/alarm/{id}详情
POST/alarm/confirm/{id}确认告警
POST/alarm/batchConfirm批量确认
POST/alarm/handle处理闭环(带备注)
POST/alarm/export导出 Excel

查询采用 REST 名词 + GET;“确认/处理”这类状态流转使用动词子路径(/confirm/handle),比强行 PUT 整个对象更清晰、更安全——避免前端误改其他字段。

5.2 通用控制器示例

@RestController
@RequestMapping("/alarm")
public class AlarmController {

    @Autowired private AlarmService service;

    /** 前端轮询:当前所有未恢复告警 */
    @GetMapping("/current")
    public Result> current() {
        return Result.ok(service.listUnfinished());
    }

    /** 确认(幂等) */
    @PostMapping("/confirm/{id}")
    public Result confirm(@PathVariable Long id) {
        service.confirm(id, currentUser());
        return Result.ok();
    }

    /** 处理闭环 */
    @PostMapping("/handle")
    public Result handle(@RequestParam Long id,
                               @RequestParam(required = false) String remark) {
        service.handle(id, remark, currentUser());
        return Result.ok();
    }
}

5.3 权限与审计

  • 每个写接口都需要添加权限校验(如 Spring Security @PreAuthorize)。
  • “确认/处理”操作要记录操作人 + 操作时间,形成完整的审计链,便于责任追溯。

六、前端展示层:三种实时推送方案对比

前端需要“实时”感知新告警,有三种主流技术路线可选:

方案原理优点缺点适用
轮询 PollingsetInterval 定时请求实现最简单、无长连接、兼容性强有延迟、有空请求开销秒级实时够用、部署简单
SSEEventSource 服务端单向推原生断线重连、比 WebSocket 轻量单向、老 IE 不支持只需服务端→客户端推送
WebSocket全双工长连接真正实时、双向需维护连接/心跳/鉴权高频、双向交互

6.1 轮询(最常用,通用示例)

class AlarmPoller {
  constructor(fetchFn, { interval = 5000, onNew } = {}) {
    this.fetchFn = fetchFn;      // 返回 Promise
    this.interval = interval;
    this.onNew = onNew;
    this.knownIds = new Set();
    this.timer = null;
  }

  start() {
    this.tick();                 // 立即拉一次
    this.timer = setInterval(() => this.tick(), this.interval);
  }

  async tick() {
    try {
      const list = await this.fetchFn();
      // 找出本轮"新出现"的告警
      const fresh = list.filter(a => !this.knownIds.has(a.id));
      list.forEach(a => this.knownIds.add(a.id));
      // 清理已恢复的 id,防止 Set 无限膨胀
      const alive = new Set(list.map(a => a.id));
      this.knownIds = new Set([...this.knownIds].filter(id => alive.has(id)));
      if (fresh.length && this.onNew) this.onNew(fresh);
    } catch (e) {
      console.error('轮询告警失败', e);   // 失败不中断下一轮
    }
  }

  stop() { clearInterval(this.timer); this.timer = null; }
}

// 用法
const poller = new AlarmPoller(() => api.getCurrentAlarms(), {
  interval: 5000,
  onNew: (alarms) => alarms.forEach(a => alarmManager.alarm({
    title: a.deviceName, body: a.alarmName, level: mapLevel(a.alarmLevel),
  })),
});
poller.start();

轮询的两个关键技巧:

  1. “新增”判定采用 id 集合 diff,不要每次全量弹提醒,否则同一告警会反复触发。
  2. 及时清理已恢复的 id,避免 Set 内存泄漏。

6.2 SSE(服务端主动推)

// 前端
const es = new EventSource('/alarm/stream');
es.addEventListener('alarm', (e) => {
  const alarm = JSON.parse(e.data);
  alarmManager.alarm({ title: alarm.deviceName, body: alarm.alarmName });
});
es.onerror = () => console.warn('SSE 断开,浏览器将自动重连');
// 后端 Spring MVC
@GetMapping(value = "/alarm/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
    SseEmitter emitter = new SseEmitter(0L);       // 不超时
    emitterRegistry.add(emitter);                  // 存起来,产生告警时广播
    emitter.onCompletion(() -> emitterRegistry.remove(emitter));
    return emitter;
}
// 产生新告警时:emitter.send(SseEmitter.event().name("alarm").data(alarm));

6.3 WebSocket(双向)

const ws = new WebSocket('wss://host/ws/alarm');
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === 'ALARM') alarmManager.alarm(msg.payload);
};
// 心跳保活
setInterval(() => ws.readyState === 1 && ws.send('ping'), 30000);

选型建议:秒级监控大屏优先轮询(最稳定、运维成本最低);需要毫秒级或服务端主动推送且不想维护重连时建议使用 SSE;有双向指令交互(如页面下发控制命令)才考虑 WebSocket

七、前端提醒层

新告警到达后,触发“提醒三件套”:

  • Web Audio 蜂鸣:根据告警等级播放不同急促度的合成音,无需音频文件。
  • 浏览器 Notification:仅在 document.visibilityState !== 'visible'(用户切走)时弹出系统通知,避免前台重复骚扰。
  • 标题 / fa vicon 红点角标:后台时使用 document.title 计数 + Canvas 绘制 fa vicon 角标,将用户“拉回来”。

统一入口示例:

poller.onNew = (alarms) => {
  alarms.forEach(a => alarmManager.alarm({
    title: a.deviceName,
    body: a.alarmName,
    level: mapLevel(a.alarmLevel),
    tag: 'alarm-' + a.id,        // 去重,防刷屏
  }));
};

八、闭环处理:确认 / 处理 / 超时

告警不能只“响”,必须能够被人闭环,否则就变成了噪音。典型的状态机如下:

        产生
待确认 ───────▶ (用户点确认) ──▶ 已确认 ──▶ (处理完+备注) ──▶ 已处理
   │
   └─(超过N分钟无人确认)──▶ 超时未确认   ← 用于考核值班响应

8.1 确认(幂等)

public void confirm(Long id, String user) {
    Alarm a = new Alarm();
    a.setId(id);
    a.setHandleStatus(STATUS_CONFIRMED);
    a.setHandleUser(user);
    a.setHandleTime(new Date());
    mapper.updateSelective(a);      // 只更新非空字段
}

8.2 前端确认交互

async function confirmAlarm(id) {
  await api.confirm(id);
  this.$message.success('已确认');
  this.refresh();          // 刷新当前告警列表
  alarmManager.clear();    // 清角标/停声音
}

幂等性:确认/处理接口需要能被重复调用而不出错(重复点击、网络重试)。使用“设置目标状态”而非“状态+1”即可天然实现幂等。

九、兜底补偿:僵尸告警与数据自愈

实时系统必须考虑异常路径,否则数据会逐渐失真:

9.1 两类典型脏数据

问题成因后果
僵尸告警恢复边沿丢失(设备离线、缓存过期、进程重启)stop_time 永远为 NULL,大屏持续显示红色报警
超时未确认无人值守,告警长期处于“待确认”状态无法考核响应时效,告警不断堆积

9.2 兜底任务(定时扫描自愈)

/** 每分钟跑一次,修复未闭合告警 */
public void fallback() {
    Date now = new Date();
    for (Alarm a : mapper.selectAllUnfinished()) {
        long elapsedMin = (now.getTime() - a.getStartTime().getTime()) / 60000;

        // 1) 超时未确认 → 标记
        if (a.getHandleStatus() == STATUS_PENDING && elapsedMin >= TIMEOUT_MIN) {
            mapper.markTimeout(a.getId());
        }

        // 2) 僵尸告警 → 若数据源已恢复正常/持续离线超阈值,则强制关闭
        Boolean live = readCurrentState(a);           // 从缓存/实时源读当前状态
        if (Boolean.FALSE.equals(live)) {
            mapper.forceClose(a.getId(), now, "点位已恢复,兜底关闭");
        } else if (live == null && offlineTooLong(a)) {
            mapper.forceClose(a.getId(), now, "设备持续离线,兜底关闭");
        }
    }
}

9.3 防误关的两个细节

  • 离线要“持续超阈值”才关闭:使用一个 Map 记录,避免瞬时网络抖动导致误关正常告警。
  • 区分“离线”与“恢复”:数据源读取不到(null)表示离线,读取到且为正常表示真恢复,两者处理方式不同。

十、关键设计要点汇总

要点
采集/判定使用边沿检测而非电平,避免大量重复数据;判定逻辑与采集解耦
去重以“数据库是否存在未闭合记录”为状态源,幂等且重启不丢失
生命周期stop_time IS NULL 表示未恢复;闭合时计算 total_time
并发闭合 UPDATE 带 AND stop_time IS NULL 防止重复闭合
等级名称→等级映射表化/字典化,漏配默认从严
数据库联合索引 (device_id, alarm_name, stop_time) 覆盖写路径
接口查询使用 REST 名词,状态流转使用动词子路径;写接口需鉴权 + 审计
推送选型秒级用轮询,单向实时用 SSE,双向用 WebSocket
前端去重id 集合 diff 判断“新增”,并清理已恢复 id 防止内存泄漏
提醒声音 + 仅后台通知 + 角标;使用 tag 去重防刷屏
闭环确认/处理幂等,记录操作人与时间
兜底定时扫描修复僵尸告警与超时未确认;防止抖动误关
健壮性轮询/推送失败不中断循环;异常路径必须有兜底补偿

以上即“实时设备告警”功能从数据源到界面的全链路技术详解。核心思想可提炼为三句话:采集判定用边沿、状态落库带生命周期、异常路径必有兜底;而前端则遵循只读展示 + 幂等闭环 + 恰到好处的提醒。这套模式可以直接迁移到设备监控、服务器运维告警、业务风控预警等任意实时告警场景。

来源:https://www.jb51.net/program/3678389f7.htm
上一篇IntelliJIDEA乱码问题解决 改编码设置亲测有效 下一篇SpringBoot项目引入本地JAR包详细步骤
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
FileZilla断点续传设置与操作指南
编程语言 · 2026-07-25

FileZilla断点续传设置与操作指南

FileZilla支持断点续传,需客户端与服务器均开启REST命令。设置中确保启用断点续传及继续传输选项。中断后自动或手动从断点恢复。注意服务器支持、传输模式匹配及文件完整性校验。

Debian系统C++编译器位置查找方法
编程语言 · 2026-07-25

Debian系统C++编译器位置查找方法

在Debian系统中,通过apt安装的C++编译器g++默认位于 usr bin g++,可使用which或whereis命令验证路径。g++属于build-essential软件包,若未安装则需执行sudoaptinstallbuild-essential。该包还包含gcc、make等编译工具链,g++是GNUC++编译器,实际是符号链接指向具体版本,验证

Debian系统安装C++环境的方法
编程语言 · 2026-07-25

Debian系统安装C++环境的方法

在Debian系统安装C++开发环境:先sudoaptupdate更新包列表,再sudoaptinstallbuild-essential安装编译工具链,或单独安装g++。用g++--version验证。可选安装VSCode、GDB、CMake等工具并配置默认编译器版本。

Debian系统C++开发环境配置指南
编程语言 · 2026-07-25

Debian系统C++开发环境配置指南

在Debian系统中,先执行aptupdate更新软件包列表,再安装build-essential元包即可获得GCC、G++、Make和GDB。通过运行g++--version命令验证编译器安装成功。可选安装VisualStudioCode、CLion等编辑器及CMake构建工具,并编写一个简单的HelloWorld程序,使用g++编译运行以验证环境配置正确

通过cpustat工具查看CPU状态的具体方法与详细步骤
编程语言 · 2026-07-25

通过cpustat工具查看CPU状态的具体方法与详细步骤

cpustat是sysstat包中的CPU监控工具,可按固定间隔输出带时间戳的CPU使用率统计。安装后运行cpustat即可实时显示各核心信息,常用指标包括%usr、%sys、%iowait、%steal和%idle,用于定位用户态、内核态或I O瓶颈。高级选项-c可显示单核统计,-m可同时查看内存使用,适合脚本采集和性能分析。