SpringBoot中实现告警判定算法-位报警与模拟量阈值-技术详解

SpringBoot中实现告警判定算法-位报警与模拟量阈值-技术详解

适用场景:IoT / 工业监控 / 设备告警 / 任何"周期采样 → 判定是否告警 → 落库 → 恢复"的系统。


目录

  1. 为什么告警判定要分两类
  2. 第一类:位报警(布尔量)------ 边沿检测
  3. 第二类:模拟量阈值 ------ 双阈值迟滞(回差)
  4. 两类算法对比
  5. 告警生命周期与去重(两类共用)
  6. 告警分级映射
  7. 本项目落地实现解析
  8. 通用示例代码
  9. 设计要点与踩坑清单

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

一、为什么告警判定要分两类

设备采集回来的信号本质上分两种物理类型,判定方式完全不同:

信号类型 例子 取值 判定方式
开关量 / 布尔量(位) 急停、水泵故障、风机故障、缺水故障 true/false(1 个 bit) 边沿检测:状态跳变才动作
模拟量 / 连续量 粉尘浓度 TSP、压力、流量、电流 连续数值 阈值比较:越过阈值才动作,且要防抖动

如果对这两类用同一种判定方式,会出问题:

  • 布尔量若"只要为 true 就插一条告警",2 秒一采会瞬间刷出成千上万条重复记录。
  • 模拟量若"只要 值 > 阈值 就告警、值 <= 阈值 就恢复",当数值在阈值附近抖动时会疯狂地"告警---恢复---告警---恢复"(告警抖动 / flapping)。

因此需要两套算法:位报警用边沿检测去重模拟量用双阈值迟滞防抖


二、第一类:位报警(布尔量)------ 边沿检测

2.1 电平 vs 边沿

  • 电平(Level):信号当前的状态值(true/false)。
  • 边沿(Edge) :信号状态发生的跳变
    • 上升沿 :false → true(正常→故障),此刻应生成告警
    • 下降沿 :true → false(故障→正常),此刻应恢复告警
    • 保持(状态未变):什么都不做。

只在"跳变"时动作,就天然实现了去重------一次故障从发生到恢复,只产生一条告警记录(一次上升沿开、一次下降沿关)。

2.2 算法本质:本次值与上次值对比

边沿检测需要保存"上一次的状态"。每轮采集把"本次值"与"上次值"对比:

复制代码
if (!oldStatus && newStatus)  -> 上升沿 -> 生成告警
if (oldStatus && !newStatus)  -> 下降沿 -> 恢复告警
否则                          -> 无动作

状态存哪里? 本项目把上一轮整包 PLC 数据缓存在 Redis(10s 过期),下一轮取出来做对比,既做了边沿检测的"上次状态源",又避免了额外维护状态表。

2.3 空值处理

传感器/通信可能读到 null。判定前要归一化(本项目把 null 视为"正常 false"),避免 NPE 且语义安全:

java 复制代码
oldStatus = oldStatus == null ? false : oldStatus;
newStatus = newStatus == null ? false : newStatus;

三、第二类:模拟量阈值 ------ 双阈值迟滞(回差)

3.1 单阈值的抖动问题

若只用一个阈值 T值 > T 告警、值 <= T 恢复。当实际值在 T 附近小幅波动(如 T=150,值在 149~151 抖动)时,会产生大量"告警→恢复→告警→恢复",即告警抖动(flapping),既刷屏又无意义。

3.2 迟滞(Hysteresis)/ 回差带

解决办法是用两个阈值构成一个"回差带":

  • 报警阈值 alarmThreshold(高)值 > alarmThreshold触发告警。

  • 预警/恢复阈值 warnThreshold(低)值 <= warnThreshold恢复告警。

  • 两者之间的区间(warnThreshold < 值 <= alarmThreshold)是"死区",维持当前状态不变。

    浓度

    │ ┌─────────── 告警区(> alarmThreshold 触发)
    alarmThreshold ─┤
    │ │ 死区(维持现状,不翻转)
    warnThreshold ──┤
    │ └─────────── 恢复区(<= warnThreshold 才恢复)
    └───────────────────────────▶ 时间

这样,值必须先冲高越过报警线 才告警,之后必须回落到预警线以下才恢复------中间的小抖动被死区吸收,告警不再来回翻转。这正是工业上常见的"施密特触发器(Schmitt Trigger)"思想。

