sseemitter需设超时、注册回调清理、禁用阻塞操作;必须用produces指定text/event-stream;防重复连接需concurrenthashmap或redis去重;纯sse推送优先选sseemitter而非responsebodyemitter。

怎么用 SseEmitter 建立一次有效连接
Spring Boot 里最直接的 SSE 入口就是 SseEmitter,它不是即发即走的响应体,而是一个“可多次写入、需手动结束”的长生命周期对象。你调用 emitter.send(),客户端立刻收到一条消息;但如果你忘了调用 emitter.complete() 或 emitter.completeWithError(),连接就一直挂着,内存泄漏风险立马出现。
常见错误现象:接口压测时内存飙升、GC 频繁、emitterMap 越来越大却没人清理。
- 必须为每个连接设置超时时间,例如
new SseEmitter(300000L)(5 分钟),避免客户端断开后服务端还傻等 - 务必注册
onTimeout、onCompletion、onError回调,并在回调里从缓存(如ConcurrentHashMap)中移除该emitter - 不要在 Controller 方法里直接 new Thread 并 sleep —— 这会阻塞 Tomcat 线程池;改用异步线程或
@Async(注意配置线程池)
为什么不能只靠 ResponseBodyEmitter 就完事
ResponseBodyEmitter 是更底层的抽象,而 SseEmitter 是它的子类,专为 text/event-stream 格式封装好了事件头(data:、id:、event:)、换行符和编码处理。如果你硬用 ResponseBodyEmitter,就得自己拼 "data: hello\n\n",稍有不慎就触发浏览器解析失败——表现为 EventSource 反复重连但收不到任何 message 事件。
使用场景:只有当你需要混推非标准格式(比如自定义二进制流)时才考虑 ResponseBodyEmitter;纯 SSE 推送,请无条件选 SseEmitter。
-
SseEmitter.send(SseEventBuilder)自动加data:前缀和双换行,安全 - 若手动 send 字符串,必须确保末尾是
\n\n,否则 Chrome/Firefox 会卡住不触发onmessage - 别忽略
produces = MediaType.TEXT_EVENT_STREAM_VALUE,缺了这个,Nginx 或网关可能截断流
怎么防止同一用户重复建立连接
用户刷新页面、网络抖动、移动端切后台再切回,都可能导致多个 EventSource 实例同时连向后端。如果服务端不做去重,一个 userId 就对应 N 个 SseEmitter,推送一次消息就发 N 遍,CPU 和带宽白耗。
关键不是“禁止重复创建”,而是“识别已有连接并复用或拒绝”。实际部署中还要考虑集群问题——单机 Map 拦不住其他节点上的连接。
- 本地去重:用
ConcurrentHashMap<string sseemitter></string>缓存userId → emitter,每次请求先get(),命中则直接返回已存在的emitter - 集群去重:必须引入外部协调,比如 Redis + SETNX 或分布式锁;或者干脆把连接状态下沉到 MQ,由统一调度中心分发消息
- 前端配合:在
EventSource实例销毁前调用.close(),并在重连时带上唯一lastEventId,便于服务端做幂等判断
WebFlux 的 Flux<serversentevent></serversentevent> 和传统 SseEmitter 怎么选
如果你的业务本身就是响应式栈(比如数据来自 Reactor 流、R2DBC、或要对接 Kafka Flux),那直接上 Flux<serversentevent>></serversentevent> 更自然,没有线程切换开销,也天然支持背压。但如果你只是定时查 DB、发几条日志、推个告警,硬套 WebFlux 反而绕远路——得学 Flux.generate、delayElements、doOnNext 一堆操作符,还容易写出阻塞代码(比如在 map 里调 JDBC)。
性能影响很实在:WebFlux 版本在万级并发下资源占用更低;但开发成本高,调试链路长,出错堆栈难读。
- 新项目 + 数据源已是响应式(如 MongoDB Reactive、PostgreSQL R2DBC)→ 优先
Flux - 老系统改造、简单轮询、DB 查询为主 → 用
SseEmitter更稳,别为了“响应式”而响应式 - 别在
Flux的map或flatMap里写Thread.sleep()或同步 IO,这会让整个 event loop 卡住
真正麻烦的从来不是怎么发第一条消息,而是怎么保证第 1000 条不丢、第 10000 个连接不垮、凌晨三点报警时你能快速定位是前端没监听 error,还是 Redis 锁失效导致重复推送。










