订单状态实时通知的生产级完整方案
核心是解决实际项目中可能出现的消息丢失、用户离线、重连、消息顺序、多实例部署等问题------下面我会提供可直接落地的方案设计、完整代码,以及每个关键问题的解决方案。
一、核心需求与方案设计
1. 核心需求
- 订单状态变更(下单→支付中→支付成功→发货→签收等)时,实时推送给对应用户(Web端/移动端)。
- 解决实际问题:用户离线不丢消息、WebSocket断连自动重连、消息不重复/不丢失、同一订单消息顺序正确、支持多服务实例部署。
2. 整体架构
markdown
订单服务 → 发送状态消息(带唯一ID+订单ID+状态)→ RabbitMQ(持久化+确认机制)→ 消费者(手动ACK)
→ 检查用户是否在线:
- 在线 → WebSocket实时推送
- 离线 → Redis缓存离线消息
→ 用户上线(WebSocket重连)→ 拉取Redis离线消息 → 推送完成后删除缓存
3. 关键技术选型
- 后端:Spring Boot 2.7.x + Spring AMQP(RabbitMQ)+ Spring WebSocket + Redis(离线消息缓存)
- 前端:HTML5 WebSocket + 自动重连逻辑
- 中间件:RabbitMQ 3.9+(可靠消息传输)、Redis 6.x(离线消息+分布式锁)
4. 核心问题解决方案
| 实际问题 | 解决方案 |
|---|---|
| 消息丢失 | RabbitMQ队列/消息持久化、生产者确认(Publisher Confirm)、消费者手动ACK |
| 用户离线消息丢失 | Redis缓存用户离线消息,用户上线后自动拉取 |
| WebSocket断连 | 前端自动重连(3秒重试一次)、后端保留用户连接状态(短时间内重连无需重新初始化) |
| 同一订单消息顺序错乱 | RabbitMQ单队列(同一订单消息入同一队列)+ 消费者单线程处理 |
| 多服务实例部署推送失败 | Redis发布订阅同步消息到所有实例,仅连接当前实例的用户接收推送 |
| 消息重复推送 | 消息加唯一ID(订单ID+状态变更时间戳),Redis记录已处理ID去重 |
二、完整代码实现(生产级)
1. 前置准备
- 启动RabbitMQ(开启管理界面,默认账号guest/guest)
- 启动Redis(默认端口6379,无密码)
- 项目统一配置:
application.yml
yaml
spring:
# RabbitMQ配置(可靠消息传输核心)
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
# 生产者确认机制(确保消息发送到MQ)
publisher-confirm-type: correlated
# 生产者返回机制(确保消息路由成功)
publisher-returns: true
template:
mandatory: true # 路由失败时返回消息
# 消费者手动ACK(确保消息被正确处理)
listener:
simple:
acknowledge-mode: manual # 手动确认
concurrency: 1 # 单线程消费(保证消息顺序)
max-concurrency: 1
prefetch: 1 # 每次只拉取1条消息,处理完再拉取(顺序性)
# Redis配置(离线消息缓存)
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms
# Web配置
web:
resources:
static-locations: classpath:/static/ # 前端页面存放目录
# 应用配置
server:
port: 8080
servlet:
context-path: /
2. 依赖配置(pom.xml)
xml
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
3. 核心配置类
(1)RabbitMQ配置(可靠消息+顺序性)
typescript
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.rabbitmq.client.Channel;
/**
* RabbitMQ配置:确保消息可靠传输+顺序性
*/
@Configuration
public class RabbitMQOrderConfig {
// 订单状态通知队列(持久化)
public static final String ORDER_STATUS_QUEUE = "queue.order.status.notify";
// 交换机(直连交换机,精准路由)
public static final String ORDER_STATUS_EXCHANGE = "exchange.order.status.notify";
// 路由键
public static final String ORDER_STATUS_ROUTING_KEY = "key.order.status";
/**
* 订单状态队列(持久化、非独占、不自动删除)
*/
@Bean
public Queue orderStatusQueue() {
return QueueBuilder.durable(ORDER_STATUS_QUEUE)
.exclusive(false)
.autoDelete(false)
.build();
}
/**
* 直连交换机(持久化)
*/
@Bean
public DirectExchange orderStatusExchange() {
return ExchangeBuilder.directExchange(ORDER_STATUS_EXCHANGE)
.durable(true)
.build();
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindOrderStatusQueue(Queue orderStatusQueue, DirectExchange orderStatusExchange) {
return BindingBuilder.bind(orderStatusQueue)
.to(orderStatusExchange)
.with(ORDER_STATUS_ROUTING_KEY);
}
/**
* RabbitTemplate配置(生产者确认+消息持久化)
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 消息持久化(默认是PERSISTENT,显式配置更稳妥)
rabbitTemplate.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
// 生产者确认回调(确认消息是否到达交换机)
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
System.err.println("消息发送到交换机失败:" + cause + ",消息ID:" + correlationData.getId());
// 失败重试逻辑(可结合定时任务重发)
}
});
// 消息返回回调(交换机路由到队列失败时触发)
rabbitTemplate.setReturnsCallback(returned -> {
System.err.println("消息路由到队列失败:" + returned.getReplyText() + ",消息内容:" + new String(returned.getMessage().getBody()));
});
return rabbitTemplate;
}
/**
* 消费者配置(手动ACK+单线程消费+顺序性)
*/
@Bean
public SimpleMessageListenerContainer orderStatusListenerContainer(
ConnectionFactory connectionFactory,
ChannelAwareMessageListener orderStatusMessageListener) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueueNames(ORDER_STATUS_QUEUE);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手动ACK
container.setConcurrency("1"); // 单线程消费(保证顺序)
container.setMaxConcurrency("1");
container.setPrefetchCount(1); // 每次拉取1条消息,处理完再拉取
container.setMessageListener(orderStatusMessageListener);
return container;
}
}
(2)Redis配置(离线消息缓存)
typescript
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置:用于存储离线消息
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// Key序列化(String)
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Value序列化(JSON)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setValueSerializer(jsonSerializer);
redisTemplate.setHashValueSerializer(jsonSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
(3)WebSocket配置(重连+离线消息拉取)
kotlin
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket基础配置
*/
@Configuration
public class WebSocketConfig {
// 自动注册@ServerEndpoint注解的WebSocket服务端
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4. 核心业务类
(1)订单消息模型(统一格式)
arduino
import lombok.Data;
import java.time.LocalDateTime;
/**
* 订单状态消息模型(前后端统一格式)
*/
@Data
public class OrderStatusMessage {
// 消息唯一ID(订单ID+时间戳,用于去重)
private String msgId;
// 订单ID(核心关联字段)
private String orderId;
// 用户ID(接收消息的用户)
private String userId;
// 订单状态(如:PENDING=待支付,PAID=已支付,SHIPPED=已发货,RECEIVED=已签收)
private String orderStatus;
// 状态描述(给用户看的文案)
private String statusDesc;
// 状态变更时间
private LocalDateTime changeTime;
}
(2)WebSocket服务端(核心推送逻辑)
typescript
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* WebSocket服务端:处理连接、推送消息、离线消息拉取
* 前端连接地址:ws://localhost:8080/order/ws/{userId}
*/
@ServerEndpoint("/order/ws/{userId}")
@Component
public class OrderWebSocketServer {
// 注入RedisTemplate(操作离线消息)
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 存储在线用户连接(线程安全):key=userId,value=Session
private static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
// 离线消息Redis Key前缀(user:offline:msg:1001 → 用户1001的离线消息)
private static final String OFFLINE_MSG_KEY_PREFIX = "user:offline:msg:";
// 消息去重Redis Key前缀(msg:processed:xxx → 已处理的消息ID)
private static final String PROCESSED_MSG_KEY_PREFIX = "msg:processed:";
/**
* 客户端连接成功时触发
*/
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
if (userId == null || userId.trim().isEmpty()) {
try {
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "用户ID不能为空"));
} catch (IOException e) {
e.printStackTrace();
}
return;
}
// 存储用户连接
ONLINE_USER_SESSIONS.put(userId, session);
System.out.println("用户[" + userId + "]连接成功,当前在线人数:" + ONLINE_USER_SESSIONS.size());
// 拉取离线消息并推送
pullOfflineMessages(userId, session);
}
/**
* 拉取用户离线消息并推送
*/
private void pullOfflineMessages(String userId, Session session) {
String offlineMsgKey = OFFLINE_MSG_KEY_PREFIX + userId;
try {
// 从Redis获取所有离线消息(List结构,左进右出)
List<Object> offlineMessages = redisTemplate.opsForList().range(offlineMsgKey, 0, -1);
if (offlineMessages != null && !offlineMessages.isEmpty()) {
for (Object msg : offlineMessages) {
if (msg != null && session.isOpen()) {
// 推送离线消息
session.getBasicRemote().sendText(JSON.toJSONString(msg));
// 标记消息已处理(去重)
OrderStatusMessage orderMsg = JSON.parseObject(JSON.toJSONString(msg), OrderStatusMessage.class);
markMessageProcessed(orderMsg.getMsgId());
}
}
// 推送完成后删除离线消息
redisTemplate.delete(offlineMsgKey);
System.out.println("用户[" + userId + "]离线消息推送完成,共" + offlineMessages.size() + "条");
}
} catch (Exception e) {
System.err.println("用户[" + userId + "]拉取离线消息失败:" + e.getMessage());
}
}
/**
* 客户端断开连接时触发
*/
@OnClose
public void onClose(@PathParam("userId") String userId, Session session) {
ONLINE_USER_SESSIONS.remove(userId);
System.out.println("用户[" + userId + "]断开连接,当前在线人数:" + ONLINE_USER_SESSIONS.size());
}
/**
* 连接异常时触发
*/
@OnError
public void onError(@PathParam("userId") String userId, Session session, Throwable error) {
System.err.println("用户[" + userId + "]WebSocket连接异常:" + error.getMessage());
ONLINE_USER_SESSIONS.remove(userId);
try {
if (session.isOpen()) {
session.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 核心方法:向指定用户推送订单状态消息(对外提供调用)
*/
public boolean pushOrderStatusMessage(OrderStatusMessage message) {
// 1. 消息去重检查(避免重复推送)
if (isMessageProcessed(message.getMsgId())) {
System.out.println("消息[" + message.getMsgId() + "]已处理,跳过推送");
return true;
}
String userId = message.getUserId();
Session session = ONLINE_USER_SESSIONS.get(userId);
try {
// 2. 用户在线:直接推送
if (session != null && session.isOpen()) {
session.getBasicRemote().sendText(JSON.toJSONString(message));
markMessageProcessed(message.getMsgId()); // 标记已处理
System.out.println("向用户[" + userId + "]推送订单[" + message.getOrderId() + "]状态:" + message.getOrderStatus());
return true;
} else {
// 3. 用户离线:存入Redis离线消息
String offlineMsgKey = OFFLINE_MSG_KEY_PREFIX + userId;
redisTemplate.opsForList().rightPush(offlineMsgKey, message);
// 离线消息有效期7天(可根据业务调整)
redisTemplate.expire(offlineMsgKey, 7, TimeUnit.DAYS);
System.out.println("用户[" + userId + "]离线,缓存订单[" + message.getOrderId() + "]状态消息");
return true;
}
} catch (Exception e) {
System.err.println("推送订单状态消息失败:" + e.getMessage());
return false;
}
}
/**
* 标记消息已处理(Redis存储,有效期24小时)
*/
private void markMessageProcessed(String msgId) {
String processedKey = PROCESSED_MSG_KEY_PREFIX + msgId;
redisTemplate.opsForValue().set(processedKey, "1", 24, TimeUnit.HOURS);
}
/**
* 检查消息是否已处理(去重)
*/
private boolean isMessageProcessed(String msgId) {
String processedKey = PROCESSED_MSG_KEY_PREFIX + msgId;
return Boolean.TRUE.equals(redisTemplate.hasKey(processedKey));
}
// 提供静态方法供消费者调用(通过Spring上下文获取实例)
private static OrderWebSocketServer instance;
@Autowired
public void setInstance(OrderWebSocketServer instance) {
OrderWebSocketServer.instance = instance;
}
public static OrderWebSocketServer getInstance() {
return instance;
}
}
(3)RabbitMQ消费者(接收消息并推送)
java
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* RabbitMQ消费者:监听订单状态消息,调用WebSocket推送
*/
@Component
public class OrderStatusMessageListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
// 1. 解析消息内容
String msgBody = new String(message.getBody(), "UTF-8");
OrderStatusMessage orderMsg = JSON.parseObject(msgBody, OrderStatusMessage.class);
System.out.println("收到订单状态消息:" + JSON.toJSONString(orderMsg));
// 2. 调用WebSocket推送消息
boolean pushSuccess = OrderWebSocketServer.getInstance().pushOrderStatusMessage(orderMsg);
// 3. 推送成功:手动ACK(确认消息已处理)
if (pushSuccess) {
channel.basicAck(deliveryTag, false);
} else {
// 推送失败:拒绝消息(重回队列,最多重试3次,超过则进入死信队列)
int retryCount = message.getMessageProperties().getHeader("x-retry-count") == null ? 0 : (int) message.getMessageProperties().getHeader("x-retry-count");
if (retryCount < 3) {
message.getMessageProperties().setHeader("x-retry-count", retryCount + 1);
channel.basicNack(deliveryTag, false, true); // 重回队列
} else {
channel.basicNack(deliveryTag, false, false); // 拒绝并丢弃(或进入死信队列)
System.err.println("消息[" + orderMsg.getMsgId() + "]重试3次失败,拒绝处理");
}
}
} catch (Exception e) {
// 异常处理:拒绝消息,避免死循环
channel.basicNack(deliveryTag, false, false);
System.err.println("处理订单状态消息异常:" + e.getMessage());
}
}
}
(4)订单服务(模拟订单状态变更并发送消息)
less
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 订单服务:模拟订单状态变更,发送消息到RabbitMQ
*/
@RestController
public class OrderServiceController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 模拟订单状态变更(对外提供接口,实际项目中由订单系统内部调用)
* 测试地址:http://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=PAID&statusDesc=订单支付成功
*/
@GetMapping("/order/changeStatus")
public String changeOrderStatus(
@RequestParam String orderId,
@RequestParam String userId,
@RequestParam String orderStatus,
@RequestParam String statusDesc) {
try {
// 1. 构建订单状态消息
OrderStatusMessage message = new OrderStatusMessage();
// 消息唯一ID(订单ID+时间戳,避免重复)
String msgId = orderId + "_" + System.currentTimeMillis();
message.setMsgId(msgId);
message.setOrderId(orderId);
message.setUserId(userId);
message.setOrderStatus(orderStatus);
message.setStatusDesc(statusDesc);
message.setChangeTime(LocalDateTime.now());
// 2. 发送消息到RabbitMQ(带消息ID,用于确认)
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(
RabbitMQOrderConfig.ORDER_STATUS_EXCHANGE,
RabbitMQOrderConfig.ORDER_STATUS_ROUTING_KEY,
JSON.toJSONString(message),
correlationData
);
System.out.println("订单[" + orderId + "]状态变更为[" + orderStatus + "],消息已发送到RabbitMQ");
return "订单状态变更成功!消息ID:" + msgId;
} catch (Exception e) {
System.err.println("订单状态变更失败:" + e.getMessage());
return "订单状态变更失败!";
}
}
}
5. 前端页面(支持自动重连+消息展示)
创建 src/main/resources/static/order-notify.html:
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>订单状态实时通知</title>
<style>
.container { width: 800px; margin: 20px auto; }
.message { padding: 10px; margin: 10px 0; border: 1px solid #eee; border-radius: 4px; }
.pending { background-color: #fff3cd; color: #856404; }
.paid { background-color: #d4edda; color: #155724; }
.shipped { background-color: #d1ecf1; color: #0c5460; }
.received { background-color: #f8d7da; color: #721c24; }
.offline { color: #666; font-style: italic; }
</style>
</head>
<body>
<div class="container">
<h1>订单状态实时通知(用户ID:1001)</h1>
<div id="messageContainer"></div>
</div>
<script>
// 配置
const userId = "1001"; // 固定用户ID(实际项目中从登录态获取)
const wsUrl = `ws://${window.location.host}/order/ws/${userId}`;
let websocket = null;
let reconnectTimer = null; // 重连定时器
// 初始化WebSocket
initWebSocket();
// 初始化方法
function initWebSocket() {
if ('WebSocket' in window) {
websocket = new WebSocket(wsUrl);
bindWebSocketEvents();
} else {
alert("您的浏览器不支持WebSocket,无法接收实时通知!");
}
}
// 绑定WebSocket事件
function bindWebSocketEvents() {
// 连接成功
websocket.onopen = function() {
console.log("WebSocket连接成功");
clearReconnectTimer(); // 清除重连定时器
addMessage("连接成功,正在接收订单实时通知...", "system");
};
// 接收消息
websocket.onmessage = function(event) {
const message = JSON.parse(event.data);
console.log("收到订单通知:", message);
// 格式化时间
const time = new Date(message.changeTime).toLocaleString();
// 添加消息到页面
let statusClass = getStatusClass(message.orderStatus);
let msgHtml = `
<div class="message ${statusClass}">
<p>时间:${time}</p>
<p>订单号:${message.orderId}</p>
<p>状态:${message.statusDesc}</p>
</div>
`;
document.getElementById("messageContainer").innerHTML += msgHtml;
};
// 连接关闭
websocket.onclose = function(event) {
console.log("WebSocket连接关闭,错误码:" + event.code + ",原因:" + event.reason);
addMessage("连接已断开,正在尝试重连...", "offline");
startReconnectTimer(); // 启动重连
};
// 连接错误
websocket.onerror = function(error) {
console.error("WebSocket连接错误:", error);
addMessage("连接异常,正在尝试重连...", "offline");
startReconnectTimer(); // 启动重连
};
}
// 启动重连定时器(3秒重试一次)
function startReconnectTimer() {
if (!reconnectTimer) {
reconnectTimer = setInterval(function() {
console.log("尝试重连WebSocket...");
initWebSocket();
}, 3000);
}
}
// 清除重连定时器
function clearReconnectTimer() {
if (reconnectTimer) {
clearInterval(reconnectTimer);
reconnectTimer = null;
}
}
// 根据订单状态获取样式类
function getStatusClass(orderStatus) {
switch(orderStatus) {
case "PENDING": return "pending";
case "PAID": return "paid";
case "SHIPPED": return "shipped";
case "RECEIVED": return "received";
default: return "";
}
}
// 添加系统消息
function addMessage(content, type) {
const time = new Date().toLocaleString();
let msgHtml = `
<div class="message ${type}">
<p>【系统】${time}:${content}</p>
</div>
`;
document.getElementById("messageContainer").innerHTML += msgHtml;
}
// 页面关闭时关闭WebSocket连接
window.onbeforeunload = function() {
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.close();
}
};
</script>
</body>
</html>
三、测试流程(验证所有核心功能)
1. 环境准备
- 启动RabbitMQ(默认端口5672,管理界面:http://localhost:15672)
- 启动Redis(默认端口6379)
- 启动Spring Boot应用(端口8080)
2. 功能测试
(1)在线用户实时接收通知
-
调用订单状态变更接口(模拟支付成功):
bashhttp://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=PAID&statusDesc=订单支付成功,正在准备发货 -
观察前端页面:无需刷新,立即收到"订单支付成功"的通知。
(2)离线用户消息缓存与推送
-
关闭前端页面(用户1001离线)
-
调用接口模拟订单发货:
bashhttp://localhost:8080/order/changeStatus?orderId=ORDER20240520001&userId=1001&orderStatus=SHIPPED&statusDesc=订单已发货,快递单号:SF123456789 -
重新打开前端页面:页面加载后,自动拉取离线消息,显示"订单已发货"的通知。
(3)WebSocket断连重连
- 打开前端页面,然后在浏览器开发者工具(F12)的「Network」面板中,禁用WebSocket连接(或关闭RabbitMQ/Redis模拟异常)
- 观察页面:显示"连接断开,正在尝试重连"
- 恢复RabbitMQ/Redis连接:页面自动重连成功,继续接收后续通知。
(4)消息去重测试
- 重复调用同一订单的同一状态接口(比如再次调用支付成功)
- 观察前端页面:仅接收一次通知,控制台显示"消息已处理,跳过推送"。
四、生产环境扩展建议
-
多实例部署:
- 方案:使用Redis发布订阅(当一个实例收到MQ消息后,通过Redis发布消息,其他实例订阅后推送给本地连接的用户)
- 核心:确保所有实例都能接收消息,仅向连接在本实例的用户推送。
-
死信队列:
- 配置RabbitMQ死信队列,处理重试3次仍失败的消息(如用户长期离线且Redis故障),避免消息丢失。
-
消息监控:
- 集成Spring Boot Actuator监控RabbitMQ队列长度、WebSocket连接数、离线消息数。
- 对接ELK栈或日志平台,记录消息推送日志,便于问题排查。
-
权限控制:
- 前端WebSocket连接时,携带用户登录令牌(如JWT),后端验证合法性后再建立连接。
总结
- 核心流程:订单状态变更 → RabbitMQ可靠传输 → 消费者处理 → 在线用户WebSocket推送/离线用户Redis缓存 → 重连后拉取离线消息。
- 关键解决的问题:消息不丢失、用户离线不丢消息、断连自动重连、消息不重复、顺序正确。
- 落地要点:依赖RabbitMQ的持久化+确认机制、Redis的离线消息缓存、WebSocket的重连逻辑,三者协同实现生产级可靠的实时通知。
这套方案完全基于Spring Cloud生态,代码可直接复用,同时覆盖了实际项目中可能遇到的核心问题,适合直接落地到生产环境。