3.3 采样频率控制

模拟量还常配合采样降频:即使采集任务 2s 一轮,TSP 可以只每 N 秒采样并入库一次(比如tsp.sample.interval` 默认 10s),减少历史数据量与写入压力。


四、两类算法对比

维度 位报警(边沿检测) 模拟量阈值(双阈值迟滞)
输入 布尔量(1 bit) 连续数值
触发条件 上升沿 false→true 值 > alarmThreshold
恢复条件 下降沿 true→false 值 <= warnThreshold
去重手段 依赖"上次状态"对比 依赖回差带 + 未闭合记录判断
防抖手段 天然去重(跳变才动作) 回差带(死区)吸收抖动
状态源 上次采集值(本项目存 Redis) 当前值 + 数据库未恢复记录
典型信号 急停、水泵/风机故障、缺水 粉尘浓度、压力、温度

五、告警生命周期与去重(两类共用)

不论哪类算法,产生的告警都落成一条有生命周期的记录:

复制代码
start_time ────(持续中,stop_time = NULL)────▶ stop_time
                                               total_time = stop - start
handle_status: 0待确认 → 1已确认 / 2超时未确认 → 3已处理

5.1 触发(开告警)

生成一条新记录:设置设备/料棚、告警名、开始时间、等级、处理状态=待确认、备注。

5.2 恢复(关告警)

找到该设备该告警"未闭合"的记录,写入 stop_time(并可计算持续时长)。

5.3 去重的两种实现

  • 位报警:靠"上次状态"对比。只有上升沿才插入,天然不会重复插。
  • 模拟量 :额外用"数据库是否存在未恢复记录 "兜底去重(selectUnfinishedAlarm(...) != null)。这样即使进程重启(内存态丢失)或多实例并发,也不会对同一持续告警重复插入。

经验:以"数据库未闭合记录"作为状态源,比纯内存状态更健壮(重启不丢、多实例安全),是模拟量告警推荐的去重方式。


六、告警分级映射

把"告警名称 → 严重等级"抽成映射表,新增告警只改配置、不改判定逻辑:

  • 等级:1 提示 / 2 预警 / 3 严重 / 4 致命。
  • 未命中默认按"严重"处理,保证"漏配也不漏报"。

AlarmConstants 的映射示例:急停、总故障=致命(4);各类故障=严重(3);低流量、TSP 超标=预警(2)。


七、项目落地实现解析

7.1 位报警:checkAlarmStatus(边沿检测)

采集任务每轮取出上一轮缓存的 oldData 与本轮 newData,对 11 类布尔故障逐一做边沿检测:

java 复制代码
private void checkAlarmStatus(BusDevice device, Long shedId, String alarmName,
                              Boolean oldStatus, Boolean newStatus, Date now) {
    Long deviceId = device.getId();
    oldStatus = oldStatus == null ? false : oldStatus;   // 空值归一化
    newStatus = newStatus == null ? false : newStatus;

    // 上升沿:正常→报警,生成新告警
    if (!oldStatus && newStatus) {
        BusDeviceAlarm alarm = new BusDeviceAlarm();
        alarm.setShedId(shedId);
        alarm.setDeviceId(deviceId);
        alarm.setAlarmName(alarmName);
        alarm.setStartTime(now);
        alarm.setAlarmLevel(AlarmConstants.getAlarmLevel(alarmName)); // 名称→等级
        alarm.setHandleStatus(AlarmConstants.HANDLE_STATUS_PENDING);
        alarm.setRemark("PLC触发" + alarmName + ",设备:" + device.getDeviceName()
                + ",PLC:" + device.getPlcAddress());
        busDeviceAlarmService.insertBusDeviceAlarm(alarm);
    }

    // 下降沿:报警→正常,恢复告警
    if (oldStatus && !newStatus) {
        busDeviceAlarmService.updateAlarmStopTime(deviceId, alarmName, now);
    }
}

调度侧对多类故障批量判定(急停、风机/水泵故障、摇摆/仰俯/超限位/编码器/缺水故障、总故障等):

java 复制代码
checkAlarmStatus(device, shedId, "急停", oldData.getEStop(), newData.getEStop(), now);
checkAlarmStatus(device, shedId, "摇摆故障", oldData.getSwingFault(), newData.getSwingFault(), now);
// ... 其余同理

"上次状态"从哪来?------上一轮整包数据缓存在 Redis:

java 复制代码
String plcCacheKey = Constants.PLC_KEY + device.getPlcAddress();
PlcData oldPlcData = redisCache.getCacheObject(plcCacheKey);   // 取上次
redisCache.setCacheObject(plcCacheKey, plcData, 10, TimeUnit.SECONDS); // 存本次
detectAndUploadAlarm(device, oldPlcData, plcData);             // 边沿检测

首次采集 oldData == null 时直接 return,不做判定(无历史可比)。

7.2 模拟量阈值:tspThresholdCheck(双阈值迟滞 + DB 去重)

java 复制代码
private void tspThresholdCheck(BusDevice device, double tspValue, Date now) {
    Long deviceId = device.getId();
    Long shedId = device.getShedId();
    // 阈值配置:设备级优先,料棚级兜底
    BusTspThresholdConfig config =
            tspThresholdConfigService.selectEnableConfigByDeviceId(deviceId, shedId);
    if (config == null) return; // 无配置不检测

    double alarmThreshold = config.getAlarmThreshold().doubleValue(); // 报警阈值(高)
    double warnThreshold  = config.getWarnThreshold().doubleValue();  // 恢复阈值(低)

    // DB 去重:以"未恢复记录"为准,重启/多实例不重复插
    boolean isCurrentlyAlarming =
            busDeviceAlarmService.selectUnfinishedAlarm(deviceId, "TSP粉尘浓度超标") != null;

    if (tspValue > alarmThreshold) {
        if (!isCurrentlyAlarming) {           // 触发(越过高阈值 且 当前无未恢复记录)
            BusDeviceAlarm alarm = new BusDeviceAlarm();
            alarm.setShedId(shedId);
            alarm.setDeviceId(deviceId);
            alarm.setAlarmName("TSP粉尘浓度超标");
            alarm.setStartTime(now);
            alarm.setAlarmLevel(AlarmConstants.getAlarmLevel("TSP粉尘浓度超标"));
            alarm.setHandleStatus(AlarmConstants.HANDLE_STATUS_PENDING);
            alarm.setRemark("TSP浓度=" + tspValue + "μg/m³,报警阈值=" + alarmThreshold + "...");
            busDeviceAlarmService.insertBusDeviceAlarm(alarm);
        }
    } else if (tspValue <= warnThreshold) {   // 恢复(回落到低阈值以下)
        if (isCurrentlyAlarming) {
            busDeviceAlarmService.updateAlarmStopTime(deviceId, "TSP粉尘浓度超标", now);
        }
    }
    // warnThreshold < tspValue <= alarmThreshold 落在"死区",维持现状不动作
}

采样降频(tsp.sample.interval 默认 10s)与开关(tsp.sample.enabled)由 sampleAndCheckTsp 控制,超标检查前还会异步写一条 TSP 历史记录。

7.3 阈值配置的分级兜底

selectEnableConfigByDeviceId(deviceId, shedId):优先取设备级 阈值配置,没有则回退到料棚级配置。这让运营能"整棚统一配 + 个别设备特殊配",是"配置驱动 + 就近覆盖"的典型设计。


八、通用示例代码

8.1 通用边沿检测器(位报警)

java 复制代码
public class EdgeAlarmDetector {

    public enum Edge { RISING, FALLING, NONE }

    /** 对比上次/本次布尔状态,返回边沿类型 */
    public static Edge detect(Boolean oldStatus, Boolean newStatus) {
        boolean o = oldStatus != null && oldStatus;
        boolean n = newStatus != null && newStatus;
        if (!o && n) return Edge.RISING;   // 触发
        if (o && !n) return Edge.FALLING;  // 恢复
        return Edge.NONE;
    }
}

// 使用
switch (EdgeAlarmDetector.detect(old, now)) {
    case RISING:  alarmService.open(deviceId, name);  break;
    case FALLING: alarmService.close(deviceId, name); break;
    default: /* 无动作 */
}

8.2 通用双阈值迟滞判定(模拟量)

java 复制代码
public class HysteresisAlarm {

    private final double high;   // 触发阈值
    private final double low;    // 恢复阈值 (low < high)

    public HysteresisAlarm(double high, double low) {
        if (low >= high) throw new IllegalArgumentException("low 必须小于 high");
        this.high = high;
        this.low = low;
    }

    public enum Action { TRIGGER, RECOVER, KEEP }

    /**
     * @param value       当前测量值
     * @param alarmingNow 当前是否处于告警中(建议来自"数据库未闭合记录")
     */
    public Action evaluate(double value, boolean alarmingNow) {
        if (value > high && !alarmingNow)  return Action.TRIGGER;  // 越过高阈值且未在告警
        if (value <= low && alarmingNow)   return Action.RECOVER;  // 回落到低阈值以下且在告警
        return Action.KEEP;                                        // 死区/状态不变
    }
}

// 使用
boolean alarming = alarmService.hasUnfinished(deviceId, "浓度超标");
switch (new HysteresisAlarm(150, 100).evaluate(value, alarming)) {
    case TRIGGER: alarmService.open(deviceId, "浓度超标"); break;
    case RECOVER: alarmService.close(deviceId, "浓度超标"); break;
    default: /* KEEP,不动作 */
}

8.3 告警名称 → 等级映射(配置化)

java 复制代码
public class AlarmLevels {
    public static final int INFO=1, WARN=2, SERIOUS=3, FATAL=4;
    private static final Map<String,Integer> MAP = new HashMap<>();
    static {
        MAP.put("急停", FATAL);
        MAP.put("浓度超标", WARN);
        // ...
    }
    /** 未命中默认从严,避免漏报 */
    public static int of(String name) { return MAP.getOrDefault(name, SERIOUS); }
}

九、设计要点与踩坑清单

主题 要点
区分信号类型 布尔量走边沿检测,模拟量走双阈值迟滞,不可混用。
位报警去重 只在跳变时动作;靠"上次状态"对比(本项目缓存在 Redis)。
首次采集 oldData == null 时不判定,避免把初始状态误当上升沿。
空值归一化 读到 null 统一按"正常/无数据"处理,防 NPE 与误报。
模拟量防抖 必须用双阈值(回差带),单阈值会告警抖动刷屏。
high/low 关系 恢复阈值必须严格小于触发阈值,否则死区不成立。
模拟量去重 以"数据库未闭合记录"为状态源,重启/多实例安全。
采样降频 模拟量可独立于采集频率降频入库(如 10s),减小数据量。
告警生命周期 触发写 start,恢复写 stop(可算时长);stop_time IS NULL 表未恢复。
分级映射 名称→等级配置化,漏配默认从严。
阈值配置分级 设备级优先、料棚级兜底,兼顾统一与个性化。
异常隔离 单个告警判定异常应 try-catch 兜底,不影响整轮采集与其它设备。

小结

告警判定的核心是"按信号物理类型选算法 ":布尔量用边沿检测 (跳变才动作,天然去重),模拟量用双阈值迟滞 (回差带吸收抖动)。两者最终都收敛到统一的"带生命周期的告警记录 + 未闭合记录去重 + 名称分级 "模型。本项目在 GetModbusTCPDataTask 中用 checkAlarmStatus(位报警)与 tspThresholdCheck(模拟量)分别实现了这两类算法,并用 Redis 缓存上次状态、数据库未闭合记录双重去重,构成了一套健壮、可平移的工业告警判定范式。

相关推荐
行者全栈架构师1 小时前
混元 Hy3 Agent 实战:季度报告 3 小时变 40 分钟
算法·架构·代码规范
YDS8291 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
阿拉雷️1 小时前
部署实战】Docker + AI Agent:让AI一键部署Spring Boot到服务器,从打包到上线只要一条指令
人工智能·spring boot·docker
兰令水1 小时前
hot100【acm版】【2026.7.21打卡-java版本】
java·开发语言·算法·leetcode·面试
IT_Octopus1 小时前
一次由 RestTemplate 引发的 Connection reset:被 XML 截胡的请求体
xml·java·spring boot
好好沉淀2 小时前
主键选择(自增 vs UUID vs 雪花算法)
java·算法
退休倒计时2 小时前
【每日一题】LeetCode 131. 分割回文串 TypeScript
算法·leetcode·typescript
小白说大模型2 小时前
AI Agent 调试实战:链路追踪、Prompt 可视化与异常定位的系统方法
java·人工智能·python·算法·prompt
重生之我是Java开发战士2 小时前
【Java EE】Spring Boot配置文件
java·spring boot·java-ee