3D打印机管理后台与RabbitMQ集成的业务场景

3D打印机管理后台与RabbitMQ集成的业务场景

文章目录

问题

系统管理着成千上万台分布在全球各地的工业级3D打印机。这些打印机在长时间打印过程中可能会发生各种异常,例如:

  • 喷头堵塞 (Extruder Jam)
  • 热床温度异常 (Heated Bed Temperature Error)
  • 材料耗尽 (Filament Runout)
  • 设备离线 (Device Offline)

遇到的问题:客户投诉**「告警慢」**

系统有严重延迟!有时候打印机已经停摆报警了10分钟,你们的平台后台才弹出一条通知消息。这导致了我们的产线停工,损失巨大!

分析

根本原因分析(为什么不用数据库,而用RabbitMQ?)

如果系统设计是打印机 -> HTTP API -> 直接写数据库,那么在高并发场景下(50台设备同时上报),会遇到以下瓶颈:

  1. 数据库写入压力 :50台设备几乎同时告警,意味着50个INSERT请求瞬间到达数据库,极易造成数据库连接池爆满、写入缓慢。
  2. 同步处理阻塞 :在将告警信息写入数据库后,系统还需要进行一系列后续操作(如下文所述),这些操作如果是同步的,会严重拖慢整个API的响应时间,让设备等待,从而形成瓶颈。

解决方案:引入RabbitMQ进行异步解耦

架构如下图所示:
数据与通知 云印后端系统 打印机设备端 异步消费者集群 HTTP API HTTP API HTTP API 将报警事件发布至 路由至 路由至 路由至 报警记录数据库 运维人员手机/邮箱 浏览器实时监控大屏 API服务器
接收报警 生成事件 RabbitMQ
Exchange: alarm_events 消费者C1
持久化至数据库 Queue: alarm_db_writer 消费者C2
发送邮件/SMS Queue: alarm_notifier 消费者C3
WebSocket推送至控制台 Queue: alarm_dashboard 报警接收API 打印机A
喷头堵塞 打印机B
材料耗尽 打印机C
设备离线

如上图所示,整个高效、解耦的处理流程是:

1.接收告警 (API) :设备通过HTTP API上报告警。API服务只做两件事:验证请求、将告警信息作为一个消息体(JSON格式)立即发布(Producer) 到RabbitMQ的一个名为 alarm_events的Exchange中。API随即返回202 Accepted给设备,告知其"告警已收到",处理非常迅速。

2.消息路由 (RabbitMQ) :RabbitMQ的 alarm_eventsExchange会根据设定的路由键(routing key),将消息同时分发到三个不同的Queue中:

  • alarm_db_queue(用于数据持久化)
  • alarm_notify_queue(用于发送通知)
  • alarm_dashboard_queue(用于前台大屏实时展示)

3.异步处理 (Consumers) :后台运行着多个消费者(Consumer) 服务,各自独立地从上述Queue中获取消息并处理:

消费者C1 (DB Writer) :从 alarm_db_queue取消息,异步写入数据库。即使数据库偶尔缓慢,也不会影响其他操作。

消费者C2 (Notifier) :从 alarm_notify_queue取消息,调用短信或邮件服务发送告警通知。这是通知延迟的根源,但确保了通知最终必达。

消费者C3 (Dashboard Pusher) :从 alarm_dashboard_queue取消息,通过WebSocket实时推送到运维控制台的大屏页面。这是速度最快的一环。

在这个真实场景中,RabbitMQ的作用是:

  • 异步解耦:将耗时的操作(写库、通知)与核心API分离,快速响应设备。
  • 削峰填谷:瞬间的50条告警被队列缓存起来,由消费者逐步消化,避免系统被冲垮。
  • 提高可靠性:即使某个消费者服务(如发短信的)暂时崩溃,告警消息也会在队列中存活,待服务恢复后继续处理,确保消息不丢失。
  • 弹性扩展:如果告警数量持续巨大,您可以简单地启动更多个消费者实例来并行处理队列中的消息。

实战演示

