SSE消息推送服务技术方案
1. 技术概述
1.1 什么是SSE
SSE(Server-Sent Events)是一种基于HTTP的单向服务器推送技术,允许服务器向客户端持续推送事件流。SSE是W3C标准规范,基于HTTP/1.1协议,浏览器原生支持,无需额外库。
核心特点:
- 单向通信:服务器 → 客户端
- 文本格式:使用UTF-8编码的文本流
- 自动重连:浏览器断线自动重连,支持断点续传
- 事件流格式:标准化的消息格式
1.2 SSE vs WebSocket vs 轮询
| 对比维度 | SSE | WebSocket | 轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务端→客户端) | 双向 | 单向(客户端请求) |
| 协议 | HTTP/1.1 | HTTP/1.1 + WebSocket协议 | HTTP |
| 浏览器支持 | 现代浏览器原生支持 | 现代浏览器支持 | 全部支持 |
| 实现复杂度 | 简单 | 中等 | 简单 |
| 连接数 | 每个域名限制6个(HTTP/1.1) | 无限制 | 受限于并发数 |
| 自动重连 | ✅ 原生支持 | ❌ 需手动实现 | ❌ 每次重新请求 |
| 数据格式 | 文本(UTF-8) | 文本/二进制 | JSON/文本 |
| 适用场景 | 消息通知、实时更新 | 聊天、游戏 | 低频数据获取 |
选型建议:
- SSE:服务端推送、消息通知、进度更新
- WebSocket:双向通信、即时聊天、在线游戏
- 轮询:低频数据获取、兼容性要求高
1.3 核心概念
事件流(Event Stream):
data: 第一条消息
event: message
id: 1
retry: 3000
data: 第二条消息
event: notification
id: 2
data: {"count": 5}
event: unread
id:3
SSE响应头:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
2. SSE核心概念
2.1 事件格式
SSE事件由多个字段组成,每个字段占一行:
| 字段 | 说明 | 示例 |
|---|---|---|
data |
事件数据,可多行(每行以data:开头) |
data: {"count": 5} |
event |
事件类型,客户端可监听特定事件 | event: message |
id |
事件ID,用于断线重连恢复 | id: 12345 |
retry |
重连间隔(毫秒) | retry: 3000 |
: |
注释,用于保持连接(心跳) | : keep-alive |
完整事件示例:
event: message
id: 1625097600000
data: {"unreadCount": 5, "type": "unread", "timestamp": 1625097600000}
event: heartbeat
data: 1625097601000
2.2 连接生命周期
┌─────────┐ 1.建立连接 ┌──────────┐
│ 客户端 │ ───────────────→ │ 服务端 │
└─────────┘ └──────────┘
↑ │
│ │
│ 4.自动重连 │ 2.持续推送
│←───────────────────────────┤
│ │
│ │
│ 3.连接断开 │
│←───────────────────────────┘
连接状态:
- 连接建立:客户端发送请求,服务端返回
text/event-stream - 消息推送:服务端持续发送事件数据
- 连接断开:超时、网络故障、服务端关闭
- 自动重连:浏览器自动重连,发送
Last-Event-ID恢复
2.3 重连机制
浏览器自动重连:
// 前端代码示例
const eventSource = new EventSource('/messages/stream');
// 监听连接打开
eventSource.onopen = (event) => {
console.log('SSE连接已建立');
};
// 监听错误(自动重连)
eventSource.onerror = (event) => {
console.log('连接断开,正在重连...');
};
断点续传:
- 服务端使用
id字段标识事件 - 重连时浏览器发送
Last-Event-ID请求头 - 服务端根据ID发送后续事件
2.4 事件类型
本项目中定义的事件类型:
/**
* SSE事件类型枚举
*/
public enum SseEventEnum {
/**
* 未读消息数量更新事件
*/
MESSAGE("message"),
/**
* 心跳检测事件
*/
HEARTBEAT("heartbeat");
private final String eventName;
}
3. 使用场景
3.1 消息推送
场景描述:用户接收实时消息通知,如未读消息数量更新。
实现方案:
- 客户端建立SSE连接
- 服务端检测到新消息时推送更新
- 前端实时更新UI显示
3.2 实时通知
场景描述:系统公告、预警通知、审批通知等。
优势:
- 无需客户端轮询,节省资源
- 服务端主动推送,实时性强
- 自动重连,可靠性高
3.3 进度更新
场景描述:长时间任务的进度实时反馈,如文件上传、数据导出。
实现方式:
event: progress
data: {"percent": 10, "status": "processing"}
event: progress
data: {"percent": 50, "status": "processing"}
event: progress
data: {"percent": 100, "status": "completed"}
4. 项目实战
4.1 架构设计
┌──────────────────────────────────────────────────────────┐
│ 前端层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ EventSource → SSE连接 → 监听事件 → 更新UI │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↓ HTTP
┌──────────────────────────────────────────────────────────┐
│ 控制器层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MessageController.streamUnreadCount() │ │
│ │ - 创建SseEmitter │ │
│ │ - 立即推送当前未读数 │ │
│ │ - 返回emitter保持连接 │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 连接管理层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SseConnectionManager │ │
│ │ - ConcurrentHashMap存储连接 │ │
│ │ - 创建/移除/发送消息 │ │
│ │ - 心跳检测与清理 │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────────┐
│ 服务层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MessageServiceImpl.pushUnreadCount() │ │
│ │ - 查询未读消息数量 │ │
│ │ - 通过SSE推送给用户 │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
4.2 核心组件
组件1:SSE连接管理器
职责:管理所有用户的SSE连接,提供连接的创建、移除、消息发送功能。
@Slf4j
@Component
public class SseConnectionManager {
/**
* 用户唯一标识到SseEmitter的映射
* 使用ConcurrentHashMap保证线程安全
*/
private final Map<String, SseEmitter> userEmitters = new ConcurrentHashMap<>();
/**
* SSE超时时间(毫秒),30分钟
*/
private static final long SSE_TIMEOUT = 30 * 60 * 1000L;
/**
* 创建SSE连接
* @param userUniqueCode 用户唯一标识
* @return SseEmitter实例
*/
public SseEmitter createConnection(String userUniqueCode) {
// 如果该用户已有连接,先关闭旧连接
removeConnection(userUniqueCode);
// 创建新的SseEmitter,设置超时时间
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
// 捕获当前emitter的引用,避免闭包捕获过期的userUniqueCode
final SseEmitter currentEmitter = emitter;
// 设置连接关闭、超时、错误的回调
emitter.onCompletion(() -> {
// 只有当map中的emitter还是当前这个时才移除,避免误删新连接
if (userEmitters.get(userUniqueCode) == currentEmitter) {
log.debug("SSE连接关闭,用户: {}", userUniqueCode);
userEmitters.remove(userUniqueCode);
}
});
emitter.onTimeout(() -> {
log.debug("SSE连接超时,用户: {}", userUniqueCode);
removeConnection(userUniqueCode);
});
emitter.onError(throwable -> {
log.debug("SSE连接异常,用户: {}, 错误类型: {}",
userUniqueCode, throwable.getClass().getSimpleName());
removeConnection(userUniqueCode);
});
// 存储连接
userEmitters.put(userUniqueCode, emitter);
log.info("创建SSE连接成功,用户: {}, 当前连接数: {}",
userUniqueCode, userEmitters.size());
return emitter;
}
}
组件2:SSE事件类型枚举
职责:定义所有SSE推送的事件类型,便于集中管理和统一维护。
@Getter
@AllArgsConstructor
public enum SseEventEnum {
/**
* 未读消息数量更新事件
*/
MESSAGE("message"),
/**
* 心跳检测事件
*/
HEARTBEAT("heartbeat");
private final String eventName;
}
组件3:未读消息DTO
职责:封装SSE推送的数据结构。
@Data
@Builder
public class UnreadMessageDTO {
/**
* 未读消息数量
*/
private Long unreadCount;
/**
* 消息类型(message: 未读消息数量)
*/
private String type;
/**
* 时间戳
*/
private Long timestamp;
public static UnreadMessageDTO of(Long unreadCount) {
return UnreadMessageDTO.builder()
.unreadCount(unreadCount)
.type("unread")
.timestamp(System.currentTimeMillis())
.build();
}
}
4.3 代码实现
控制器层:SSE连接端点
@RestController
@RequestMapping("/messages")
public class MessageController {
private final MessageService messageService;
private final SseConnectionManager sseConnectionManager;
/**
* 建立SSE连接
* 客户端通过此接口建立SSE连接,实时接收未读消息数量更新
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamUnreadCount() {
// 获取当前用户的唯一标识
String userUniqueCode = UserContextHolder.getUserUniqueCode();
log.info("用户建立SSE连接: {}", userUniqueCode);
// 创建SSE连接
SseEmitter emitter = sseConnectionManager.createConnection(userUniqueCode);
try {
// 立即推送当前未读消息数量
Long unreadCount = messageService.getUnreadCount(userUniqueCode);
UnreadMessageDTO unreadMessageDTO = UnreadMessageDTO.of(unreadCount);
emitter.send(SseEmitter.event()
.name(SseEventEnum.MESSAGE.getEventName())
.data(unreadMessageDTO));
} catch (Exception e) {
log.error("初始未读消息数量推送失败,用户: {}", userUniqueCode, e);
emitter.completeWithError(e);
}
return emitter;
}
}
服务层:消息推送
@Service
public class MessageServiceImpl implements MessageService {
private final SseConnectionManager sseConnectionManager;
/**
* 推送未读消息数量
* @param userUniqueCode 用户唯一标识
*/
@Override
public void pushUnreadCount(String userUniqueCode) {
if (userUniqueCode == null || userUniqueCode.trim().isEmpty()) {
return;
}
try {
// 查询未读消息数量
Long unreadCount = getUnreadCount(userUniqueCode);
// 创建响应对象
UnreadMessageDTO unreadMessageDTO = UnreadMessageDTO.of(unreadCount);
// 通过SSE推送给当前连接的用户
sseConnectionManager.sendToUser(userUniqueCode, unreadMessageDTO);
log.debug("推送未读消息数量成功,用户: {}, 未读数: {}",
userUniqueCode, unreadCount);
} catch (Exception e) {
log.error("推送未读消息数量失败,用户: {}", userUniqueCode, e);
}
}
/**
* 查询消息详情时,自动标记为已读并推送更新
*/
@Override
public Message getByIdWithContent(Long id) {
Message message = getById(id);
if (message == null) {
return null;
}
// 若为未读消息,则标记为已读
if (!message.getReadStatus()) {
lambdaUpdate().eq(Message::getId, message.getId())
.set(Message::getReadStatus, Boolean.TRUE)
.update();
// 推送用户消息通知
pushUnreadCount(message.getBelongUserUniqueCode());
}
return message;
}
}
连接管理:发送消息与心跳检测
@Slf4j
@Component
public class SseConnectionManager {
private final Map<String, SseEmitter> userEmitters = new ConcurrentHashMap<>();
/**
* 向指定用户发送消息
*/
public boolean sendToUser(String userUniqueCode, Object data) {
SseEmitter emitter = userEmitters.get(userUniqueCode);
if (emitter == null) {
log.debug("用户无SSE连接,跳过推送,用户: {}", userUniqueCode);
return false;
}
try {
emitter.send(SseEmitter.event()
.name(SseEventEnum.MESSAGE.getEventName())
.data(data));
log.debug("向用户推送消息成功,用户: {}", userUniqueCode);
return true;
} catch (Exception e) {
log.debug("向用户推送消息失败,用户: {}, 错误类型: {}",
userUniqueCode, e.getClass().getSimpleName());
removeConnection(userUniqueCode);
return false;
}
}
/**
* 定时清理过期连接(每次执行完毕后间隔5分钟再次执行)
*/
@Scheduled(fixedDelay = 5 * 60 * 1000)
public void cleanupInactiveConnections() {
int beforeCount = userEmitters.size();
// 使用快照遍历,避免在遍历过程中修改map
CopyOnWriteArraySet<String> userCodes =
new CopyOnWriteArraySet<>(userEmitters.keySet());
// 异步执行心跳检测,避免慢连接阻塞整个清理过程
for (String userCode : userCodes) {
final String currentUserCode = userCode;
asyncExecutor.execute(() -> {
SseEmitter emitter = userEmitters.get(currentUserCode);
if (emitter == null) {
return;
}
try {
// 尝试发送心跳检测
emitter.send(SseEmitter.event()
.name(SseEventEnum.HEARTBEAT.getEventName())
.data(System.currentTimeMillis()));
} catch (Exception e) {
log.debug("清理不活跃连接,用户: {}, 错误类型: {}",
currentUserCode, e.getClass().getSimpleName());
removeConnection(currentUserCode);
}
});
}
int afterCount = userEmitters.size();
if (beforeCount != afterCount) {
log.info("定时清理完成,清理前: {},清理后: {}",
beforeCount, afterCount);
}
}
}
4.4 前端对接
建立SSE连接
// 建立SSE连接
const eventSource = new EventSource('/messages/stream');
// 监听消息事件
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('收到消息推送:', data);
// 更新UI显示未读数量
updateUnreadCount(data.unreadCount);
});
// 监听连接打开
eventSource.onopen = () => {
console.log('SSE连接已建立');
};
// 监听错误(自动重连)
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
};
// 手动关闭连接(如用户退出登录)
function closeSSEConnection() {
eventSource.close();
console.log('SSE连接已关闭');
}
React Hook封装
import { useEffect, useState } from 'react';
function useUnreadMessage() {
const [unreadCount, setUnreadCount] = useState(0);
useEffect(() => {
const eventSource = new EventSource('/messages/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
setUnreadCount(data.unreadCount);
});
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
};
// 组件卸载时关闭连接
return () => {
eventSource.close();
};
}, []);
return unreadCount;
}
// 使用示例
function MessageNotification() {
const unreadCount = useUnreadMessage();
return (
<div>
<span>未读消息: {unreadCount}</span>
</div>
);
}
Vue3 Composition API封装
import { ref, onUnmounted } from 'vue';
export function useUnreadMessage() {
const unreadCount = ref(0);
let eventSource = null;
const connect = () => {
eventSource = new EventSource('/messages/stream');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
unreadCount.value = data.unreadCount;
});
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
};
};
const disconnect = () => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
// 组件挂载时建立连接
connect();
// 组件卸载时关闭连接
onUnmounted(() => {
disconnect();
});
return { unreadCount, connect, disconnect };
}
5. Nginx配置方案
5.1 SSE与Nginx
SSE基于HTTP长连接,Nginx默认配置会缓冲响应导致消息推送延迟。因此需要特殊配置禁用缓冲、设置超时,确保实时推送。
核心问题:
- Nginx默认开启代理缓冲,导致消息堆积后一次性推送
- 默认超时时间较短(60秒),长连接会被断开
- 负载均衡时后端切换导致连接断开
5.2 必要配置项
# SSE接口专用location配置
location /messages/stream {
# 🔴 必配项1:代理到后端服务
proxy_pass http://backend_server;
# 🔴 必配项2:禁用缓冲(核心配置)
# SSE是流式传输,必须禁用缓冲才能实时推送
proxy_buffering off;
# 🔴 必配项3:设置HTTP版本
# SSE需要HTTP/1.1,支持keep-alive长连接
proxy_http_version 1.1;
# 🔴 必配项4:清除Connection头
# 保持长连接,避免Nginx将Connection改为close
proxy_set_header Connection '';
# 🔴 必配项5:禁用缓存
# 确保每次都是实时数据,不使用缓存
proxy_cache off;
# 🔴 必配项6:超时设置
# 保持连接时间,建议大于后端SSE超时时间(本项目30分钟)
proxy_read_timeout 1900s; # 31分钟 > 30分钟
proxy_send_timeout 1900s;
proxy_connect_timeout 60s;
# 🔴 必配项7:传递SSE响应头
# 确保text/event-stream正确传递到客户端
proxy_pass_request_headers on;
}
5.3 可选优化配置
# 高并发场景优化
location /messages/stream {
# ... 必配项 ...
# ⚡ 可选优化1:限制请求速率
# 防止恶意用户频繁建立连接导致资源耗尽
limit_req_zone $request_uri zone=sse_limit:10m rate=10r/s;
limit_req zone=sse_limit burst=5 nodelay;
# ⚡ 可选优化2:连接数限制
# 限制单个IP的并发SSE连接数,防止滥用
limit_conn_zone $binary_remote_addr zone=sse_conn:10m;
limit_conn sse_conn 2;
# ⚡ 可选优化3:显式禁用Nginx加速缓冲
add_header X-Accel-Buffering no;
# ⚡ 可选优化4:单独配置日志
# SSE连接会产生大量日志,建议单独配置或关闭
access_log /var/log/nginx/sse_access.log combined buffer=32k flush=5s;
error_log /var/log/nginx/sse_error.log warn;
# ⚡ 可选优化5:错误处理与故障转移
# 后端不可用时的处理策略
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
proxy_next_upstream_tries 2;
# ⚡ 可选优化6:传递原始请求信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
5.4 常见问题与解决
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 消息推送延迟 | Nginx缓冲未禁用 | proxy_buffering off; |
| 连接频繁断开 | 超时时间过短 | 增大proxy_read_timeout |
| 前端无法建立连接 | HTTP版本错误 | 设置proxy_http_version 1.1; |
| 多实例负载均衡断连 | 后端实例切换导致 | 使用IP哈希或会话保持 |
| 高并发下性能差 | 无连接限制 | 添加limit_conn限制 |
| 心跳检测失败 | Nginx过滤心跳数据 | 确保注释格式正确(:开头) |
多实例负载均衡配置
# 方案1:使用IP哈希(推荐)
upstream backend_server {
ip_hash; # 基于客户端IP的哈希,确保同一用户连接同一后端
server 192.168.1.10:8080 max_fails=2 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=2 fail_timeout=30s;
server 192.168.1.12:8080 max_fails=2 fail_timeout=30s;
keepalive 32; # 保持连接池,减少握手开销
}
# 方案2:使用会话保持(需要nginx-plus或sticky模块)
upstream backend_server {
sticky cookie srv_id expires=1h domain=.example.com path=/;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
5.5 完整配置示例
# 定义限流zone
limit_req_zone $request_uri zone=sse_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=sse_conn:10m;
# 后端服务配置
upstream backend_server {
ip_hash; # IP哈希,确保同一用户连接同一后端
server 192.168.1.10:8080 max_fails=2 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=2 fail_timeout=30s;
server 192.168.1.12:8080 max_fails=2 fail_timeout=30s;
keepalive 32; # 保持连接池
}
server {
listen 80;
server_name example.com;
# SSE接口配置
location /messages/stream {
# === 必配项 ===
proxy_pass http://backend_server;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_cache off;
# 超时设置(大于后端30分钟)
proxy_read_timeout 1900s;
proxy_send_timeout 1900s;
proxy_connect_timeout 60s;
# === 优化配置 ===
# 禁用加速缓冲
add_header X-Accel-Buffering no;
# 传递原始请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 限流配置
limit_req zone=sse_limit burst=5 nodelay;
limit_conn sse_conn 2;
# 错误处理
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
# 日志配置(单独文件)
access_log /var/log/nginx/sse_access.log combined buffer=32k flush=5s;
error_log /var/log/nginx/sse_error.log warn;
# 禁用SOCKS代理
proxy_redirect off;
}
# 普通API接口(保持默认配置)
location /api/ {
proxy_pass http://backend_server;
# 普通接口可以开启缓冲提升性能
proxy_buffering on;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
}
5.6 Nginx配置检查清单
## Nginx SSE配置检查清单
### ✅ 必配项
- [ ] `proxy_buffering off;` - 禁用缓冲
- [ ] `proxy_http_version 1.1;` - 设置HTTP版本
- [ ] `proxy_set_header Connection '';` - 清除Connection头
- [ ] `proxy_read_timeout` - 设置合理的读超时(建议 > 后端超时)
- [ ] `proxy_cache off;` - 禁用缓存
### ✅ 推荐配置
- [ ] `add_header X-Accel-Buffering no;` - 显式禁用加速缓冲
- [ ] `ip_hash;` - 负载均衡使用IP哈希
- [ ] `limit_conn` - 限制单IP连接数
- [ ] 单独配置access_log - 避免日志膨胀
### ✅ 测试验证
- [ ] 直接访问后端,SSE正常
- [ ] 通过Nginx访问,SSE正常
- [ ] 长时间连接不断开
- [ ] 消息推送无延迟
- [ ] 后端重启后自动重连
6. 生产实践
6.1 异常处理
问题1:AsyncRequestNotUsableException
现象:关闭SSE连接时抛出AsyncRequestNotUsableException异常。
原因:响应已不可用时仍然尝试完成连接。
解决方案:
/**
* 安全地完成SSE连接
* 处理各种异常情况,避免抛出 AsyncRequestNotUsableException 等异常
*/
private void safeComplete(SseEmitter emitter, String userUniqueCode) {
try {
emitter.complete();
log.debug("SSE连接已安全关闭,用户: {}", userUniqueCode);
} catch (Exception e) {
// 捕获所有异常,避免传播到全局异常处理器
if (e.getClass().getSimpleName().contains("AsyncRequestNotUsable")) {
log.debug("SSE响应已不可用,跳过关闭,用户: {}", userUniqueCode);
} else if (e.getClass().getSimpleName().contains("IllegalStateException")) {
log.debug("SSE连接处于非法状态,跳过关闭,用户: {}", userUniqueCode);
} else {
log.debug("关闭SSE连接时发生异常,用户: {}, 异常类型: {}",
userUniqueCode, e.getClass().getSimpleName());
}
}
}
问题2:误删新连接
现象:用户重连时,旧连接的回调误删新连接。
解决方案:
public SseEmitter createConnection(String userUniqueCode) {
removeConnection(userUniqueCode);
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
// 捕获当前emitter的引用,避免闭包捕获过期的userUniqueCode
final SseEmitter currentEmitter = emitter;
emitter.onCompletion(() -> {
// 只有当map中的emitter还是当前这个时才移除,避免误删新连接
if (userEmitters.get(userUniqueCode) == currentEmitter) {
log.debug("SSE连接关闭,用户: {}", userUniqueCode);
userEmitters.remove(userUniqueCode);
}
});
userEmitters.put(userUniqueCode, emitter);
return emitter;
}
6.2 性能优化
优化1:并发安全
使用ConcurrentHashMap:
// 使用ConcurrentHashMap保证线程安全
private final Map<String, SseEmitter> userEmitters = new ConcurrentHashMap<>();
使用快照遍历:
// 使用快照遍历,避免在遍历过程中修改map
CopyOnWriteArraySet<String> userCodes = new CopyOnWriteArraySet<>(userEmitters.keySet());
for (String userCode : userCodes) {
// 处理连接
}
优化2:自定义线程池
/**
* 异步执行器,用于心跳检测等耗时操作
* 使用有界线程池,避免资源耗尽
*/
private final Executor asyncExecutor = new ThreadPoolExecutor(
10, // 核心线程数
50, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列容量
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "sse-cleanup-" + threadNumber.getAndIncrement());
thread.setDaemon(true);
return thread;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行
);
优化3:心跳检测
/**
* 定时清理过期连接(每次执行完毕后间隔5分钟再次执行)
* 使用 fixedDelay 避免任务重叠执行
*/
@Scheduled(fixedDelay = 5 * 60 * 1000)
public void cleanupInactiveConnections() {
// 使用快照遍历
CopyOnWriteArraySet<String> userCodes = new CopyOnWriteArraySet<>(userEmitters.keySet());
// 异步执行心跳检测,避免慢连接阻塞整个清理过程
for (String userCode : userCodes) {
final String currentUserCode = userCode;
asyncExecutor.execute(() -> {
SseEmitter emitter = userEmitters.get(currentUserCode);
if (emitter == null) {
return;
}
try {
// 尝试发送心跳检测
emitter.send(SseEmitter.event()
.name(SseEventEnum.HEARTBEAT.getEventName())
.data(System.currentTimeMillis()));
} catch (Exception e) {
// 心跳失败,移除连接
removeConnection(currentUserCode);
}
});
}
}
6.3 监控告警
监控指标
/**
* 获取当前连接数
*/
public int getConnectionCount() {
return userEmitters.size();
}
/**
* 检查用户是否在线
*/
public boolean isUserOnline(String userUniqueCode) {
return userEmitters.containsKey(userUniqueCode);
}
建议监控指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 当前连接数 | 实时SSE连接数 | > 10000 |
| 消息推送成功率 | 成功推送数/总推送数 | < 95% |
| 异常连接清理数 | 定时清理的连接数 | > 100/5分钟 |
| 心跳检测失败率 | 心跳失败数/总连接数 | > 10% |
日志记录
// 创建连接日志
log.info("创建SSE连接成功,用户: {}, 当前连接数: {}",
userUniqueCode, userEmitters.size());
// 推送消息日志
log.debug("向用户推送消息成功,用户: {}", userUniqueCode);
// 清理连接日志
log.info("定时清理完成,清理前: {},清理后: {}",
beforeCount, afterCount);
7. 总结
7.1 关键要点
-
SSE适用场景
- 服务端单向推送
- 消息通知、实时更新
- 自动重连需求
-
核心实现
- 使用
SseEmitter建立连接 ConcurrentHashMap管理连接- 心跳检测保持连接活跃
- 使用
-
Nginx配置关键
proxy_buffering off;禁用缓冲proxy_http_version 1.1;设置HTTP版本proxy_read_timeout超时时间大于后端
-
异常处理
safeComplete()安全关闭- 避免误删新连接
- 捕获
AsyncRequestNotUsableException
7.2 项目经验
-
连接管理
- 用户重连时先关闭旧连接
- 使用
final引用避免闭包问题 - 心跳检测间隔5分钟
-
性能优化
- 使用
CopyOnWriteArraySet快照遍历 - 自定义线程池异步处理
- 限制单IP连接数
- 使用
-
生产实践
- Nginx使用IP哈希负载均衡
- 单独配置SSE日志避免膨胀
- 监控连接数和推送成功率
7.3 技术选型建议
| 场景 | 推荐技术 | 原因 |
|---|---|---|
| 消息推送 | SSE | 简单、自动重连、浏览器原生支持 |
| 即时聊天 | WebSocket | 双向通信、低延迟 |
| 系统通知 | SSE | 服务端推送、断线重连 |
| 进度更新 | SSE | 流式传输、实时反馈 |
附录
A. SSE事件格式规范
data: 消息内容
event: 事件类型
id: 事件ID
retry: 重连间隔(毫秒)
data: {"key": "value"}
event: message
id: 1625097600000
: 心跳(注释)
data: 多行数据
data: 第二行
data: 第三行
B. 前端EventSource API
// 建立连接
const eventSource = new EventSource('/messages/stream');
// 监听所有消息
eventSource.onmessage = (event) => {
console.log(event.data);
};
// 监听特定事件
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log(data);
});
// 监听连接打开
eventSource.onopen = (event) => {
console.log('连接已建立');
};
// 监听错误
eventSource.onerror = (event) => {
console.error('连接错误');
};
// 关闭连接
eventSource.close();
// 获取状态
console.log(eventSource.readyState); // 0:连接中 1:已打开 2:已关闭
C. 后端SseEmitter API
// 创建SseEmitter
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30分钟超时
// 发送消息
emitter.send(SseEmitter.event()
.name("message") // 事件名称
.id("12345") // 事件ID
.retry(3000) // 重连间隔
.data(messageData) // 数据
.comment("心跳检测")); // 注释
// 完成连接
emitter.complete();
// 完成连接(带错误)
emitter.completeWithError(exception);
// 设置回调
emitter.onCompletion(() -> {});
emitter.onTimeout(() -> {});
emitter.onError(throwable -> {});

浙公网安备 33010602011771号