Java + Redis 实现工业设备告警去抖,避免变频器故障重复推送

工业设备接入平台后,一个常见问题很快就会暴露出来:同一台变频器在电压波动或通信不稳定时,可能在几秒内连续上报多条相同故障。如果服务端收到一条就通知一次,值班人员很快会被重复消息淹没。

本文以创安睿控工业驱控设备的接入场景为例,拆解一套可直接落地的告警去抖方案。重点不在于"少发消息",而在于保留故障事实的同时,把同一事件的重复抖动合并掉。

先定义什么算重复告警

设备上报的原始消息可以简化成下面这样:

json 复制代码
{
  "deviceId": "INV-1024",
  "faultCode": "OV",
  "level": 2,
  "occurredAt": 1785990123000,
  "status": "ACTIVE"
}

仅用 deviceId + faultCode 去重还不够。同一个过压故障在上午解除,下午再次发生,这是两个独立事件,不能被长期压掉。比较实用的规则是:

  1. 同一设备、同一故障码、同一状态组成去重键。
  2. 在短时间窗口内重复到达,只更新计数,不再次通知。
  3. 状态从 ACTIVE 变为 RECOVERED 时立即放行。
  4. 窗口结束后再次出现,视为新事件。

这个规则能区分"网络重传"和"故障重新发生"。窗口长度要根据采样频率与现场容忍度设置,不宜写死在代码里。

Redis 去抖的原子实现

单实例服务可以用本地缓存,但设备平台通常会横向扩容。本地缓存无法在多个实例之间共享,重启后状态也会丢失。Redis 更适合保存短周期去抖状态。

最简单的实现是 SET key value NX EX

java 复制代码
public boolean shouldNotify(AlarmEvent event) {
    String key = String.format("alarm:dedup:%s:%s:%s",
            event.deviceId(), event.faultCode(), event.status());

    Boolean first = stringRedisTemplate.opsForValue()
            .setIfAbsent(key, "1", Duration.ofSeconds(30));

    return Boolean.TRUE.equals(first);
}

第一次写入成功,说明当前窗口内还没有同类事件,可以继续发送通知。后续重复消息写入失败,只记录到数据库或指标系统。

如果还需要统计窗口内重复次数,可以使用 Lua 脚本,让"首次写入、设置过期时间、计数递增"在 Redis 内一次完成:

lua 复制代码
local count = redis.call('INCR', KEYS[1])
if count == 1 then
  redis.call('EXPIRE', KEYS[1], ARGV[1])
  return 1
end
return 0

Java 侧只根据返回值决定是否通知:

java 复制代码
private static final DefaultRedisScript<Long> DEDUP_SCRIPT =
        new DefaultRedisScript<>(LUA_TEXT, Long.class);

public boolean shouldNotify(AlarmEvent event) {
    String key = buildDedupKey(event);
    Long result = stringRedisTemplate.execute(
            DEDUP_SCRIPT, List.of(key), "30");
    return Long.valueOf(1L).equals(result);
}

Lua 的价值在于原子性。如果先 INCR 再单独 EXPIRE,服务恰好在两条命令之间中断,键可能永不过期,后续真实告警也会被误判。

恢复事件不能简单去重

故障恢复代表设备状态已经变化,应该让运维人员及时看到。可以在收到 RECOVERED 时删除对应的活动告警键,并为恢复消息建立独立的短窗口:

java 复制代码
if (event.status() == AlarmStatus.RECOVERED) {
    redisTemplate.delete(activeKey(event.deviceId(), event.faultCode()));
    return shouldNotifyRecovery(event);
}

更稳妥的做法是把通知状态建模为小型状态机:NORMAL -> ACTIVE -> RECOVERED -> NORMAL。Redis 负责快速判断,数据库保存完整事件轨迹。这样即使通知渠道暂时失败,平台仍能补发,而不是把"是否通知"误当成"故障是否存在"。

上线前要补的三个边界

第一,设备时间不能直接作为唯一判断依据。现场网关可能离线缓存,恢复网络后批量补传。服务端应同时保留设备发生时间和平台接收时间。

第二,去抖键需要版本。故障码映射规则调整后,旧键可能干扰新逻辑,可以在键前缀中加入规则版本,例如 alarm:v2:dedup

第三,去抖不等于丢弃。被压制的重复消息仍应累计 repeatCount,并在告警详情里显示。值班人员不需要收到二十次推送,但需要知道故障在三十秒内抖动了二十次。

在创安睿控这类变频器与工业驱控设备接入场景中,告警质量往往比告警数量更重要。先把事件边界、恢复逻辑和补传场景定义清楚,再选择 Redis 实现,系统才不会在"降噪"的同时漏掉真正的设备异常。

相关推荐
mayaairi11 小时前
Vue2 事件绑定完全指南:从入门到原理
前端·javascript·vue.js
鲨鱼辣钊13 小时前
【FastAPI筑基-Day19】APScheduler定时任务全实战|自动执行、动态启停、后台常驻
java·spring·fastapi
ℋᙚᵐⁱᒻᵉ鲸落17 小时前
移动端滑动手势冲突:overflow: auto导致外层横向滑动失效
前端·javascript·css·vue.js·html
学长毕业设计17 小时前
基于SpringBoot的公益基金管理系统(源码+文档+讲解视频)
java·spring boot·后端
东小西17 小时前
【SAA实战】第 3 篇 · 工具调用全攻略:把业务能力交给 Agent 自己调度
java·后端·spring
东小西17 小时前
【SAA实战】第 4 篇 · Agent 短期记忆:saver 让 Agent 跨轮记得住(threadId 隔离)
java·后端·spring
许彰午17 小时前
22-DataCenter报文序列化
java·低代码·架构·状态模式
2601_9620652518 小时前
[MySQL] SQL优化之性能分析
java·sql·mysql
默_笙18 小时前
🎃 前端学了 Next.js,后端该学啥?NestJS 就是 Node 版的蜜雪冰城
前端·javascript
小范同学_18 小时前
JDK1.7 与 JDK1.8 HashMap 底层原理对比 + 数组并发扩容死循环详解
java·开发语言