核心思路:

  1. 模拟打印机设备:创建一个简单的HTTP客户端,模拟向后端API发送告警。
  2. 告警接收API:创建一个Spring Boot控制器,接收告警,快速验证并将告警事件发布到RabbitMQ。
  3. RabbitMQ配置 :定义交换机(alarm_events)、队列(alarm_db_writer, alarm_notifier, alarm_dashboard)以及它们之间的绑定关系。
  4. 异步消费者
    • 模拟数据库写入消费者:监听alarm_db_writer队列,模拟耗时的数据库操作。
    • 模拟通知发送消费者:监听alarm_notifier队列,模拟发送邮件/SMS。
    • 模拟大屏推送消费者:监听alarm_dashboard队列,模拟通过WebSocket推送(这里简化为打印日志)。

项目结构:

java 复制代码
printer-alarm-system/
├── src/main/java/com/example/printer/
│   ├── PrinterAlarmSystemApplication.java (主启动类)
│   ├── config/
│   │   └── RabbitMQConfig.java (RabbitMQ配置)
│   ├── controller/
│   │   └── AlarmController.java (告警接收API)
│   ├── dto/
│   │   └── PrinterAlarmEvent.java (告警事件数据传输对象)
│   ├── producer/
│   │   └── AlarmEventPublisher.java (告警事件发布者)
│   └── consumer/
│       ├── DbWriterConsumer.java (数据库写入消费者)
│       ├── NotifierConsumer.java (通知发送消费者)
│       └── DashboardConsumer.java (大屏推送消费者)
├── src/main/resources/
│   └── application.yml (Spring Boot配置)
└── pom.xml (Maven依赖)

1. pom.xml (添加依赖)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.grant.code</groupId>
	<artifactId>printer-alarm-system</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>printer-alarm-system</name>
	<description>Demo project for Spring Boot + RabbitMQ with 3D Printer Alarms</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-- Spring Boot AMQP Starter for RabbitMQ -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

		<!-- Jackson for JSON processing -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>

		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
			<version>2.0.1.Final</version>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

2. application.yml (配置RabbitMQ连接和消费者)

yml 复制代码
spring:
  rabbitmq:
    host: XXX # 根据你的RabbitMQ服务器地址修改
    port: XXX     # 根据你的RabbitMQ服务器端口修改
    username: XXX # 根据你的RabbitMQ用户名修改
    password: XXX # 根据你的RabbitMQ密码修改
    virtual-host: /
    listener:
      simple:
        # 手动确认模式,确保消息处理成功后再确认
        acknowledge-mode: manual
        # 预取消息数量,控制消费者负载
        prefetch: 10
        # 重试配置
        retry:
          enabled: true
          max-attempts: 3 # 最大重试次数
          initial-interval: 2000ms # 初始重试间隔
    # 发布者确认
    publisher-confirm-type: correlated
    publisher-returns: true

# 自定义配置
app:
  rabbitmq:
    exchange: alarm_events
    queue:
      db: alarm_db_writer
      notify: alarm_notifier
      dashboard: alarm_dashboard
    routing-key: alarm.event

logging:
  level:
    com.example.printer: DEBUG # 启用DEBUG日志查看详细流程

server:
  port: 8080 # API服务端口

3. PrinterAlarmEvent.java (数据传输对象)

java 复制代码
package com.grant.code.printeralarmsystem.dto;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;
import java.util.Objects;

public class PrinterAlarmEvent {

    private String printerId; // 打印机ID
    private String alarmType; // 指定告警类型,例如:喷头堵塞 (Extruder Jam)、材料耗尽 (Filament Runout)
    private String description; // 具体描述
    private String severity; // 这个指示告警的严重程度,例如:"Low", "Medium", "High"

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime timestamp;

    // Constructors
    public PrinterAlarmEvent() {}

    public PrinterAlarmEvent(String printerId, String alarmType, String description, String severity) {
        this.printerId = printerId;
        this.alarmType = alarmType;
        this.description = description;
        this.severity = severity;
        this.timestamp = LocalDateTime.now();
    }

