当前位置: 首页 > news >正文

项目初版设计的报警体系架构与 Java 并发踩过的坑

顺德展厅 — 报警体系架构与 Java 并发知识点

编写日期:2025-05-27


目录

  1. 防重机制(幂等保护)
    • 1.1 HCS 放行防重 — releasing
    • 1.2 WDS 启动防重 — wdsDelivering
    • 1.3 两者对比
  2. 报警体系架构
    • 2.1 整体架构图
    • 2.2 三层设计
    • 2.3 报警触发与消除对照表
    • 2.4 完整生命周期示例
    • 2.5 前端 API 预留
  3. Java 并发知识点
    • 3.1 volatile 关键字
    • 3.2 volatile vs static
    • 3.3 ConcurrentLinkedDeque — 线程安全双端队列
    • 3.4 AtomicInteger — 原子计数器
    • 3.5 ScheduledFuture — 定时任务
    • 3.6 try-finally 保证解锁
  4. 后续扩展方向

1. 防重机制(幂等保护)

1.1 HCS 放行防重 — releasing

问题场景:操作员手抖连续点了两下"启动"按钮,不应该放行两个箱子。

实现方式volatile boolean + try-finally

// ConveyorServiceImpl.javaprivate volatile boolean releasing = false;  // 声明public Result<String> startDemo() {// ① 检查:有人在放行吗?if (releasing) {return Result.error("正在放行中,请稍候");  // 被拦截!}// ② 上锁releasing = true;try {// ③ 执行实际逻辑(检查PLC、写PLC、调WDS等)return doStartDemo();} finally {// ④ 无论成功/失败/异常,都解锁releasing = false;}
}

时间线

请求A → releasing=false? ✅ → releasing=true → 执行放行 → finally releasing=false
请求B → releasing=true?  ✅ → 直接返回错误 ← 被拦截!

关键点

  • finally 块保证 100% 会执行,不会出现"锁死"
  • 方法级别的短暂锁(毫秒级)

1.2 WDS 启动防重 — wdsDelivering

问题场景:调了 WDS startFlow 后,WDS 机器人需要 30~120 秒才能把箱子搬到。这期间不能重复调用。

实现方式volatile boolean + 远端回调解锁 + 超时自动解锁

// ConveyorServiceImpl.javaprivate volatile boolean wdsDelivering = false;  // 声明

四条解锁路径

#场景代码位置说明
1 WDS 调用失败/异常 callWdsStartFlow() wdsDelivering = false 立刻解锁
2 WDS 搬运成功 confirmEntry() entry-confirm 回调到达时解锁
3 WDS 120秒超时 startWdsDeliveryTimeout() 定时器自动解锁 + 触发报警
4 手动重置 resetWdsState() 前端调试面板按钮

时间线(正常情况)

t=0s    点"启动"(无箱)├─ wdsDelivering = true          ← 上锁├─ wdsClient.startFlow()         ← 调WDS└─ startWdsDeliveryTimeout()     ← 启动120秒倒计时t=5s    再次点"启动"└─ wdsDelivering=true → 返回"WDS正在搬运中" ← 被拦截!t=45s   WDS送到了,回调 confirmEntry()├─ cancelWdsDeliveryTimeout()    ← 取消倒计时└─ wdsDelivering = false         ← 解锁!

时间线(WDS故障)

t=0s    点"启动" → wdsDelivering=true → 调WDS → 启动120秒倒计时
t=120s  定时器触发├─ wdsDelivering = false          ← 超时解锁!└─ alarmService.warning(...)      ← 触发报警
t=121s  可以再次点"启动"重试

关键点

  • 不能用 try-finally!因为成功时不能解锁,要等远端回调
  • 超时兜底防止永远锁死

1.3 两者对比

维度HCS放行(releasing)WDS启动(wdsDelivering)
锁的范围 方法执行期间(毫秒级) 远端操作期间(秒~分钟级)
解锁方式 try-finally 自动解锁 远端回调 / 超时 / 手动
超时保护 不需要(执行很快) 120秒超时自动解锁
本质 同步互斥锁 异步状态机

2. 报警体系架构

2.1 整体架构图

