公司动态
SpringBoot中实现告警判定算法-位报警与模拟量阈值-技术详解
SpringBoot中实现告警判定算法-位报警与模拟量阈值-技术详解适用场景IoT / 工业监控 / 设备告警 / 任何周期采样 → 判定是否告警 → 落库 → 恢复的系统。目录为什么告警判定要分两类第一类位报警布尔量—— 边沿检测第二类模拟量阈值 —— 双阈值迟滞回差两类算法对比告警生命周期与去重两类共用告警分级映射本项目落地实现解析通用示例代码设计要点与踩坑清单注博客https://blog.csdn.net/badao_liumang_qizhi一、为什么告警判定要分两类设备采集回来的信号本质上分两种物理类型判定方式完全不同信号类型例子取值判定方式开关量 / 布尔量位急停、水泵故障、风机故障、缺水故障true/false1 个 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 数据缓存在 Redis10s 过期下一轮取出来做对比既做了边沿检测的上次状态源又避免了额外维护状态表。2.3 空值处理传感器/通信可能读到 null。判定前要归一化本项目把 null 视为正常 false避免 NPE 且语义安全oldStatusoldStatusnull?false:oldStatus;newStatusnewStatusnull?false:newStatus;三、第二类模拟量阈值 —— 双阈值迟滞回差3.1 单阈值的抖动问题若只用一个阈值T值 T告警、值 T恢复。当实际值在T附近小幅波动如 T150值在 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 类布尔故障逐一做边沿检测privatevoidcheckAlarmStatus(BusDevicedevice,LongshedId,StringalarmName,BooleanoldStatus,BooleannewStatus,Datenow){LongdeviceIddevice.getId();oldStatusoldStatusnull?false:oldStatus;// 空值归一化newStatusnewStatusnull?false:newStatus;// 上升沿正常→报警生成新告警if(!oldStatusnewStatus){BusDeviceAlarmalarmnewBusDeviceAlarm();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()PLCdevice.getPlcAddress());busDeviceAlarmService.insertBusDeviceAlarm(alarm);}// 下降沿报警→正常恢复告警if(oldStatus!newStatus){busDeviceAlarmService.updateAlarmStopTime(deviceId,alarmName,now);}}调度侧对多类故障批量判定急停、风机/水泵故障、摇摆/仰俯/超限位/编码器/缺水故障、总故障等checkAlarmStatus(device,shedId,急停,oldData.getEStop(),newData.getEStop(),now);checkAlarmStatus(device,shedId,摇摆故障,oldData.getSwingFault(),newData.getSwingFault(),now);// ... 其余同理上次状态从哪来——上一轮整包数据缓存在 RedisStringplcCacheKeyConstants.PLC_KEYdevice.getPlcAddress();PlcDataoldPlcDataredisCache.getCacheObject(plcCacheKey);// 取上次redisCache.setCacheObject(plcCacheKey,plcData,10,TimeUnit.SECONDS);// 存本次detectAndUploadAlarm(device,oldPlcData,plcData);// 边沿检测首次采集oldData null时直接 return不做判定无历史可比。7.2 模拟量阈值tspThresholdCheck双阈值迟滞 DB 去重privatevoidtspThresholdCheck(BusDevicedevice,doubletspValue,Datenow){LongdeviceIddevice.getId();LongshedIddevice.getShedId();// 阈值配置设备级优先料棚级兜底BusTspThresholdConfigconfigtspThresholdConfigService.selectEnableConfigByDeviceId(deviceId,shedId);if(confignull)return;// 无配置不检测doublealarmThresholdconfig.getAlarmThreshold().doubleValue();// 报警阈值(高)doublewarnThresholdconfig.getWarnThreshold().doubleValue();// 恢复阈值(低)// DB 去重以未恢复记录为准重启/多实例不重复插booleanisCurrentlyAlarmingbusDeviceAlarmService.selectUnfinishedAlarm(deviceId,TSP粉尘浓度超标)!null;if(tspValuealarmThreshold){if(!isCurrentlyAlarming){// 触发越过高阈值 且 当前无未恢复记录BusDeviceAlarmalarmnewBusDeviceAlarm();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);}}elseif(tspValuewarnThreshold){// 恢复回落到低阈值以下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 通用边沿检测器位报警publicclassEdgeAlarmDetector{publicenumEdge{RISING,FALLING,NONE}/** 对比上次/本次布尔状态返回边沿类型 */publicstaticEdgedetect(BooleanoldStatus,BooleannewStatus){booleanooldStatus!nulloldStatus;booleannnewStatus!nullnewStatus;if(!on)returnEdge.RISING;// 触发if(o!n)returnEdge.FALLING;// 恢复returnEdge.NONE;}}// 使用switch(EdgeAlarmDetector.detect(old,now)){caseRISING:alarmService.open(deviceId,name);break;caseFALLING:alarmService.close(deviceId,name);break;default:/* 无动作 */}8.2 通用双阈值迟滞判定模拟量publicclassHysteresisAlarm{privatefinaldoublehigh;// 触发阈值privatefinaldoublelow;// 恢复阈值 (low high)publicHysteresisAlarm(doublehigh,doublelow){if(lowhigh)thrownewIllegalArgumentException(low 必须小于 high);this.highhigh;this.lowlow;}publicenumAction{TRIGGER,RECOVER,KEEP}/** * param value 当前测量值 * param alarmingNow 当前是否处于告警中建议来自数据库未闭合记录 */publicActionevaluate(doublevalue,booleanalarmingNow){if(valuehigh!alarmingNow)returnAction.TRIGGER;// 越过高阈值且未在告警if(valuelowalarmingNow)returnAction.RECOVER;// 回落到低阈值以下且在告警returnAction.KEEP;// 死区/状态不变}}// 使用booleanalarmingalarmService.hasUnfinished(deviceId,浓度超标);switch(newHysteresisAlarm(150,100).evaluate(value,alarming)){caseTRIGGER:alarmService.open(deviceId,浓度超标);break;caseRECOVER:alarmService.close(deviceId,浓度超标);break;default:/* KEEP不动作 */}8.3 告警名称 → 等级映射配置化publicclassAlarmLevels{publicstaticfinalintINFO1,WARN2,SERIOUS3,FATAL4;privatestaticfinalMapString,IntegerMAPnewHashMap();static{MAP.put(急停,FATAL);MAP.put(浓度超标,WARN);// ...}/** 未命中默认从严避免漏报 */publicstaticintof(Stringname){returnMAP.getOrDefault(name,SERIOUS);}}九、设计要点与踩坑清单主题要点区分信号类型布尔量走边沿检测模拟量走双阈值迟滞不可混用。位报警去重只在跳变时动作靠上次状态对比本项目缓存在 Redis。首次采集oldData null时不判定避免把初始状态误当上升沿。空值归一化读到 null 统一按正常/无数据处理防 NPE 与误报。模拟量防抖必须用双阈值回差带单阈值会告警抖动刷屏。high/low 关系恢复阈值必须严格小于触发阈值否则死区不成立。模拟量去重以数据库未闭合记录为状态源重启/多实例安全。采样降频模拟量可独立于采集频率降频入库如 10s减小数据量。告警生命周期触发写 start恢复写 stop可算时长stop_time IS NULL表未恢复。分级映射名称→等级配置化漏配默认从严。阈值配置分级设备级优先、料棚级兜底兼顾统一与个性化。异常隔离单个告警判定异常应 try-catch 兜底不影响整轮采集与其它设备。小结告警判定的核心是按信号物理类型选算法布尔量用边沿检测跳变才动作天然去重模拟量用双阈值迟滞回差带吸收抖动。两者最终都收敛到统一的带生命周期的告警记录 未闭合记录去重 名称分级模型。本项目在GetModbusTCPDataTask中用checkAlarmStatus位报警与tspThresholdCheck模拟量分别实现了这两类算法并用 Redis 缓存上次状态、数据库未闭合记录双重去重构成了一套健壮、可平移的工业告警判定范式。