    // Getters and Setters
    public String getPrinterId() { return printerId; }
    public void setPrinterId(String printerId) { this.printerId = printerId; }

    public String getAlarmType() { return alarmType; }
    public void setAlarmType(String alarmType) { this.alarmType = alarmType; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public String getSeverity() { return severity; }
    public void setSeverity(String severity) { this.severity = severity; }

    public LocalDateTime getTimestamp() { return timestamp; }
    public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }

    @Override
    public String toString() {
        return "PrinterAlarmEvent{" +
                "printerId='" + printerId + '\'' +
                ", alarmType='" + alarmType + '\'' +
                ", description='" + description + '\'' +
                ", severity='" + severity + '\'' +
                ", timestamp=" + timestamp +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PrinterAlarmEvent that = (PrinterAlarmEvent) o;
        // 只比较 printerId 和 alarmType,即如果printerId和alarmType相同,则认为两个事件相同
        return Objects.equals(printerId, that.printerId) && Objects.equals(alarmType, that.alarmType);
    }

    @Override
    public int hashCode() {
        return Objects.hash(printerId, alarmType);
    }
}

4. RabbitMQConfig.java (RabbitMQ配置类)

java 复制代码
package com.grant.code.printeralarmsystem.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ配置类
 */
@Configuration
public class RabbitMQConfig {

    @Value("${app.rabbitmq.exchange}")
    private String exchangeName; // 交换机名称

    @Value("${app.rabbitmq.queue.db}")
    private String dbQueueName; // 数据库写入队列名称

    @Value("${app.rabbitmq.queue.notify}")
    private String notifyQueueName; // 通知队列名称

    @Value("${app.rabbitmq.queue.dashboard}")
    private String dashboardQueueName; // 大屏显示队列名称

    @Value("${app.rabbitmq.routing-key}")
    private String routingKey; // 路由键,用于匹配队列

    // 创建交换机
    @Bean
    public DirectExchange alarmEventExchange() {
        return new DirectExchange(exchangeName, true, false); // durable=true表示持久化, autoDelete=false表示不自动删除
    }

    // 创建队列
    @Bean
    public Queue alarmDbWriterQueue() {
        return QueueBuilder.durable(dbQueueName).build(); // durable=true表示持久化,这里创建的是数据库写入队列
    }

    @Bean
    public Queue alarmNotifierQueue() {
        return QueueBuilder.durable(notifyQueueName).build(); // durable=true表示持久化,这里创建的是通知队列
    }

    @Bean
    public Queue alarmDashboardQueue() {
        return QueueBuilder.durable(dashboardQueueName).build(); // durable=true表示持久化,这里创建的是大屏显示队列
    }

    // 绑定队列到交换机
    @Bean
    public Binding bindingDbWriter(Queue alarmDbWriterQueue, DirectExchange alarmEventExchange) {
        return BindingBuilder.bind(alarmDbWriterQueue).to(alarmEventExchange).with(routingKey);
    }

    @Bean
    public Binding bindingNotifier(Queue alarmNotifierQueue, DirectExchange alarmEventExchange) {
        return BindingBuilder.bind(alarmNotifierQueue).to(alarmEventExchange).with(routingKey);
    }

    @Bean
    public Binding bindingDashboard(Queue alarmDashboardQueue, DirectExchange alarmEventExchange) {
        return BindingBuilder.bind(alarmDashboardQueue).to(alarmEventExchange).with(routingKey);
    }

    // 配置消息转换器为JSON
    @Bean
    public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    // 配置RabbitTemplate使用JSON转换器和确认回调
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); // 创建RabbitTemplate,connectionFactory为连接工厂
        rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter()); // 设置消息转换器为JSON
        // 开启发布确认
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                System.out.println(" [√] 消息发送成功: " + correlationData);
            } else {
                System.err.println(" [×] 消息发送失败: " + cause);
            }
        });
        // 开启发布返回 (针对无法路由的消息)
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(returned -> {
             System.err.println(" [!] 消息无法路由: " + returned);
        });
        return rabbitTemplate;
    }
}