触发点                      统一服务                       输出
┌──────────────┐         ┌────────────────┐         ┌─────────────┐
│ KUKA执行失败  │──┐      │                │    ┌──→ │ 日志(分级) │
│ KUKA通信异常  │──┤      │   AlarmService │    │    └─────────────┘
│ 箱子途中卡住  │──┤      │                │    │    ┌─────────────┐
│ WDS搬运超时   │──┼─────→│   fire()       │────┼──→ │  WebSocket  │→ 前端实时弹窗
│ WDS取箱超时   │──┤      │   resolve()    │    │    └─────────────┘
│ 扫码9999     │──┘      │                │    │    ┌─────────────┐
│              │         │                │    └──→ │  内存队列    │→ 查询历史
└──────────────┘         └────────────────┘         └─────────────┘(后续可加 邮件/钉钉)

核心设计思想

  • 触发方只管调 alarmService.warning/critical,不需要关心推送/存储的细节
  • 新增报警类型只需 1行触发 + 1行消除,AlarmService 本身不需要改
  • 后续加邮件只改 fire() 一个方法

2.2 三层设计

第1层:报警级别 — AlarmLevel 枚举

// AlarmLevel.java
public enum AlarmLevel {CRITICAL("critical", "严重"),  // 红色 🚨 需要立即处理WARNING("warning", "警告"),    // 黄色 ⚠️ 需要关注INFO("info", "信息");          // 蓝色 ℹ️ 记录信息
}

第2层:报警事件 — AlarmEvent DTO

// AlarmEvent.java — 每个报警就是一张"工单"
@Data
public class AlarmEvent {private AlarmLevel level;          // 严重程度private String device;             // 哪个设备(kuka/wds/conveyor/scanner)private String alarmCode;          // 唯一编码(用于匹配和消除)private String message;            // 人类可读描述private LocalDateTime timestamp;   // 触发时间private boolean resolved;          // 是否已消除private LocalDateTime resolvedTime;// 消除时间private String resolvedMessage;    // 消除说明
}

第3层:报警服务 — AlarmService

// AlarmService.java — 对外接口
alarmService.critical("kuka", "KUKA_EXEC_FAIL", "KUKA机械臂执行失败");  // 触发
alarmService.warning("wds", "WDS_DELIVERY_TIMEOUT", "WDS搬运超时120秒"); // 触发
alarmService.resolve("KUKA_EXEC_FAIL", "KUKA执行成功,报警自动消除");     // 消除

fire() 内部流水线(触发报警时依次执行):

fire(event)├── 第1步: 日志输出│     CRITICAL → log.error(...)    ← 日志里红色│     WARNING  → log.warn(...)     ← 日志里黄色│     INFO     → log.info(...)     ← 日志里白色│├── 第2步: WebSocket 推送│     拼接 emoji + 设备名 + 消息│     → 前端 WebSocket 收到 action="alarm_event" 可以弹窗│└── 第3步: 内存持久化alarmQueue.addFirst(event)   ← 最新的插到头部队列超200条 → pollLast()     ← 删最老的,防内存泄漏CRITICAL/WARNING → unreadCount+1  ← INFO不算未读

resolve() 内部逻辑(消除报警时):

resolve(alarmCode, resolveMessage)├── 遍历 alarmQueue├── 找到 alarmCode匹配 且 resolved=false 的报警├── 标记 resolved=true + 记录消除时间和原因├── WebSocket 推送 "✅ 报警已消除"└── 只消除最近一条(return)找不到?→ 静默忽略(正常情况,可能从来没报过这个警)

2.3 报警触发与消除对照表

报警编码级别触发时机消除时机(恢复点)
KUKA_EXEC_FAIL CRITICAL KUKA 执行失败 下次 KUKA 执行成功
KUKA_COMM_ERROR CRITICAL KUKA 通信异常 下次 KUKA 执行成功
BOX_STUCK_R1_TO_R102 WARNING R1放行后90秒未到R102 箱子到达 R[102]
BOX_STUCK_R102_TO_R210 WARNING R102放行后90秒未到R210 箱子到达 R[210]
WDS_DELIVERY_TIMEOUT WARNING WDS 搬运120秒超时 entry-confirm 到达
WDS_ENDCONFIRM_TIMEOUT WARNING WDS 取箱60秒超时 end-confirm 到达
BARCODE_9999_R102 WARNING R102 PLC箱号=9999 — (一次性告警)
BARCODE_9999_R210 WARNING/CRITICAL R210 PLC箱号=9999 — (一次性告警)

