SSE消息推送服务技术方案

1. 技术概述

1.1 什么是SSE

SSEServer-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.连接断开               │
     │←───────────────────────────┘

连接状态:

  1. 连接建立:客户端发送请求,服务端返回text/event-stream
  2. 消息推送:服务端持续发送事件数据
  3. 连接断开:超时、网络故障、服务端关闭
  4. 自动重连:浏览器自动重连,发送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 关键要点

  1. SSE适用场景

    • 服务端单向推送
    • 消息通知、实时更新
    • 自动重连需求
  2. 核心实现

    • 使用SseEmitter建立连接
    • ConcurrentHashMap管理连接
    • 心跳检测保持连接活跃
  3. Nginx配置关键

    • proxy_buffering off; 禁用缓冲
    • proxy_http_version 1.1; 设置HTTP版本
    • proxy_read_timeout 超时时间大于后端
  4. 异常处理

    • safeComplete() 安全关闭
    • 避免误删新连接
    • 捕获AsyncRequestNotUsableException

7.2 项目经验

  1. 连接管理

    • 用户重连时先关闭旧连接
    • 使用final引用避免闭包问题
    • 心跳检测间隔5分钟
  2. 性能优化

    • 使用CopyOnWriteArraySet快照遍历
    • 自定义线程池异步处理
    • 限制单IP连接数
  3. 生产实践

    • 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 -> {});

posted @ 2026-03-15 12:18  flycloudy  阅读(2)  评论(0)    收藏  举报