5. AlarmEventPublisher.java (告警事件发布者)

java 复制代码
package com.grant.code.printeralarmsystem.publisher;

import com.grant.code.printeralarmsystem.config.RabbitMQConfig;
import com.grant.code.printeralarmsystem.dto.PrinterAlarmEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.UUID;

/**
 * 告警事件发布者
 */
@Service
public class AlarmEventPublisher {

    private static final Logger logger = LoggerFactory.getLogger(AlarmEventPublisher.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 注入配置的交换机名称
    @org.springframework.beans.factory.annotation.Value("${app.rabbitmq.exchange}")
    private String exchangeName;

    // 注入配置的路由键
    @org.springframework.beans.factory.annotation.Value("${app.rabbitmq.routing-key}")
    private String routingKey;


    public void publishAlarmEvent(PrinterAlarmEvent event) {
        // 生成唯一ID用于确认
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        logger.info(" [x] 发布告警事件: {} (ID: {})", event, correlationData.getId());
        // 发布到交换机,使用配置的路由键
        rabbitTemplate.convertAndSend(exchangeName, routingKey, event, correlationData);
    }
}

6. AlarmController.java (告警接收API)

java 复制代码
package com.grant.code.printeralarmsystem.controller;

import com.grant.code.printeralarmsystem.dto.PrinterAlarmEvent;
import com.grant.code.printeralarmsystem.publisher.AlarmEventPublisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

/**
 * 告警接收API
 */
@RestController
@RequestMapping("/api/alarms")
public class AlarmController {

    private static final Logger logger = LoggerFactory.getLogger(AlarmController.class);

    @Autowired
    private AlarmEventPublisher alarmEventPublisher;

    /**
     * 接收打印机告警的API端点
     * @param alarmEvent 告警事件对象
     * @return ResponseEntity 202 Accepted 表示已接收
     */
    @PostMapping("/receive")
    public ResponseEntity<String> receiveAlarm(@RequestBody @Valid PrinterAlarmEvent alarmEvent) {
        logger.info(" [x] 接收到告警: {}", alarmEvent);

        // 1. 快速验证(这里简化)
        if (alarmEvent.getPrinterId() == null || alarmEvent.getPrinterId().isEmpty()) {
            logger.warn(" [!] 无效的打印机ID");
            return ResponseEntity.badRequest().body("Invalid printer ID");
        }

        // 2. 将告警事件发布到RabbitMQ (异步操作)
        alarmEventPublisher.publishAlarmEvent(alarmEvent);

        // 3. 立即返回202 Accepted,表示请求已被接受处理
        logger.info(" [x] 告警已发布至队列,返回202 Accepted");
        return ResponseEntity.status(HttpStatus.ACCEPTED).body("Alarm received and queued for processing.");
    }
}

7.三个消费者

java 复制代码
package com.grant.code.printeralarmsystem.consumer;

import com.grant.code.printeralarmsystem.dto.PrinterAlarmEvent;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * 大屏消费者
 */
@Service
public class DashboardConsumer {

    private static final Logger logger = LoggerFactory.getLogger(DashboardConsumer.class);

    /**
     * 监听 alarm_dashboard 队列
     * @param event 告警事件
     * @param channel AMQP Channel 用于手动确认
     * @param tag Delivery Tag 用于确认特定消息
     */
    @RabbitListener(queues = "${app.rabbitmq.queue.dashboard}")
    public void handleDashboardPush(PrinterAlarmEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        try {
            logger.info(" [Dashboard Consumer] 开始处理大屏推送告警事件: {}", event);

            // 模拟非常快速的推送操作 (例如 0.1秒)
            Thread.sleep(100);
            // 这里应该是实际的WebSocket推送逻辑
            // webSocketService.sendToDashboard("/topic/alarms", event);
            logger.info(" [Dashboard Consumer] 已将告警推送到大屏: {}", event.getPrinterId());

            // 手动确认消息
            channel.basicAck(tag, false);
            logger.debug(" [Dashboard Consumer] 消息确认成功: {}", tag);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error(" [Dashboard Consumer] 处理中断: {}", event, e);
            try {
                channel.basicNack(tag, false, true);
            } catch (IOException ioException) {
                logger.error(" [Dashboard Consumer] 无法NACK消息: {}", tag, ioException);
            }
        } catch (Exception e) {
            logger.error(" [Dashboard Consumer] 推送大屏时发生错误: {}", event, e);
            try {
                channel.basicNack(tag, false, false);
            } catch (IOException ioException) {
                logger.error(" [Dashboard Consumer] 无法NACK消息: {}", tag, ioException);
            }
        }
    }
}
java 复制代码
package com.grant.code.printeralarmsystem.consumer;

import com.grant.code.printeralarmsystem.dto.PrinterAlarmEvent;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

import java.io.IOException;
/*
 * 告警事件写入数据库消费者
 */
@Service
public class DbWriterConsumer {