2.4 完整生命周期示例

"箱子在 R1→R102 途中卡住" 为例:

阶段1:R[1] 放行,启动超时定时器
─────────────────────────────────ConveyorServiceImpl.doStartDemo()→ releaseEntry()                   // PLC写入,放行Roller[1]→ startTransitTimeoutR1ToR102()    // 注册90秒后执行的定时任务// 此时还没报警!只是定了个闹钟阶段2a(正常):箱子在90秒内到达R[102]
───────────────────────────────────────PlcController.doPickStatusChange()   // PLC感应到Roller[102]有箱子→ cancelTransitTimeoutR1ToR102()   // 取消闹钟(90秒的lambda永远不会执行)→ resolveAlarm("BOX_STUCK_R1_TO_R102", "箱子已到达 Roller[102]")→ resolve() 找不到匹配报警     // 因为从来没报过→ 静默忽略 ✅                   // 一切正常阶段2b(异常):箱子卡住了,90秒闹钟响了
──────────────────────────────────────────定时器触发 lambda:→ alarmService.warning("conveyor", "BOX_STUCK_R1_TO_R102", "...")→ fire(event)→ log.warn(...)                    // 日志记录→ WebSocket推送 "⚠️ 箱子卡住..."   // 前端弹窗→ alarmQueue.addFirst(event)       // 存入队列→ unreadCount+1                    // 未读+1阶段3:操作员去现场处理,箱子终于到了R[102]
─────────────────────────────────────────────PlcController.doPickStatusChange()→ resolveAlarm("BOX_STUCK_R1_TO_R102", "箱子已到达 Roller[102]")→ resolve() 找到匹配报警!→ event.setResolved(true)           // 标记已消除→ event.setResolvedTime(now())      // 记录消除时间→ WebSocket推送 "✅ 报警已消除"      // 前端关闭弹窗

2.5 前端 API 预留

AlarmService 方法前端功能说明
getUnreadCount() 🔴 铃铛红色角标 显示未读报警数量
clearUnread() 点击"已读" 角标清零
getRecentAlarms(20) 报警历史列表 展示最近20条报警记录
hasActiveAlarm(code) 设备状态指示 某报警还在活跃 → 红色闪烁

前端报警面板设想

┌──────────────────────────────────────────┐
│  🔔 报警通知 (3)              [全部已读] │  ← getUnreadCount() + clearUnread()
├──────────────────────────────────────────┤
│  🚨 10:01 KUKA机械臂执行失败            │
│     ✅ 10:03 已消除: 重试后执行成功       │  ← resolved=true 的显示灰色
│                                          │
│  ⚠️ 09:55 箱子R1→R102卡住超过90秒       │
│     ❌ 未消除                            │  ← resolved=false 的显示红色
│                                          │
│  ⚠️ 09:30 WDS搬运超时120秒              │
│     ✅ 09:32 已消除: WDS搬运完成          │
└──────────────────────────────────────────┘

3. Java 并发知识点

3.1 volatile 关键字

解决的问题:多线程之间的"可见性"。

没有 volatile 时

CPU缓存机制:每个线程有自己的工作内存(CPU缓存),不一定及时同步到主内存线程A的工作内存:  releasing = true    ← A改了
主内存:           releasing = false   ← 还没刷新过去!
线程B的工作内存:  releasing = false   ← B还在用旧值

有 volatile 时

线程A 修改 releasing = true→ 立刻写回主内存→ 线程B下次读取时,强制从主内存获取最新值→ 线程B 看到 releasing = true ✅

一句话volatile = "所有线程看到的值都是最新的"


3.2 volatile vs static

 staticvolatile
解决什么问题 归属:变量属于类还是属于对象 可见性:线程A改了,线程B能不能立刻看到
例子 全班共用一个黑板 黑板上写的字,后排同学能不能看清
多线程安全? ❌ 不保证 ✅ 保证可见性
能一起用? static volatile boolean flag

为什么我们用 volatile 不用 static

ConveyorServiceImpl 是 Spring 单例(全局只有1个实例),所以 static 和非 static 效果一样。 但 volatile 是必须的,因为前端两次快速点击会产生两个 HTTP 线程同时访问 releasing


