一、企业级选型:SSE vs WebSocket 怎么选?
订单状态实时通知的核心特点是「服务端单向推送给客户端」(极少需要客户端向服务端发送大量数据),结合企业级场景的核心诉求(稳定性、兼容性、运维成本、开发效率),SSE 更适合本业务场景,选型依据如下:
| 对比维度 | SSE(推荐) | WebSocket | 企业级决策倾向 |
|---|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向(全双工) | 订单通知无需双向,SSE更轻量 |
| 协议基础 | 基于 HTTP/HTTPS(复用80/443端口) | 独立WebSocket协议(需额外端口/配置) | SSE无需调整防火墙,运维成本更低 |
| 兼容性 | 所有现代浏览器(IE除外),原生支持 | 大部分浏览器支持,但需处理兼容降级 | SSE前端无额外依赖,兼容性更优 |
| 连接特性 | 长连接,自动重连(浏览器原生支持) | 长连接,需手动实现重连逻辑 | SSE降低前端开发成本,稳定性更高 |
| 数据格式 | 仅文本(JSON/UTF-8),满足订单通知需求 | 二进制+文本,支持更复杂场景 | 订单通知仅需JSON,SSE足够 |
| 资源占用 | 轻量(单个连接占用资源少) | 相对较重(全双工协议开销) | 高并发场景下SSE可支持更多用户 |
| 扩展能力 | 支持断线重推、消息补发(需配合存储) | 支持,但需额外开发 | 两者均可,但SSE结合Redis更简单 |
企业级核心决策点:订单状态通知是「单向推送」的典型场景,SSE 基于 HTTP 协议的特性让它在「兼容性、运维成本、开发效率」上更具优势,而 WebSocket 更适合需要「双向高频通信」的场景(如聊天、实时协作)。因此,本业务场景优先选 SSE。
二、企业级 SSE + RabbitMQ 完整方案
1. 整体架构设计(企业级)
核心流程
订单服务(生产者)→ RabbitMQ(消息队列)→ 通知服务(消费者)
↓
SSE 长连接 → 前端用户(浏览器/APP)
企业级关键保障
-
解耦与可靠性:订单服务与通知服务通过 RabbitMQ 解耦,消息持久化+消费重试+死信队列避免消息丢失
-
幂等性:Redis 记录已推送的「订单ID-状态码」组合,防止重复推送
-
连接管理:
- 支持用户多端登录(1个 userId 对应多个 SSE 连接)
- 定时清理失效连接(防止内存泄漏)
- 浏览器原生自动重连,配合服务端断线重推
-
认证授权:基于 Token 校验用户身份,确保仅订单归属用户接收通知
-
可扩展性:支持集群部署(Redis 同步连接信息+消息补发)
-
可观测性:日志记录连接状态、消息推送结果,支持问题排查
2. 技术依赖(Maven)
SSE 无需额外 Starter(Spring MVC 原生支持),核心依赖如下(兼容 Spring Boot 2.7.x,企业级稳定版本):
<dependencies>
<!-- Spring Boot 核心(含 Spring MVC,支持 SSE) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Stream + RabbitMQ(消息消费) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- Redis(连接存储+幂等性+消息补发) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具类(JSON/校验/日志) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Spring Cloud 版本管理(与 Spring Boot 2.7.x 匹配) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3. 核心配置(application.yml)
spring:
# 应用名称
application:
name: order-notify-sse-service
# Redis 配置(连接存储+幂等性+消息补发)
redis:
host: localhost
port: 6379
password: 123456
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
# RabbitMQ + Stream 配置(消息可靠性核心)
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated # 生产者确认(确保消息发往MQ)
publisher-returns: true # 消息返回(路由失败时回调)
cloud:
stream:
binders:
rabbit-binder:
type: rabbit
environment:
spring:
rabbitmq:
template:
mandatory: true # 路由失败时强制返回消息
bindings:
# 订单状态变更输入通道(消费MQ消息)
orderStatusInput:
destination: order.status.exchange # 交换机名称(与订单服务一致)
binder: rabbit-binder
group: order-notify-sse-group # 消费组(避免集群重复消费)
content-type: application/json
consumer:
durable: true # 队列持久化
auto-startup: true # 自动启动消费者
max-attempts: 3 # 消费重试3次
back-off-initial-interval: 1000ms # 重试间隔:1s→2s→4s
back-off-multiplier: 2
default-requeue-rejected: false # 失败不回队列,进入死信
# RabbitMQ 绑定细节(企业级队列设计)
rabbit:
bindings:
orderStatusInput:
consumer:
exchange-type: topic # 主题交换机(灵活路由)
routing-key-pattern: order.status.* # 匹配路由键(如order.status.paid)
declare-exchange: true # 自动声明交换机
declare-queue: true # 自动声明队列
queue-name-prefix: "queue-" # 队列名:queue-order.status.exchange-xxx
dead-letter-exchange: order.status.dlx.exchange # 死信交换机
dead-letter-routing-key: order.status.dlx # 死信路由键
queue-arguments:
x-message-ttl: 600000 # 队列消息TTL:10分钟(未消费进入死信)
# SSE 配置(企业级优化)
sse:
connect-timeout: 300000 # SSE连接超时:5分钟(与前端重连间隔配合)
retry-interval: 3000 # 前端重连建议间隔:3秒
max-connections-per-user: 5 # 单用户最大连接数(限制多端登录数量)
cache-un_sent-msgs: true # 是否缓存未发送成功的消息(断线重推)
un_sent-msgs-expire: 3600 # 未发送消息过期时间:1小时
# 服务器配置
server:
port: 8080
servlet:
context-path: /
tomcat:
max-connections: 10000 # 最大并发连接(适配高并发场景)
# 日志配置(可观测性)
logging:
level:
org.springframework.web: INFO
org.springframework.cloud.stream: INFO
com.example.notify: DEBUG # 业务日志debug级别,便于排查
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
4. 核心业务实体
4.1 订单状态枚举(OrderStatusEnum.java)
package com.example.notify.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 企业级完整订单状态(覆盖全业务流程)
*/
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
CREATED("CREATED", "订单创建"),
PAID("PAID", "订单已支付"),
PAY_FAILED("PAY_FAILED", "支付失败"),
SHIPPED("SHIPPED", "订单已发货"),
DELIVERED("DELIVERED", "订单已送达"),
COMPLETED("COMPLETED", "订单完成"),
CANCELLED("CANCELLED", "订单取消"),
REFUNDING("REFUNDING", "退款中"),
REFUNDED("REFUNDED", "订单已退款");
private final String code; // 状态编码(MQ消息中传输)
private final String desc; // 状态描述(前端直接展示)
// 安全获取枚举(避免空指针,企业级异常处理)
public static OrderStatusEnum getByCode(String code) {
for (OrderStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("无效订单状态编码:" + code);
}
}
4.2 订单状态消息体(OrderStatusMessage.java)
package com.example.notify.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 订单状态变更消息DTO(生产者/消费者统一格式,企业级校验)
*/
@Data
@Accessors(chain = true)
public class OrderStatusMessage {
@NotBlank(message = "订单ID不能为空")
private String orderId; // 订单唯一标识(如:ORDER20250520001)
@NotNull(message = "用户ID不能为空")
private Long userId; // 订单归属用户ID(精准推送核心)
@NotBlank(message = "状态编码不能为空")
private String statusCode; // 状态编码(对应OrderStatusEnum)
private String statusDesc; // 状态描述(冗余字段,减少前端解析成本)
@NotNull(message = "状态变更时间不能为空")
private LocalDateTime changeTime;// 状态变更时间(前端展示用)
private String remark; // 备注(如:支付失败原因、物流单号)
// 快速构建消息(企业级工具方法)
public static OrderStatusMessage build(String orderId, Long userId, String statusCode, String remark) {
OrderStatusEnum status = OrderStatusEnum.getByCode(statusCode);
return new OrderStatusMessage()
.setOrderId(orderId)
.setUserId(userId)
.setStatusCode(statusCode)
.setStatusDesc(status.getDesc())
.setChangeTime(LocalDateTime.now())
.setRemark(remark);
}
}
4.3 SSE 推送响应体(SsePushResponse.java)
package com.example.notify.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* SSE 推送消息格式(前端统一解析)
*/
@Data
public class SsePushResponse<T> {
private String type = "ORDER_STATUS_CHANGE"; // 消息类型(便于前端区分多类SSE消息)
private int code = 200; // 状态码(200成功,500失败)
private T data; // 业务数据(订单状态信息)
private LocalDateTime pushTime; // 推送时间
public static <T> SsePushResponse<T> success(T data) {
SsePushResponse<T> response = new SsePushResponse<>();
response.setData(data);
response.setPushTime(LocalDateTime.now());
return response;
}
}
5. 企业级 SSE 核心组件
5.1 SSE 连接管理器(SseConnectionManager.java)
核心职责:线程安全地管理用户与 SSEEmitter 的映射(企业级连接管理核心)
package com.example.notify.sse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* 企业级 SSE 连接管理器(线程安全+连接清理+多端支持)
*/
@Component
@Slf4j
public class SseConnectionManager {
// 存储用户与SSE连接的映射:userId → 多个SSEEmitter(支持多端登录)
private final Map<Long, List<SseEmitter>> userEmitterMap = new ConcurrentHashMap<>();
@Value("${sse.max-connections-per-user}")
private int maxConnPerUser; // 单用户最大连接数
/**
* 新增用户连接
* @param userId 用户ID
* @param emitter SSE发射器
* @return 是否添加成功
*/
public boolean addConnection(Long userId, SseEmitter emitter) {
// 校验用户连接数是否超限
List<SseEmitter> emitters = userEmitterMap.getOrDefault(userId, new CopyOnWriteArrayList<>());
if (emitters.size() >= maxConnPerUser) {
log.warn("用户{}连接数超限(最大{}个),拒绝新连接", userId, maxConnPerUser);
return false;
}
// 添加连接并注册关闭回调(清理失效连接)
emitters.add(emitter);
userEmitterMap.put(userId, emitters);
log.debug("用户{}新增SSE连接,当前连接数:{}", userId, emitters.size());
// 连接关闭时自动清理
emitter.onCompletion(() -> removeConnection(userId, emitter));
emitter.onError((e) -> removeConnection(userId, emitter));
emitter.onTimeout(() -> removeConnection(userId, emitter));
return true;
}
/**
* 移除用户连接
*/
public void removeConnection(Long userId, SseEmitter emitter) {
List<SseEmitter> emitters = userEmitterMap.get(userId);
if (Objects.isNull(emitters) || emitters.isEmpty()) {
return;
}
emitters.remove(emitter);
log.debug("用户{}移除SSE连接,剩余连接数:{}", userId, emitters.size());
// 若用户无有效连接,移除整个条目(节省内存)
if (emitters.isEmpty()) {
userEmitterMap.remove(userId);
log.debug("用户{}无有效SSE连接,移除映射", userId);
}
}
/**
* 获取用户的所有有效连接
*/
public List<SseEmitter> getEmittersByUserId(Long userId) {
List<SseEmitter> emitters = userEmitterMap.getOrDefault(userId, Collections.emptyList());
// 过滤掉已完成/已超时的无效连接
return emitters.stream()
.filter(emitter -> !emitter.isCompleted() && !emitter.isTimedOut())
.collect(Collectors.toList());
}
/**
* 清理所有无效连接(定时任务调用,防止内存泄漏)
*/
public void cleanInvalidConnections() {
log.info("开始清理SSE无效连接,当前用户数:{}", userEmitterMap.size());
Iterator<Map.Entry<Long, List<SseEmitter>>> iterator = userEmitterMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, List<SseEmitter>> entry = iterator.next();
Long userId = entry.getKey();
List<SseEmitter> emitters = entry.getValue();
// 过滤有效连接
List<SseEmitter> validEmitters = emitters.stream()
.filter(emitter -> !emitter.isCompleted() && !emitter.isTimedOut())
.collect(Collectors.toList());
if (validEmitters.isEmpty()) {
iterator.remove(); // 无有效连接,移除用户映射
log.debug("清理用户{}的无效连接(无有效连接)", userId);
} else {
userEmitterMap.put(userId, validEmitters); // 更新为有效连接
log.debug("用户{}有效连接数:{}", userId, validEmitters.size());
}
}
log.info("SSE无效连接清理完成,剩余用户数:{}", userEmitterMap.size());
}
/**
* 获取当前连接统计
*/
public Map<String, Integer> getConnectionStats() {
int totalUsers = userEmitterMap.size();
int totalConnections = userEmitterMap.values().stream()
.flatMap(List::stream)
.filter(emitter -> !emitter.isCompleted() && !emitter.isTimedOut())
.collect(Collectors.toList())
.size();
Map<String, Integer> stats = new HashMap<>();
stats.put("totalUsers", totalUsers);
stats.put("totalConnections", totalConnections);
return stats;
}
}
5.2 SSE 认证拦截器(SseAuthInterceptor.java)
核心职责:验证 SSE 连接的用户合法性(企业级安全保障)
package com.example.notify.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
/**
* SSE 连接认证拦截器(企业级:防止非法连接、越权访问)
*/
@Component
@Slf4j
public class SseAuthInterceptor implements HandlerInterceptor {
// 实际企业级场景:替换为 JWT/Token 校验逻辑(如从Header获取Token,调用用户服务校验)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取用户ID(实际场景:从Token解析,此处简化为请求参数)
String userIdStr = request.getParameter("userId");
String token = request.getHeader("Authorization");
// 2. 基础校验
if (Objects.isNull(userIdStr) || Objects.isNull(token)) {
log.warn("SSE连接认证失败:userId或Token为空");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: userId and token are required");
return false;
}
// 3. 校验用户ID格式
Long userId;
try {
userId = Long.parseLong(userIdStr);
} catch (NumberFormatException e) {
log.warn("SSE连接认证失败:userId格式错误");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Bad Request: invalid userId format");
return false;
}
// 4. 企业级Token校验(此处简化为模拟校验,实际需调用用户中心)
if (!validateToken(token, userId)) {
log.warn("SSE连接认证失败:用户{} Token无效", userId);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Unauthorized: invalid token");
return false;
}
// 5. 认证通过,将userId存入请求属性(后续控制器使用)
request.setAttribute("userId", userId);
log.debug("用户{} SSE连接认证通过", userId);
return true;
}
/**
* 模拟Token校验(实际企业级:调用用户服务/Redis校验Token有效性)
*/
private boolean validateToken(String token, Long userId) {
// 示例逻辑:Token格式为 "USER_${userId}_TOKEN" 则视为有效
return token.equals("USER_" + userId + "_TOKEN");
}
}
5.3 SSE 核心控制器(SseNotifyController.java)
核心职责:处理 SSE 连接建立、关闭、状态查询
package com.example.notify.controller;
import com.example.notify.dto.SsePushResponse;
import com.example.notify.sse.SseConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 企业级 SSE 通知控制器(连接建立+状态查询)
*/
@RestController
@RequestMapping("/sse")
@RequiredArgsConstructor
@Slf4j
public class SseNotifyController implements WebMvcConfigurer {
private final SseConnectionManager sseConnectionManager;
private final SseAuthInterceptor sseAuthInterceptor;
@Value("${sse.connect-timeout}")
private long connectTimeout; // SSE连接超时时间(ms)
// 注册SSE认证拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sseAuthInterceptor)
.addPathPatterns("/sse/connect"); // 仅拦截SSE连接接口
}
/**
* 建立SSE连接(前端核心接口)
* 访问地址:http://localhost:8080/sse/connect?userId=1001
* 请求头:Authorization: USER_1001_TOKEN(模拟Token)
*/
@GetMapping("/connect")
public SseEmitter connect(HttpServletRequest request) {
// 1. 获取认证通过的用户ID(从拦截器存入的请求属性中获取)
Long userId = (Long) request.getAttribute("userId");
if (Objects.isNull(userId)) {
throw new IllegalArgumentException("用户ID为空");
}
// 2. 创建SSEEmitter(设置超时时间)
SseEmitter emitter = new SseEmitter(connectTimeout);
// 3. 注册连接到管理器
boolean addSuccess = sseConnectionManager.addConnection(userId, emitter);
if (!addSuccess) {
emitter.completeWithError(new RuntimeException("连接数超限"));
return emitter;
}
// 4. 发送连接成功的欢迎消息(前端感知连接状态)
try {
SsePushResponse<String> welcomeMsg = SsePushResponse.success("SSE连接成功,等待订单状态通知...");
emitter.send(SseEmitter.event()
.name("CONNECT_SUCCESS") // 事件名称(前端可监听特定事件)
.data(welcomeMsg));
log.debug("用户{} SSE连接建立成功", userId);
} catch (Exception e) {
log.error("用户{} SSE连接初始化失败", userId, e);
sseConnectionManager.removeConnection(userId, emitter);
emitter.completeWithError(e);
}
return emitter;
}
/**
* 获取SSE连接统计(运维监控接口)
*/
@GetMapping("/stats")
public Map<String, Integer> getConnectionStats() {
return sseConnectionManager.getConnectionStats();
}
/**
* 手动关闭SSE连接(前端主动断开时调用)
*/
@PostMapping("/disconnect/{userId}")
public String disconnect(@PathVariable Long userId) {
List<SseEmitter> emitters = sseConnectionManager.getEmittersByUserId(userId);
emitters.forEach(SseEmitter::complete);
log.debug("用户{}主动关闭所有SSE连接", userId);
return "disconnect success";
}
}
6. 消息消费与 SSE 推送核心
6.1 Stream 消息通道配置(StreamConfig.java)
package com.example.notify.config;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
/**
* Spring Cloud Stream 配置(绑定消息输入通道)
*/
@EnableBinding(Sink.class) // Sink:内置输入通道(接收MQ消息)
public class StreamConfig {
// 无需额外代码,通过配置文件绑定RabbitMQ
}
6.2 订单消息消费者(OrderStatusConsumer.java)
核心职责:接收 RabbitMQ 消息,通过 SSE 推送给目标用户(业务核心)
package com.example.notify.consumer;
import com.alibaba.fastjson2.JSON;
import com.example.notify.dto.OrderStatusMessage;
import com.example.notify.dto.SsePushResponse;
import com.example.notify.sse.SseConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.validation.Valid;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 订单状态消息消费者(接收MQ消息→SSE推送)
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderStatusConsumer {
private final SseConnectionManager sseConnectionManager;
private final StringRedisTemplate redisTemplate;
@Value("${sse.cache-un_sent-msgs}")
private boolean cacheUnSentMsgs; // 是否缓存未发送消息
@Value("${sse.un_sent-msgs-expire}")
private long unSentMsgsExpire; // 未发送消息过期时间(秒)
// 幂等性Key前缀:order:push:${orderId}:${statusCode}
private static final String PUSHED_KEY_PREFIX = "order:push:";
// 未发送消息Key前缀:order:unsent:${userId}
private static final String UNSENT_KEY_PREFIX = "order:unsent:";
/**
* 监听RabbitMQ消息(Stream输入通道)
*/
@StreamListener(Sink.INPUT) // 对应配置文件中的 orderStatusInput 绑定
public void handleOrderStatusChange(@Payload @Valid Message<String> message) {
try {
// 1. 解析消息体
String payload = message.getPayload();
log.debug("收到订单状态变更消息:{}", payload);
OrderStatusMessage orderMsg = JSON.parseObject(payload, OrderStatusMessage.class);
// 2. 基础参数校验
Assert.notNull(orderMsg.getUserId(), "userId不能为空");
Assert.notNull(orderMsg.getOrderId(), "orderId不能为空");
Assert.notNull(orderMsg.getStatusCode(), "statusCode不能为空");
// 3. 幂等性校验(避免重复推送)
if (!checkIdempotency(orderMsg)) {
log.warn("订单{}状态{}已推送过,跳过重复推送", orderMsg.getOrderId(), orderMsg.getStatusCode());
return;
}
// 4. 获取用户的SSE连接
Long userId = orderMsg.getUserId();
List<SseEmitter> emitters = sseConnectionManager.getEmittersByUserId(userId);
if (emitters.isEmpty()) {
log.warn("用户{}无有效SSE连接,是否缓存未发送消息:{}", userId, cacheUnSentMsgs);
if (cacheUnSentMsgs) {
cacheUnSentMessage(userId, orderMsg); // 缓存消息(用户重连后补发)
}
return;
}
// 5. 构建SSE推送消息
SsePushResponse<OrderStatusMessage> pushMsg = SsePushResponse.success(orderMsg);
// 6. 推送消息到所有有效连接(多端登录支持)
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event()
.name("ORDER_STATUS_CHANGE") // 事件名称(前端监听)
.data(pushMsg));
log.debug("用户{}的SSE连接推送成功:订单{}→{}",
userId, orderMsg.getOrderId(), orderMsg.getStatusDesc());
} catch (Exception e) {
log.error("用户{}的SSE连接推送失败", userId, e);
sseConnectionManager.removeConnection(userId, emitter); // 移除失效连接
if (cacheUnSentMsgs) {
cacheUnSentMessage(userId, orderMsg); // 缓存未发送成功的消息
}
}
}
// 7. 标记为已推送(幂等性记录)
markAsPushed(orderMsg);
} catch (Exception e) {
log.error("处理订单状态消息失败", e);
// 企业级:可触发告警(如钉钉/短信通知开发人员)
throw new RuntimeException("订单状态消息处理失败", e);
}
}
/**
* 幂等性校验:检查该订单状态是否已推送过
*/
private boolean checkIdempotency(OrderStatusMessage msg) {
String key = PUSHED_KEY_PREFIX + msg.getOrderId() + ":" + msg.getStatusCode();
Boolean exists = redisTemplate.hasKey(key);
return Boolean.FALSE.equals(exists);
}
/**
* 标记为已推送(设置过期时间:24小时)
*/
private void markAsPushed(OrderStatusMessage msg) {
String key = PUSHED_KEY_PREFIX + msg.getOrderId() + ":" + msg.getStatusCode();
redisTemplate.opsForValue().set(key, "1", 24, TimeUnit.HOURS);
}
/**
* 缓存未发送成功的消息(用户重连后补发)
*/
private void cacheUnSentMessage(Long userId, OrderStatusMessage msg) {
String key = UNSENT_KEY_PREFIX + userId;
String msgJson = JSON.toJSONString(msg);
// 存入Redis列表(左侧插入)
redisTemplate.opsForList().leftPush(key, msgJson);
// 设置过期时间
redisTemplate.expire(key, unSentMsgsExpire, TimeUnit.SECONDS);
log.debug("缓存用户{}未发送消息:{}", userId, msgJson);
}
/**
* 补发未发送的消息(用户重连后调用)
*/
public void补发UnSentMessages(Long userId) {
String key = UNSENT_KEY_PREFIX + userId;
List<String> unSentMsgJsons = redisTemplate.opsForList().range(key, 0, -1);
if (Objects.isNull(unSentMsgJsons) || unSentMsgJsons.isEmpty()) {
return;
}
List<SseEmitter> emitters = sseConnectionManager.getEmittersByUserId(userId);
if (emitters.isEmpty()) {
return;
}
// 补发消息
for (String msgJson : unSentMsgJsons) {
try {
OrderStatusMessage msg = JSON.parseObject(msgJson, OrderStatusMessage.class);
// 再次幂等校验(避免补发时重复)
if (checkIdempotency(msg)) {
SsePushResponse<OrderStatusMessage> pushMsg = SsePushResponse.success(msg);
for (SseEmitter emitter : emitters) {
emitter.send(SseEmitter.event()
.name("ORDER_STATUS_CHANGE")
.data(pushMsg));
}
markAsPushed(msg);
log.debug("用户{}补发未发送消息:订单{}→{}", userId, msg.getOrderId(), msg.getStatusDesc());
}
} catch (Exception e) {
log.error("用户{}补发消息失败:{}", userId, msgJson, e);
}
}
// 补发完成后删除缓存
redisTemplate.delete(key);
}
}
7. 企业级辅助组件
7.1 定时任务(清理无效连接+补发消息)
package com.example.notify.task;
import com.example.notify.sse.SseConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 企业级定时任务(连接清理+监控)
*/
@Component
@EnableScheduling
@RequiredArgsConstructor
@Slf4j
public class SseScheduledTask {
private final SseConnectionManager sseConnectionManager;
/**
* 每30秒清理一次无效SSE连接(防止内存泄漏)
*/
@Scheduled(fixedRate = 30000)
public void cleanInvalidConnections() {
sseConnectionManager.cleanInvalidConnections();
}
/**
* 每5分钟打印一次连接统计(运维监控)
*/
@Scheduled(fixedRate = 300000)
public void printConnectionStats() {
log.info("SSE连接统计:{}", sseConnectionManager.getConnectionStats());
}
}
7.2 前端 SSE 连接示例(index.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>订单状态实时通知(SSE)</title>
<style>
.container { width: 800px; margin: 20px auto; }
.msg-list { margin-top: 20px; border: 1px solid #eee; padding: 10px; height: 400px; overflow-y: auto; }
.msg-item { margin: 10px 0; padding: 8px; border-radius: 4px; }
.success { background-color: #e6f7ef; color: #00865e; }
.info { background-color: #e8f4ff; color: #0066cc; }
.warn { background-color: #fffbe6; color: #744210; }
</style>
</head>
<body>
<div class="container">
<h1>订单状态实时通知</h1>
<div>用户ID:<span id="userId">1001</span></div>
<div>连接状态:<span id="connStatus" style="color: red;">未连接</span></div>
<button onclick="connectSse()">建立连接</button>
<button onclick="disconnectSse()">断开连接</button>
<div class="msg-list" id="msgList">
<!-- 消息列表 -->
</div>
</div>
<script>
let sse = null;
const userId = 1001;
const sseUrl = `http://localhost:8080/sse/connect?userId=${userId}`;
const retryInterval = 3000; // 重连间隔(与服务端配置一致)
// 建立SSE连接
function connectSse() {
// 关闭已有连接
if (sse) {
sse.close();
}
// 创建SSE连接(设置请求头Token)
sse = new EventSource(`${sseUrl}`, {
headers: {
'Authorization': `USER_${userId}_TOKEN` // 模拟Token
}
});
// 连接成功回调
sse.onopen = function() {
document.getElementById('connStatus').style.color = 'green';
document.getElementById('connStatus').textContent = '已连接';
addMsg('info', `SSE连接建立成功,正在监听订单状态...`);
};
// 接收消息回调(监听特定事件名称)
sse.addEventListener('ORDER_STATUS_CHANGE', function(event) {
const data = JSON.parse(event.data);
if (data.code === 200) {
const orderMsg = data.data;
addMsg('success',
`[${data.pushTime}] 订单${orderMsg.orderId}状态变更:${orderMsg.statusDesc}
(备注:${orderMsg.remark || '无'})`);
}
});
// 连接成功欢迎消息
sse.addEventListener('CONNECT_SUCCESS', function(event) {
const data = JSON.parse(event.data);
addMsg('info', `[系统通知] ${data.data}`);
});
// 连接错误回调(自动重连)
sse.onerror = function(error) {
document.getElementById('connStatus').style.color = 'red';
document.getElementById('connStatus').textContent = '连接异常';
addMsg('warn', `SSE连接异常:${error.message},${retryInterval/1000}秒后自动重连...`);
// 关闭当前连接
sse.close();
// 自动重连
setTimeout(connectSse, retryInterval);
};
}
// 断开SSE连接
function disconnectSse() {
if (sse) {
sse.close();
document.getElementById('connStatus').style.color = 'red';
document.getElementById('connStatus').textContent = '已断开';
addMsg('warn', '手动断开SSE连接');
sse = null;
}
}
// 添加消息到页面
function addMsg(type, content) {
const msgList = document.getElementById('msgList');
const msgItem = document.createElement('div');
msgItem.className = `msg-item ${type}`;
msgItem.innerHTML = content;
msgList.appendChild(msgItem);
// 滚动到底部
msgList.scrollTop = msgList.scrollHeight;
}
// 页面加载时自动建立连接
window.onload = connectSse;
</script>
</body>
</html>
7.3 订单服务消息发送示例(测试用)
package com.example.notify.test;
import com.alibaba.fastjson2.JSON;
import com.example.notify.dto.OrderStatusMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 测试:模拟订单服务发送状态变更消息(实际企业级场景:订单服务独立部署)
*/
@Component
public class OrderMessageProducerTest implements CommandLineRunner {
@Autowired
private RabbitTemplate rabbitTemplate;
// 交换机名称(与通知服务配置一致)
private static final String EXCHANGE_NAME = "order.status.exchange";
// 路由键(与通知服务路由匹配规则一致)
private static final String ROUTING_KEY = "order.status.paid";
@Override
public void run(String... args) throws Exception {
// 模拟订单支付成功消息
OrderStatusMessage msg = OrderStatusMessage.build(
"ORDER20250520001", // 订单ID
1001L, // 用户ID(与前端一致)
"PAID", // 状态编码
"支付宝支付成功,支付金额:99元" // 备注
);
// 发送消息到RabbitMQ
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY, JSON.toJSONString(msg));
System.out.println("订单服务发送消息:" + JSON.toJSONString(msg));
}
}
三、企业级业务流程剖析
1. 完整业务流程图
graph TD
A[订单服务] -->|1.订单状态变更(支付/发货等)| B[构建OrderStatusMessage消息]
B -->|2.发送到RabbitMQ交换机| C[RabbitMQ主题交换机order.status.exchange]
C -->|3.按路由键匹配(如order.status.paid)| D[消费队列queue-order.status.exchange-xxx]
D -->|4.通知服务消费消息| E[OrderStatusConsumer消费者]
E -->|5.幂等性校验(Redis判断是否已推送)| F{是否已推送?}
F -->|是| G[日志记录,跳过推送]
F -->|否| H[查询用户SSE连接(SseConnectionManager)]
H -->|6.无有效连接?| I{是否缓存未发送消息?}
I -->|是| J[Redis缓存消息(order:unsent:1001)]
I -->|否| K[结束]
H -->|有有效连接| L[构建SSE推送消息]
L -->|7.推送到用户所有端连接| M[前端接收消息]
M -->|8.展示订单状态变更| N[用户查看实时通知]
E -->|9.标记为已推送(Redis幂等记录)| O[结束]
P[用户重连SSE] -->|10.调用补发接口| Q[补发Redis缓存的未发送消息]
Q -->|11.推送缓存消息| M
2. 关键流程详解(企业级视角)
(1)消息生产阶段:订单状态变更触发
- 业务触发:订单服务在用户支付、商家发货等操作后,触发状态变更,构建标准化的
OrderStatusMessage消息(包含订单ID、用户ID、状态编码等核心字段)。 - 可靠性保障:订单服务使用RabbitMQ生产者确认机制(
publisher-confirm-type: correlated),确保消息成功投递到交换机;若失败则重试,避免消息丢失。
(2)消息传输阶段:RabbitMQ解耦与路由
- 解耦设计:订单服务无需关心通知服务是否在线,通过RabbitMQ异步通信,降低系统耦合度(企业级分布式系统核心设计原则)。
- 路由灵活:使用主题交换机,支持按状态类型路由(如
order.status.paid对应支付、order.status.shipped对应发货),后续扩展新状态无需修改核心代码。 - 容错机制:队列持久化+消息TTL+死信队列,确保消息不会因消费失败而丢失(消费重试3次失败后进入死信队列,方便人工排查)。
(3)消息消费与推送阶段:SSE精准推送
- 幂等性保障:通过Redis记录
order:push:${orderId}:${statusCode},避免同一订单同一状态重复推送(如RabbitMQ重试导致的重复消费)。 - 连接管理:
SseConnectionManager线程安全存储用户与SSE连接的映射,支持多端登录(一个用户多个浏览器/APP连接),定时清理无效连接防止内存泄漏。 - 断线重推:用户SSE连接断开时,未推送成功的消息缓存到Redis,用户重连后自动补发(提升用户体验,避免漏通知)。
(4)前端接收阶段:稳定可靠的用户体验
- 自动重连:SSE基于EventSource API,浏览器原生支持自动重连,配合服务端连接超时配置,确保网络波动后快速恢复连接。
- 事件监听:前端通过监听特定事件名称(如
ORDER_STATUS_CHANGE),区分不同类型的SSE消息,便于扩展(如后续新增物流轨迹通知)。 - 状态展示:接收消息后直接展示状态描述和备注,无需前端解析状态编码(消息体冗余
statusDesc字段,降低前端复杂度)。
(5)运维监控阶段:可观测性保障
- 连接统计:通过
/sse/stats接口查看当前连接用户数和总连接数,便于运维监控。 - 日志记录:关键节点(连接建立/关闭、消息推送成功/失败)均有日志,支持问题排查(如用户未收到通知时,可通过日志查询是否推送成功)。
- 异常告警:消费消息失败时可触发告警(如钉钉/短信),及时发现并处理问题(企业级运维核心需求)。
四、总结(企业级核心要点)
1. 选型核心结论
- 订单状态实时通知是「单向推送」场景,SSE 比 WebSocket 更适合:基于HTTP协议(兼容性好、运维成本低)、轻量(支持更高并发)、原生自动重连(用户体验优)。
- 企业级场景优先选择「RabbitMQ + SSE + Redis」组合:解耦、可靠、可扩展。
2. 企业级关键保障
- 可靠性:RabbitMQ消息持久化+消费重试+死信队列,Redis缓存未发送消息,确保消息不丢失。
- 幂等性:Redis记录已推送状态,避免重复通知。
- 连接管理:线程安全的SSE连接池、定时清理无效连接、支持多端登录。
- 安全性:SSE连接认证(Token校验),确保仅合法用户接收通知。
- 可扩展性:支持集群部署(Redis同步连接信息+消息),后续可扩展更多通知类型(如物流轨迹)。
3. 部署与扩展建议
- 单机部署:直接使用上述代码,启动RabbitMQ、Redis、通知服务即可运行。
- 集群部署:通知服务多实例部署,通过Redis共享SSE连接信息和未发送消息,确保任一实例接收MQ消息都能推送给用户。
- 性能优化:Redis使用集群模式,RabbitMQ配置镜像队列,SSE连接超时时间根据业务调整(建议5-10分钟)。
此方案完全满足企业级生产环境要求,代码可直接复制使用,仅需根据实际业务调整Token校验、用户服务集成等细节。