    private static final Logger logger = LoggerFactory.getLogger(DbWriterConsumer.class);

    /**
     * 监听 alarm_db_writer 队列
     * @param event 告警事件
     * @param channel AMQP Channel 用于手动确认
     * @param tag Delivery Tag 用于确认特定消息
     * header注解:用于获取消息的 Delivery Tag,Delivery Tag是消息的唯一标识符,用于确认消息处理成功或失败
     */
    @RabbitListener(queues = "${app.rabbitmq.queue.db}") // 监听 alarm_db_writer 队列
    public void handleDbWrite(PrinterAlarmEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        try {
            logger.info(" [DB Consumer] 开始处理告警事件: {}", event);

            // 模拟耗时的数据库写入操作 (例如 2秒)
            Thread.sleep(2000);
            // 这里应该是实际的数据库插入逻辑
            // repository.save(alarmRecord);
            logger.info(" [DB Consumer] 成功将告警写入数据库: {}", event.getPrinterId());

            // 手动确认消息
            channel.basicAck(tag, false);
            logger.debug(" [DB Consumer] 消息确认成功: {}", tag);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error(" [DB Consumer] 处理中断: {}", event, e);
            try {
                // 拒绝消息并重新入队
                channel.basicNack(tag, false, true);
            } catch (IOException ioException) {
                logger.error(" [DB Consumer] 无法NACK消息: {}", tag, ioException);
            }
        } catch (Exception e) {
            logger.error(" [DB Consumer] 处理告警事件时发生错误: {}", event, e);
            try {
                // 拒绝消息,不重新入队 (可以配置死信队列处理)
                channel.basicNack(tag, false, false);
            } catch (IOException ioException) {
                logger.error(" [DB Consumer] 无法NACK消息: {}", tag, ioException);
            }
        }
    }
}
java 复制代码
package com.grant.code.printeralarmsystem.consumer;

import com.grant.code.printeralarmsystem.dto.PrinterAlarmEvent;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * 告警通知消费者
 */
@Service
public class NotifierConsumer {

    private static final Logger logger = LoggerFactory.getLogger(NotifierConsumer.class);