3.3 ConcurrentLinkedDeque — 线程安全双端队列

为什么不用普通 LinkedList

// 报警可能同时从多个线程触发:
线程1(PLC轮询线程):  alarmQueue.addFirst(箱子卡住报警)
线程2(KUKA工作线程):  alarmQueue.addFirst(KUKA故障报警)
线程3(定时器线程):    alarmQueue.addFirst(WDS超时报警)// LinkedList 内部是链表指针操作:
// node.prev = head; head.next = node; head = node;
// 三个线程同时改 head 指针 → 互相覆盖 → 数据丢失或 NPE 崩溃!// ConcurrentLinkedDeque 内部用 CAS(Compare-And-Swap)无锁算法:
// "我要把 head 从 A 改成 B,但只有 head 确实还是 A 时才改,否则重试"
// → 多线程安全,不会冲突

为什么要"双端"队列?

// 我们的需求:
alarmQueue.addFirst(event);    // 新报警插到头部(最新的在前面)
alarmQueue.pollLast();         // 超过200条时,从尾部删最老的// 普通 Queue 只能:add() 到尾部,poll() 从头部 → 方向固定,不够灵活

方法名对照表

普通 QueueConcurrentLinkedDeque说明
add(e) addFirst(e) 插到头部
add(e) addLast(e) 插到尾部
poll() pollFirst() 取头部(并移除)
pollLast() 取尾部(并移除)
peek() peekFirst() 看头部(不移除)
peekLast() 看尾部(不移除)
size() size() 队列大小
for(item : deque) 可以直接遍历(从头到尾)

3.4 AtomicInteger — 原子计数器

问题:普通 intcount++ 不是原子操作。

// count++ 实际上是三步:
// 1. 读取 count 的值(假设是 5)
// 2. 计算 count + 1 = 6
// 3. 写回 count = 6// 两个线程同时 count++:
线程A: 读 count=5 → 算 6 → 写 count=6
线程B: 读 count=5 → 算 6 → 写 count=6   ← 也读到了5!
// 结果:count=6,但预期是 7!

AtomicInteger 解决方案

private final AtomicInteger unreadCount = new AtomicInteger(0);unreadCount.incrementAndGet();  // 原子 +1(等价于 count++,但线程安全)
unreadCount.get();              // 读取当前值
unreadCount.set(0);             // 设置为0

内部也是用 CAS 实现:"+1 之前先检查值没被别人改过"。


3.5 ScheduledFuture — 定时任务

用途:注册一个"N秒后执行"的任务,并且可以取消。

// 注册:90秒后执行
ScheduledFuture<?> task = scheduler.schedule(() -> {// 这个 lambda 在90秒后执行alarmService.warning("conveyor", "BOX_STUCK", "箱子卡住了");
}, 90, TimeUnit.SECONDS);// 取消:箱子提前到了,不需要报警了
task.cancel(false);  // false = 不中断正在执行的任务

在我们项目中的应用

定时任务超时时间触发动作取消条件
transitR1ToR102Task 90秒 报警:箱子R1→R102卡住 箱子到达R[102]
transitR102ToR210Task 90秒 报警:箱子R102→R210卡住 箱子到达R[210]
wdsDeliveryTimeoutTask 120秒 报警:WDS搬运超时 + 解锁 entry-confirm到达
endConfirmTimeoutTask 60秒 报警:WDS取箱超时 end-confirm到达

3.6 try-finally 保证解锁

问题:如果上锁后代码执行中途抛异常了,锁怎么办?

// 错误写法 ❌
releasing = true;
doSomething();      // 如果这里抛异常...
releasing = false;  // 这行永远不会执行!→ 永远锁死!// 正确写法 ✅
releasing = true;
try {doSomething();      // 不管成功还是异常...
} finally {releasing = false;  // finally 块 100% 会执行!
}

finally 的执行保证

doSomething() 结果finally 执行?说明
正常返回 ✅ 执行
抛异常 ✅ 执行 异常继续向上传播
return 提前返回 ✅ 执行 return 的值在 finally 之后才真正返回
唯一例外 System.exit() 或 JVM 崩溃

4. 后续扩展方向

4.1 邮件通知(已确认可行)

