RabbitMQ 消息队列、XXL-JOB 分布式定时任务与 WebSocket 实时通信
2026-07-28 周二 AI 应用 Web 开发 阅读约 18 分钟
本文目录
- 今日学习内容
- [RabbitMQ 消息队列基础](#RabbitMQ 消息队列基础)
- [Spring AMQP 整合实战](#Spring AMQP 整合实战)
- [XXL-JOB 分布式定时任务](#XXL-JOB 分布式定时任务)
- [WebSocket 实时通信](#WebSocket 实时通信)
- 踩坑记录
- 收获总结
- 明日计划
今日学习内容
今天是 Day 19,继续在第三阶段「AI 应用 Web 开发」中深耕。昨天我们完成了 Maven 私服搭建和阿里云 OSS 文件上传系统,今天按照计划,一次性攻克三个非常实用的分布式组件:
- RabbitMQ 消息队列:Direct/Fanout/Topic 三种交换机模型、Spring AMQP 整合、消息可靠性保证(确认/投递/消费)
- XXL-JOB 分布式定时任务:调度中心部署、执行器接入、任务类型(Bean/GLUE/Shell)、分片广播策略
- WebSocket 实时通信:STOMP 协议、Spring 内置 WebSocket 支持、在线聊天室与消息推送实战
前置知识回顾:Day 14 我们接触了微服务架构,Day 15 学习了 Gateway 网关和 Sentinel 限流。今天学习的消息队列和定时任务正是微服务间异步解耦的两大基础设施,而 WebSocket 则是前端实时交互的核心技术。
RabbitMQ 消息队列基础
为什么需要消息队列?
在传统的同步调用中,A 服务直接调用 B 服务,如果 B 服务挂了或者响应慢,A 服务也会跟着出问题。消息队列的核心价值就是异步解耦:
- 异步处理:用户注册后发送短信/邮件,不需要等待发送完成才返回注册成功
- 应用解耦:订单系统与库存系统通过消息队列通信,互不影响
- 流量削峰:秒杀场景下,海量请求先进入队列,系统按能力匀速消费
- 日志收集:分散的日志统一发送到队列,由消费者批量处理入库
RabbitMQ 核心概念
RabbitMQ 是基于 AMQP 协议的消息中间件,理解下面几个核心概念是入门的关键:
| 概念 | 说明 | 类比 |
|---|---|---|
| Exchange | 交换机,接收生产者消息并路由到队列 | 邮局分拣员 |
| Queue | 队列,存储消息的缓冲区 | 邮箱 |
| Binding | 绑定,Exchange 与 Queue 的路由规则 | 地址标签 |
| Routing Key | 路由键,生产者指定的投递地址 | 收件人地址 |
| Channel | 信道,轻量级连接,建立在 Connection 之上 | 光纤中的单根线 |
三种交换机类型
RabbitMQ 提供了四种交换机类型,但最常用的有三种,理解它们的区别至关重要:
1. Direct(直连)------ 精确匹配
生产者发送消息时指定 Routing Key,Exchange 只将消息投递到 Binding Key 完全相同的 Queue。适用于点对点精确投递场景。
RabbitMQ Direct Exchange 示意图Concept
Producer ──routingKey="order.created"──> [Exchange: direct]
│
bindingKey="order.created" ↓
[Queue: order_queue]
│
Consumer
2. Fanout(扇出)------ 广播
无视 Routing Key,将消息广播到所有绑定的 Queue。适用于消息需要被多个服务同时处理的场景,如日志广播、缓存失效通知。
3. Topic(主题)------ 模式匹配
Routing Key 和 Binding Key 都支持通配符匹配:* 匹配一个单词,# 匹配零个或多个单词。适用于灵活的消息分类场景。
Topic 通配符示例Concept
Binding Key "order.*.created" 匹配: order.pay.created, order.ship.created
不匹配: order.pay.cancel.created (三个单词)
Binding Key "order.#" 匹配: order, order.pay, order.pay.created, order.a.b.c.d
(# 可以匹配任意层级)
Docker 部署 RabbitMQ
docker-compose.ymlYAML
version: '3.8'
services:
rabbitmq:
image: rabbitmq:3.13-management
container_name: rabbitmq
ports:
- "5672:5672" # AMQP 协议端口
- "15672:15672" # Web 管理界面
volumes:
- rabbitmq-data:/var/lib/rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin123
volumes:
rabbitmq-data:
driver: local
管理界面 :启动后访问 http://localhost:15672,默认用户名密码为 admin/admin123。在这里可以直观看到 Exchange、Queue、Bindings 以及消息的消费情况。
Spring AMQP 整合实战
Spring 官方提供了 spring-boot-starter-amqp 来简化 RabbitMQ 的集成,让我们通过实战代码来掌握 Direct、Fanout、Topic 三种模式。
引入依赖与配置
pom.xmlXML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.ymlYAML
spring:
rabbitmq:
host: localhost
port: 5672
username: admin
password: admin123
virtual-host: /
publisher-confirm-type: correlated # 开启发布确认
publisher-returns: true # 开启投递失败回调
listener:
simple:
acknowledge-mode: manual # 手动确认
retry:
enabled: true
max-attempts: 3
initial-interval: 1000
Direct Exchange 实战 --- 订单状态通知
RabbitMQConfig.javaJava
@Configuration
public class RabbitMQConfig {
// 定义 Direct Exchange
@Bean
public DirectExchange orderExchange() {
return new DirectExchange("order.exchange");
}
// 定义队列
@Bean
public Queue orderCreatedQueue() {
return QueueBuilder.durable("order.created.queue")
.withArgument("x-dead-letter-exchange", "order.dlx")
.withArgument("x-dead-letter-routing-key", "order.dead")
.build();
}
// 绑定 Exchange 与 Queue
@Bean
public Binding orderCreatedBinding() {
return BindingBuilder.bind(orderCreatedQueue())
.to(orderExchange())
.with("order.created");
}
}
OrderProducer.javaJava
@Service
@RequiredArgsConstructor
public class OrderProducer {
private final RabbitTemplate rabbitTemplate;
public void sendOrderCreated(Order order) {
CorrelationData correlationData = new CorrelationData(order.getId());
rabbitTemplate.convertAndSend(
"order.exchange", // exchange
"order.created", // routing key
order, // message
message -> { // 消息属性处理器
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
},
correlationData // 用于确认回调
);
}
}
OrderConsumer.javaJava
@Service
@Slf4j
public class OrderConsumer {
@RabbitListener(queues = "order.created.queue")
public void onOrderCreated(Order order, Channel channel, Message message) throws IOException {
try {
log.info("收到订单创建消息: {}", order.getId());
// 业务处理:扣减库存、发送短信...
processOrder(order);
// 手动确认消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
log.error("订单处理失败: {}", order.getId(), e);
// 拒绝消息,不重新入队,进入死信队列
channel.basicNack(deliveryTag, false, false);
}
}
}
消息可靠性保证 --- 三板斧
在企业级应用中,消息丢失是不可接受的。RabbitMQ 提供了三层保障机制:
- 生产者确认(Publisher Confirm):Broker 收到消息后异步回调 confirm 接口,告知生产者消息是否成功到达 Exchange
- 消息持久化(Message Persistence) :将
deliveryMode设为 PERSISTENT,消息会写入磁盘,即使 RabbitMQ 重启也不会丢失 - 消费者手动确认(Manual ACK):消费者处理完业务后才发送 ACK,处理失败则进入死信队列(DLX)或重试
死信队列(DLX):当消息被拒绝且不重新入队、或者消息过期、或者队列达到最大长度时,消息会被转发到死信交换机。这是处理异常消息的优雅方案,避免阻塞正常队列。
XXL-JOB 分布式定时任务
为什么不用 Spring 自带的 @Scheduled?
Spring 的 @Scheduled 在单机场景下很方便,但在分布式环境中有致命缺陷:
- 任务重复执行:多台服务器同时运行相同任务,导致数据重复处理
- 无调度中心:无法统一管理、监控、手动触发任务
- 无分片能力:海量数据处理时无法分摊到多台机器并行执行
- 无失败告警:任务失败只能靠日志发现,无主动通知
XXL-JOB 是许雪里开源的分布式任务调度平台,恰好解决了这些问题,而且接入成本极低。
架构设计
XXL-JOB 采用中心化调度设计,包含两个核心角色:
- 调度中心(Admin):负责任务管理、触发、调度、监控,是一个独立的 Web 应用
- 执行器(Executor):嵌入在业务服务中,接收调度中心的指令并执行任务
Docker Compose 部署调度中心YAML
version: '3.8'
services:
xxl-job-admin:
image: xuxueli/xxl-job-admin:2.4.1
container_name: xxl-job-admin
ports:
- "8080:8080"
environment:
- PARAMS=--spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job
--spring.datasource.username=root
--spring.datasource.password=root123
depends_on:
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=root123
- MYSQL_DATABASE=xxl_job
volumes:
- ./tables_xxl_job.sql:/docker-entrypoint-initdb.d/init.sql
Spring Boot 执行器接入
application.ymlYAML
xxl:
job:
admin:
addresses: http://localhost:8080/xxl-job-admin # 调度中心地址
executor:
appname: my-service-executor # 执行器名称,调度中心会按这个名称找机器
ip: # 留空自动获取
port: 9999 # 执行器监听端口
logpath: /data/applogs/xxl-job/jobhandler # 执行日志路径
logretentiondays: 30
XxlJobConfig.javaJava
@Configuration
public class XxlJobConfig {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(adminAddresses);
executor.setAppname(appName);
executor.setLogRetentionDays(30);
return executor;
}
}
编写定时任务 --- Bean 模式
OrderCheckJob.javaJava
@Component
@Slf4j
public class OrderCheckJob {
/**
* 每 5 分钟检查一次超时未支付订单
* 在调度中心配置 Cron: 0 */5 * * * ?
*/
@XxlJob("orderTimeoutCheckHandler")
public ReturnT<String> orderTimeoutCheck() {
log.info("开始执行订单超时检查...");
// 获取分片参数,支持分布式分片处理
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
// 只处理当前分片负责的订单: id % shardTotal == shardIndex
List<Order> timeoutOrders = orderService
.findTimeoutOrders(shardIndex, shardTotal);
for (Order order : timeoutOrders) {
try {
orderService.cancelOrder(order.getId());
log.info("订单已取消: {}", order.getId());
} catch (Exception e) {
log.error("取消订单失败: {}", order.getId(), e);
}
}
return ReturnT.SUCCESS;
}
}
分片广播策略 --- 海量数据处理神器
假设有 100 万条订单需要处理,单台机器要跑很久。XXL-JOB 的分片广播策略可以将任务平均分配到多台执行器上:
- 调度中心广播触发所有执行器
- 每台执行器通过
getShardIndex()和getShardTotal()知道自己负责哪部分数据 - 通过
id % shardTotal == shardIndex过滤数据,天然避免重复处理
调度策略对比:路由策略有「第一个」、「最后一个」、「轮询」、「随机」、「一致性Hash」、「分片广播」等。其中「分片广播」最适合大数据量处理,「一致性Hash」适合有状态任务(同一任务总在同一台机器执行)。
WebSocket 实时通信
为什么不用轮询?
传统的 HTTP 轮询(Polling)和 long-polling 都有明显缺陷:前者浪费带宽和服务器资源,后者仍受 HTTP 半双工限制。WebSocket 是 HTML5 引入的全双工通信协议,客户端和服务器可以双向主动发送消息,连接建立后只需一次握手,后续数据传输开销极小。
Spring Boot 内置 WebSocket 支持
Spring 提供了两种 WebSocket 方案:
- 原生 WebSocket :使用
@ServerEndpoint注解,简单直接 - STOMP 协议:在 WebSocket 之上定义了帧格式,支持订阅/发布模式,与 Spring Messaging 深度集成
这里我们使用 STOMP 方案,因为它更规范,天然支持消息路由和广播。
后端配置
WebSocketConfig.javaJava
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单内存消息代理,目的地以 /topic 开头的消息会被广播
config.enableSimpleBroker("/topic", "/queue");
// 客户端发送消息的前缀,/app/hello 会被路由到 @MessageMapping("/hello")
config.setApplicationDestinationPrefixes("/app");
// 点对点发送的前缀
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// WebSocket 连接端点,SockJS 是降级兼容方案
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
ChatController.javaJava
@Controller
@Slf4j
public class ChatController {
private final SimpMessagingTemplate messagingTemplate;
// 接收客户端发来的消息,转发到 /topic/public
@MessageMapping("/chat.send")
public void sendMessage(@Payload ChatMessage message) {
message.setTimestamp(LocalDateTime.now());
log.info("收到消息: {}", message.getContent());
// 广播给所有订阅了 /topic/public 的客户端
messagingTemplate.convertAndSend("/topic/public", message);
}
// 用户加入通知
@MessageMapping("/chat.join")
public void join(@Payload ChatMessage message, SimpMessageHeaderAccessor headerAccessor) {
headerAccessor.getSessionAttributes().put("username", message.getSender());
message.setType(MessageType.JOIN);
messagingTemplate.convertAndSend("/topic/public", message);
}
// 系统推送(后端主动推)
public void sendSystemNotice(String content) {
ChatMessage notice = ChatMessage.builder()
.sender("System")
.content(content)
.type(MessageType.SYSTEM)
.build();
messagingTemplate.convertAndSend("/topic/public", notice);
}
}
前端 Vue 3 + STOMP 客户端
ChatRoom.vueVue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import SockJS from 'sockjs-client'
import { Client } from '@stomp/stompjs'
const messages = ref([])
const inputText = ref('')
const username = ref('User_' + Math.floor(Math.random() * 1000))
let stompClient = null
const connect = () => {
const client = new Client({
webSocketFactory: () => new SockJS('http://localhost:8080/ws'),
onConnect: () => {
console.log('WebSocket 连接成功')
// 订阅公共频道
client.subscribe('/topic/public', (message) => {
messages.value.push(JSON.parse(message.body))
})
// 发送加入消息
client.publish({
destination: '/app/chat.join',
body: JSON.stringify({ sender: username.value })
})
},
onDisconnect: () => console.log('WebSocket 断开')
})
client.activate()
stompClient = client
}
const sendMessage = () => {
if (!inputText.value.trim() || !stompClient) return
stompClient.publish({
destination: '/app/chat.send',
body: JSON.stringify({
sender: username.value,
content: inputText.value,
type: 'CHAT'
})
})
inputText.value = ''
}
onMounted(connect)
onUnmounted(() => stompClient?.deactivate())
</script>
在线人数统计与系统广播
WebSocketEventListener.javaJava
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final SimpMessagingTemplate messagingTemplate;
private final AtomicInteger onlineCount = new AtomicInteger(0);
@EventListener
public void handleSessionConnected(SessionConnectedEvent event) {
int count = onlineCount.incrementAndGet();
messagingTemplate.convertAndSend("/topic/online", count);
}
@EventListener
public void handleSessionDisconnect(SessionDisconnectEvent event) {
int count = onlineCount.decrementAndGet();
messagingTemplate.convertAndSend("/topic/online", count);
// 获取断开用户的用户名,发送离开通知
String username = (String) event.getSessionAttributes().get("username");
if (username != null) {
ChatMessage message = new ChatMessage();
message.setSender(username);
message.setType(MessageType.LEAVE);
messagingTemplate.convertAndSend("/topic/public", message);
}
}
}
系统广播场景:除了聊天室,WebSocket 还广泛应用于「订单状态实时推送」(用户支付后页面自动刷新)、「系统通知弹窗」(公告发布即刻触达)、「在线协作编辑」(多人同时编辑文档)等场景。
踩坑记录
坑 1:RabbitMQ 消费者重复消费消息
现象 :订单被处理了两次,数据库出现重复数据。
原因 :消费者处理完业务后没有发送 ACK,或者 ACK 发送失败,导致消息重新入队被再次投递。
解决 :开启手动确认模式(acknowledge-mode: manual),业务处理成功后立即 basicAck;同时数据库层对核心业务加唯一索引兜底。
坑 2:RabbitMQ 消息发送到不存在的 Exchange
现象 :生产者发送消息没有报错,但队列里永远收不到。
原因 :默认情况下 RabbitTemplate 不会报错,消息被静默丢弃。
解决 :开启 publisher-returns: true 并设置 mandatory = true,同时实现 ReturnsCallback 捕获不可路由消息,转存到死信队列或告警。
坑 3:XXL-JOB 任务重复执行
现象 :两台服务器部署了相同执行器,任务被触发两次。
原因 :执行器的 appname 相同,调度中心把它们当成同一个执行器的两个实例来做路由。
解决:这是正常行为!如果要求「同一时刻只有一个实例执行」,在调度中心把路由策略改为「第一个」或「一致性Hash」;如果要求「所有实例都执行但处理不同数据」,使用「分片广播」策略。
坑 4:WebSocket 连接成功但消息发不出去
现象 :前端显示连接成功,但发送消息后后端收不到,其他客户端也收不到。
原因 :STOMP 消息目的地写错了。前端发到 /chat/send,但后端 @MessageMapping 配置的是 /app/chat/send(因为有 setApplicationDestinationPrefixes("/app"))。
解决 :前端发送的目的地必须是 /app/chat.send,订阅的频道是 /topic/public,两者前缀不同,不要混淆。
坑 5:WebSocket 跨域问题
现象 :前端连接 WebSocket 报 403 Forbidden。
原因 :Spring 的 setAllowedOrigins("*") 在较新版本中被废弃,必须使用 setAllowedOriginPatterns("*")。
解决 :检查 registerStompEndpoints 方法,使用 setAllowedOriginPatterns 替代 setAllowedOrigins。
收获总结
今日核心收获
今天一次性拿下了分布式系统中的三大基础设施组件,收获满满:
- RabbitMQ:理解了 AMQP 核心概念(Exchange/Queue/Binding),掌握了 Direct/Fanout/Topic 三种交换机模型的使用场景,学会了用 Spring AMQP 整合,并建立了消息可靠性的三层保障(发布确认 + 消息持久化 + 手动 ACK)。
- XXL-JOB:明白了分布式定时任务与单机 @Scheduled 的本质区别,完成了调度中心 Docker 部署和执行器接入,掌握了分片广播策略处理海量数据的技巧。
- WebSocket:了解了 STOMP 协议在 WebSocket 之上的规范用法,实现了完整的在线聊天室功能(加入/离开通知、消息广播、在线人数统计),并掌握了后端主动推送消息的方法。
这三个组件的组合非常经典:RabbitMQ 负责异步解耦,XXL-JOB 负责定时调度,WebSocket 负责实时推送,几乎覆盖了所有分布式通信场景。
明日计划
Day 20 预告 · 第三阶段收尾 --- 综合项目实战:社客管家平台
- 社客管家项目需求分析与数据库设计(客户管理、IT 资产管理、工单系统)
- Spring Boot + Vue 3 全栈项目搭建与模块划分
- 整合已学技术栈:JWT 认证、Redis 缓存、RabbitMQ 异步通知、XXL-JOB 定时巡检
- WebSocket 实时消息推送(工单状态变更通知)