    /**
     * 监听 alarm_notifier 队列
     * @param event 告警事件
     * @param channel AMQP Channel 用于手动确认
     * @param tag Delivery Tag 用于确认特定消息
     */
    @RabbitListener(queues = "${app.rabbitmq.queue.notify}")
    public void handleNotification(PrinterAlarmEvent event, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        try {
            logger.info(" [Notifier Consumer] 开始处理通知告警事件: {}", event);

            // 模拟耗时的通知发送操作 (例如 3秒)
            Thread.sleep(3000);
            // 这里应该是实际的邮件/SMS发送逻辑
            // emailService.send("admin@example.com", "Printer Alarm", event.toString());
            logger.info(" [Notifier Consumer] 已发送告警通知给运维人员: {}", event.getPrinterId());

            // 手动确认消息
            channel.basicAck(tag, false);
            logger.debug(" [Notifier Consumer] 消息确认成功: {}", tag);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error(" [Notifier Consumer] 处理中断: {}", event, e);
            try {
                channel.basicNack(tag, false, true);
            } catch (IOException ioException) {
                logger.error(" [Notifier Consumer] 无法NACK消息: {}", tag, ioException);
            }
        } catch (Exception e) {
            logger.error(" [Notifier Consumer] 发送通知时发生错误: {}", event, e);
            try {
                channel.basicNack(tag, false, false);
            } catch (IOException ioException) {
                logger.error(" [Notifier Consumer] 无法NACK消息: {}", tag, ioException);
            }
        }
    }
}

8. 主启动类

java 复制代码
package com.grant.code.printeralarmsystem;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PrinterAlarmSystemApplication {

	public static void main(String[] args) {
		SpringApplication.run(PrinterAlarmSystemApplication.class, args);
	}

}

启动服务后...

Postman进行测试

控制台输出

bash 复制代码
2025-08-24T17:46:08.251+08:00  INFO 20384 --- [nio-8080-exec-3] c.g.c.p.controller.AlarmController       :  [x] 接收到告警: PrinterAlarmEvent{printerId='PRINTER_001', alarmType='喷头堵塞', description='打印头堵塞,请检查耗材。', severity='严重', timestamp=null}
2025-08-24T17:46:08.251+08:00  INFO 20384 --- [nio-8080-exec-3] c.g.c.p.publisher.AlarmEventPublisher    :  [x] 发布告警事件: PrinterAlarmEvent{printerId='PRINTER_001', alarmType='喷头堵塞', description='打印头堵塞,请检查耗材。', severity='严重', timestamp=null} (ID: 3eacb1ac-c8b0-4cd6-afcd-f119e67a4a05)
2025-08-24T17:46:08.253+08:00  INFO 20384 --- [nio-8080-exec-3] c.g.c.p.controller.AlarmController       :  [x] 告警已发布至队列,返回202 Accepted
2025-08-24T17:46:08.266+08:00  INFO 20384 --- [ntContainer#2-1] c.g.c.p.consumer.NotifierConsumer        :  [Notifier Consumer] 开始处理通知告警事件: PrinterAlarmEvent{printerId='PRINTER_001', alarmType='喷头堵塞', description='打印头堵塞,请检查耗材。', severity='严重', timestamp=null}
2025-08-24T17:46:08.266+08:00  INFO 20384 --- [ntContainer#0-1] c.g.c.p.consumer.DashboardConsumer       :  [Dashboard Consumer] 开始处理大屏推送告警事件: PrinterAlarmEvent{printerId='PRINTER_001', alarmType='喷头堵塞', description='打印头堵塞,请检查耗材。', severity='严重', timestamp=null}
 [√] 消息发送成功: CorrelationData [id=3eacb1ac-c8b0-4cd6-afcd-f119e67a4a05]
2025-08-24T17:46:08.267+08:00  INFO 20384 --- [ntContainer#1-1] c.g.c.p.consumer.DbWriterConsumer        :  [DB Consumer] 开始处理告警事件: PrinterAlarmEvent{printerId='PRINTER_001', alarmType='喷头堵塞', description='打印头堵塞,请检查耗材。', severity='严重', timestamp=null}
2025-08-24T17:46:08.376+08:00  INFO 20384 --- [ntContainer#0-1] c.g.c.p.consumer.DashboardConsumer       :  [Dashboard Consumer] 已将告警推送到大屏: PRINTER_001
2025-08-24T17:46:10.270+08:00  INFO 20384 --- [ntContainer#1-1] c.g.c.p.consumer.DbWriterConsumer        :  [DB Consumer] 成功将告警写入数据库: PRINTER_001
2025-08-24T17:46:11.269+08:00  INFO 20384 --- [ntContainer#2-1] c.g.c.p.consumer.NotifierConsumer        :  [Notifier Consumer] 已发送告警通知给运维人员: PRINTER_001

可以看到,API几乎立即返回了202,而耗时的数据库写入(2秒)和通知发送(3秒)是在后台异步进行的,这解决了客户抱怨的"告警慢"问题。大屏推送是最快的(0.1秒)。这种架构实现了异步解耦、削峰填谷和提高可靠性。

查看Rabbitmq控制台

可以发现,自动创建了我们要的交换机,队列,以及绑定关系和使用的routeKey

随便选择一个队列,点击get message,发现队列消息为空,是因为消息已经被消费了,所以不再存在于队列当中

完整执行过程

核心流程:打印机上报告警 -> 系统接收 -> 异步处理

1. 系统启动阶段
  1. 启动Spring Boot应用 (PrinterAlarmSystemApplication.main):
    • Spring Boot框架启动,加载所有配置和Bean。
  2. RabbitMQ连接与配置 (RabbitMQConfig):
    • 应用根据 application.yml 中的配置连接到RabbitMQ服务器。
    • RabbitMQConfig 类中的 @Bean方法被执行:
      • 创建一个名为 alarm_eventsDirect Exchange (交换机)。
      • 创建三个 Queue (队列):alarm_db_writer, alarm_notifier, alarm_dashboard。这些队列被设置为持久化(durable),即使RabbitMQ重启,队列也不会丢失。
      • 通过 Binding (绑定) 将这三个队列都绑定到 alarm_events 交换机上,并使用相同的 Routing Key (alarm.event)。这意味着发送到 alarm_events 交换机且 Routing Key 为 alarm.event 的消息,会被路由到所有这三个队列。
      • 配置 RabbitTemplate 使用 Jackson2JsonMessageConverter 来自动序列化/反序列化Java对象(PrinterAlarmEvent)为JSON格式进行传输,并设置发布确认回调。
  3. 消费者监听启动 (@RabbitListener):
    • Spring AMQP框架扫描到 DbWriterConsumer, NotifierConsumer, DashboardConsumer 类中的 @RabbitListener 注解。
    • 它分别为每个消费者在对应的队列 (alarm_db_writer, alarm_notifier, alarm_dashboard) 上启动监听器,等待消息的到来。

此时,系统基础设施(API、RabbitMQ交换机/队列/绑定、消费者监听)已准备就绪,等待打印机发送告警。

2. 告警产生与接收阶段
  1. 模拟打印机发送告警

    用postman发出请求:localhost:8080/api/alarms/receive

    注意:Content-Type:application/json

    json 复制代码
    {
        "printerId": "PRINTER_001",
        "alarmType": "喷头堵塞",
        "description": "打印头堵塞,请检查耗材。",
        "severity": "严重"
    }
  2. API处理 (AlarmController.receiveAlarm):

  • Spring MVC框架接收到请求,将JSON体反序列化为 PrinterAlarmEvent 对象。
  • 控制器方法 receiveAlarm 被调用。
  • 进行简单的参数校验(例如检查 printerId 是否为空)。
  • 关键步骤 :调用 AlarmEventPublisher.publishAlarmEvent(event) 方法,将接收到的 PrinterAlarmEvent 对象交给RabbitMQ处理。
  • 立即响应 :不等待RabbitMQ处理完成或消费者执行完毕,直接返回HTTP状态码 202 Accepted 给打印机。这实现了文档中提到的"快速响应设备"。
3.消息发布到mq阶段

发布消息 (AlarmEventPublisher.publishAlarmEvent):

  • 该方法通过注入的 RabbitTemplate 对象,调用 convertAndSend 方法。
  • RabbitTemplate 使用之前配置的 Jackson2JsonMessageConverterPrinterAlarmEvent 对象序列化为JSON格式的消息体。
  • 消息被发送到之前配置的 alarm_events 交换机 (exchangeName),并附带路由键 alarm.event (routingKey)。
  • RabbitTemplateConfirmCallback 被触发(异步),确认消息是否成功发送到了RabbitMQ Broker。日志会打印 [x] 消息发送成功[!] 消息发送失败
4.RabbitMQ路由阶段

交换机路由:

  • RabbitMQ Broker接收到发送到 alarm_events 交换机的消息。
  • 交换机根据绑定规则(Binding)检查消息的路由键 alarm.event
  • 因为三个队列 (alarm_db_writer, alarm_notifier, alarm_dashboard) 都绑定了相同的路由键,所以交换机会将这条消息的一个副本分别放入这三个队列中。

此时,一条告警消息已经成功进入RabbitMQ系统,并被复制分发到三个专门的处理队列中。

5. 异步消费者处理阶段

三个消费者的处理是 完全独立且并发 进行的。

消费者C3 (大屏推送 DashboardConsumer)

  1. 消息获取 : RabbitMQ将 alarm_dashboard 队列中的消息推送给监听该队列的 DashboardConsumer 实例。
  2. 处理逻辑 (handleDashboardPush):
    • 方法被调用,接收到 PrinterAlarmEvent 对象和RabbitMQ的 ChanneldeliveryTag
    • 执行模拟的快速推送操作(Thread.sleep(100))。
    • 手动确认 : 调用 channel.basicAck(tag, false) 向RabbitMQ发送确认信号,告知消息已成功处理,RabbitMQ会从 alarm_dashboard 队列中移除此消息。
    • 日志: 打印处理开始、推送成功和确认成功的日志。

消费者C1 (数据库写入 DbWriterConsumer)

  1. 消息获取 : RabbitMQ将 alarm_db_writer 队列中的消息推送给监听该队列的 DbWriterConsumer 实例。
  2. 处理逻辑 (handleDbWrite):
    • 方法被调用。
    • 执行模拟的耗时数据库写入操作(Thread.sleep(2000))。
    • 手动确认 : 处理成功后,调用 channel.basicAck 确认消息。
    • 错误处理 : 如果在处理过程中发生异常(如 InterruptedException 或其他 Exception),会调用 channel.basicNack 来否定消息。根据配置(requeue=true/false),消息可能会被重新放回队列或丢弃(或进入死信队列,如果配置了的话)。
    • 日志: 打印处理开始、写库成功和确认成功的日志。

消费者C2 (通知发送 NotifierConsumer)

  1. 消息获取 : RabbitMQ将 alarm_notifier 队列中的消息推送给监听该队列的 NotifierConsumer 实例。
  2. 处理逻辑 (handleNotification):
    • 方法被调用。
    • 执行模拟的耗时通知发送操作(Thread.sleep(3000))。
    • 手动确认 : 处理成功后,调用 channel.basicAck 确认消息。
    • 错误处理 : 同样有 try-catchbasicNack 逻辑来处理失败情况。
    • 日志: 打印处理开始、发送通知成功和确认成功的日志。

tMQ会从 alarm_dashboard 队列中移除此消息。

  • 日志: 打印处理开始、推送成功和确认成功的日志。

消费者C1 (数据库写入 DbWriterConsumer)

  1. 消息获取 : RabbitMQ将 alarm_db_writer 队列中的消息推送给监听该队列的 DbWriterConsumer 实例。
  2. 处理逻辑 (handleDbWrite):
    • 方法被调用。
    • 执行模拟的耗时数据库写入操作(Thread.sleep(2000))。
    • 手动确认 : 处理成功后,调用 channel.basicAck 确认消息。
    • 错误处理 : 如果在处理过程中发生异常(如 InterruptedException 或其他 Exception),会调用 channel.basicNack 来否定消息。根据配置(requeue=true/false),消息可能会被重新放回队列或丢弃(或进入死信队列,如果配置了的话)。
    • 日志: 打印处理开始、写库成功和确认成功的日志。

消费者C2 (通知发送 NotifierConsumer)

  1. 消息获取 : RabbitMQ将 alarm_notifier 队列中的消息推送给监听该队列的 NotifierConsumer 实例。
  2. 处理逻辑 (handleNotification):
    • 方法被调用。
    • 执行模拟的耗时通知发送操作(Thread.sleep(3000))。
    • 手动确认 : 处理成功后,调用 channel.basicAck 确认消息。
    • 错误处理 : 同样有 try-catchbasicNack 逻辑来处理失败情况。
    • 日志: 打印处理开始、发送通知成功和确认成功的日志。

整个过程通过RabbitMQ实现了异步解耦,API响应迅速,不同耗时的操作并行处理,有效解决了客户抱怨的"告警慢"问题。