HCMS 已有 EmailSendMsgUtils,只需在 AlarmService.fire() 中加一步:

// 在 fire() 方法末尾加:
if (event.getLevel() == AlarmLevel.CRITICAL) {EmailSendMsgUtils emailUtils = SpringContextUtils.getBean(EmailSendMsgUtils.class);emailUtils.sendMsg(receiverEmail, "【顺德展厅报警】" + event.getAlarmCode(), event.getMessage());
}

前置条件:在 HcmsSysConfig 表中配置好 SMTP 信息。

4.2 前端报警面板

需要实现的前端功能:

  • WebSocket 监听 action=alarm_eventaction=alarm_resolved
  • 铃铛图标 + 未读红色角标
  • 报警弹窗/抽屉面板
  • 历史列表(已消除显示灰色,未消除显示红色)

4.3 报警持久化

当前使用内存队列(重启丢失),后续如果需要:

  • 可以写入数据库(报警日志表)
  • 可以对接 ELK / Prometheus 等监控平台

 

http://www.gsyq.cn/news/1401909.html

相关文章:

  • 深入解析ORA-28040:新旧客户端与Oracle 12c/19c认证协议不匹配的根源与对策
  • 2026年推荐一下全伺服驱动杯成型机供应商 - 品牌推广大师
  • 避坑指南:STM32CubeMx配置输入捕获时,中断回调与溢出处理的那些细节
  • 从矩阵分解到网络嵌入:ISRM_NE如何构建可解释的推荐系统
  • 从热点定位到瓶颈根因:Intel VTune Profiler实战性能调优指南
  • 5分钟掌握B站视频高效下载:BiliDownloader全面使用指南
  • 用一块老芯片搞定模24计数器:手把手教你用74390与非门搭个实用小电路
  • 高效移除Windows Defender:专业级系统安全组件管理工具指南
  • Typora插件架构深度解析:如何通过62个插件实现Markdown编辑器的全面功能扩展
  • 地下物联网监测难题破解:同步LoRa Mesh网络的设计与实战
  • 从Cron任务静默失败到多层监控架构:构建可靠的系统与自我认知
  • 英雄联盟智能助手Seraphine:你的终极游戏伙伴,免费提升游戏段位
  • 从MDK5.29到5.37:版本演进、Pack生态与国内镜像获取全攻略
  • 10分钟构建专业网络拓扑图:easy-topo零基础实战指南
  • 智能车竞赛技术报告 | 从零到一:OpenART视觉模块与RT1064的嵌入式AI实践
  • 如何快速打造个性化系统监控中心:终极扩展框架指南
  • 终极指南:免费解锁《极限竞速》全部潜力 - Forza Mods AIO完全掌握教程
  • 数说AI|北航人工智能研究院:顶配资源下的科研新势力
  • 深入解析NCP1271:从工作模式到关键外围电路设计
  • 如何用AI在5分钟内将普通视频变成立体3D大片?Deep3D完整指南
  • OpenCV跨语言传图实战:C++与C#间如何用unsigned char*安全传递cv::Mat图像数据
  • 智慧医院三维透明建筑数字化升级
  • 当AI代理遭遇视觉障碍:基于像素颜色识别的自动化按钮点击方案
  • 2026崇左市本地人必选的水质检测专业机构TOP7推荐!生活饮用水检测、直饮水检测、污水废水检测、矿泉水检测,正规CMA资质检测公司排名推荐 (2026年5月水质检测最新深度调研方案) - 一修哥咨询
  • 飞书智慧中台保姆级搭建指南:零代码+AI,让企业拥有“数字大脑”
  • 2026蚌埠市本地人必选的水质检测专业机构TOP7推荐!生活饮用水检测、直饮水检测、污水废水检测、矿泉水检测,正规CMA资质检测公司排名推荐 (2026年5月水质检测最新深度调研方案) - 一修哥咨询
  • MacBook蓝牙外设连接顽疾:从信号干扰到进程冲突的深度排查与优化指南
  • C# 单元测试进阶:MSTest框架实战技巧与最佳实践
  • 从信号到频谱:np.fft.fft实战避坑与结果解读
  • ROS2 Foxy下,六轴IMU串口数据解析与Rviz2实时姿态可视化全流程(避坑串口权限与插件安装)