工程笔记
IoT 重复告警与 Redis 分布式锁实践
·
1,168 words
·
4 min read
·
Updated 2026-08-10
在铁路隐蔽工程智能监测系统中,设备传感器数据上报频率较高时,会出现同一设备的同类型告警被重复创建的问题。这导致告警记录冗余,影响系统的可靠性和用户体验。
问题现象
铁路隐蔽工程监测系统上线后,告警表出现了大量重复记录。查日志发现,同一设备、同一类型的告警,在 1 毫秒内被两个线程各创建了一次:
1 2
| 19:09:35.723 [pool-2-thread-25] 创建告警 - 设备: WT6B00000147, 类型: 振动超限 19:09:35.724 [pool-2-thread-30] 创建告警 - 设备: WT6B00000147, 类型: 振动超限
|
传感器上报频率高,数据通过线程池并行处理,同一个设备的数据被两个线程同时拿到了。
根因
问题出在原来的告警创建逻辑:
1 2 3
| if (!isActiveAlarmExist(deviceSn, alarmType)) { createAlarmRecord(deviceSn, alarmType, alarmValue, thresholdValue); }
|
isActiveAlarmExist() 查 Redis,createAlarmRecord() 写数据库并设置 Redis key。两个操作不是原子的。
线程 A 查到”没有告警”,还没写进去,线程 B 也查到”没有告警”,于是两条都写进去了。这就是竞态条件。
第一版方案:Redis 加锁
用 Redis 的 setIfAbsent 把”检查”和”占位”合成一步:
1 2 3 4 5 6 7
| Boolean isNew = redisTemplate.opsForValue() .setIfAbsent("alarm:active:" + deviceSn + ":" + alarmType, "1", 24, TimeUnit.HOURS);
if (Boolean.TRUE.equals(isNew)) { createAlarmRecord(deviceSn, alarmType, alarmValue, thresholdValue); }
|
Redis 单线程执行 setIfAbsent,保证只有一个线程能拿到锁。这步做完,重复告警基本解决了。
但还有个坑
Redis 锁设置成功了,数据库写入却失败了怎么办?比如数据库挂了、事务回滚了、网络超时了。
这时候 Redis 里留着一个 "alarm:active:xxx" 的 key,24 小时内不会过期。后续真实的告警来了,查到 Redis 里有记录,就直接跳过了——告警被静默丢弃了。
这比重复告警更严重。重复顶多是数据脏一点,丢告警是功能缺陷。
终版方案:加锁 + 重试 + 回滚
思路很简单:数据库写入失败时,把 Redis 里的锁删掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private boolean tryCreateAlarm(String deviceSn, String alarmType, Long value, Long threshold) { String key = "alarm:active:" + deviceSn + ":" + alarmType;
try { Boolean acquired = redisTemplate.opsForValue() .setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (!Boolean.TRUE.equals(acquired)) { log.info("告警已存在,跳过 - deviceSn={}, type={}", deviceSn, alarmType); return false; }
boolean success = insertWithRetry(deviceSn, alarmType, value, threshold);
if (success) { return true; } else { redisTemplate.delete(key); log.warn("数据库写入失败,已回滚 Redis 锁 - key={}", key); return false; }
} catch (Exception e) { log.warn("Redis 异常,降级直接入库: {}", e.getMessage()); createAlarmRecord(deviceSn, alarmType, value, threshold); return true; } }
|
数据库写入加了重试,最多 3 次,延迟递增:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private boolean insertWithRetry(String deviceSn, String alarmType, Long value, Long threshold) { for (int i = 1; i <= 3; i++) { try { IotAlarm alarm = new IotAlarm(); alarm.setDeviceSn(deviceSn); alarm.setAlarmType(alarmType); alarm.setAlarmValue(value); alarm.setThresholdValue(threshold); alarm.setAlarmTime(new Date()); alarm.setStatus(0L);
iotAlarmService.insertIotAlarm(alarm);
if (alarm.getId() != null) { return true; } } catch (Exception e) { log.warn("第{}次写入失败: {}", i, e.getMessage()); if (i < 3) { try { Thread.sleep(100L * i); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } } return false; }
|
几个实际考虑
为什么不用数据库唯一索引?
可以加,但不太够用。IoT 场景写入量大,唯一索引冲突会触发数据库回滚,开销不小。而且告警有状态流转(活跃→已恢复),唯一索引不好表达”同一时刻只能有一个活跃告警”这个业务语义。Redis 锁把并发控制和数据存储分开,各干各的,更轻量。
重试 3 次够吗?
够用了。1 次基本拦不住网络抖动,无限重试会拖垮线程池。3 次 + 递增延迟,既能覆盖大多数瞬时故障,又不会阻塞太久。
Redis 挂了怎么办?
代码里 catch 了 Redis 异常,直接降级写库。这时候可能又出现重复告警,但保证不丢数据。监控类系统,丢数据的后果比重复严重得多。
效果
上线后重复告警归零。Redis 和数据库的一致性通过回滚机制兜底,偶发的数据库瞬时故障也能自愈。
这套东西不复杂,核心就三点:
setIfAbsent 原子加锁,解决并发重复
- 数据库失败时删 Redis key,避免假状态
- Redis 异常时降级写库,保证可用性
适合 IoT、监控、上报这类高并发、写多读少的场景。