这类面试题最考验的不是背概念而是能不能把技术方案落地到真实生产环境。很多人知道 SSEServer-Sent Events适合做服务端推送但一到浏览器连接数限制、断线重连、消息去重这些实际坑点就卡住。今天直接拆一个高可用 SSE 系统的设计思路重点放在怎么绕过浏览器限制、怎么处理网络抖动和消息补发。1. 先搞清楚 SSE 到底适合什么场景别一上来就硬套SSE 本质上是一个长连接的 HTTP 流服务器通过text/event-stream格式持续推数据给浏览器。它和 WebSocket 最大的区别是SSE 是单向的服务端推WebSocket 是双向的可双向通信。如果你只需要实时收通知、日志流、状态更新SSE 通常更简单。但 SSE 有三个硬伤浏览器连接数限制同一个域名下Chrome 最多允许 6 个 HTTP/1.1 并发连接。如果页面里同时开了多个 SSE 连接很容易占满配额导致其他静态资源图片、CSS、JS卡住。断线重连是半自动的浏览器会在连接断开后自动重连但重连期间的消息会丢需要业务层自己补。消息顺序和去重网络抖动可能导致客户端收到重复消息或者消息顺序乱掉。面试官问“用 HTTP/2 怎么破”其实是在考你对 HTTP/2 多路复用的理解。HTTP/2 允许在同一个 TCP 连接上并行多个流这样 SSE 连接就不会占满浏览器连接数。但光知道这个不够还得说清楚怎么配、怎么测、有什么坑。2. 低配方案HTTP/1.1 下怎么缓解连接数瓶颈如果你还在用 HTTP/1.1又需要开多个 SSE 连接可以先用这几个临时方案顶一下2.1 域名分片Domain Sharding把 SSE 服务挂在一个单独的子域名下比如sse.example.com。这样浏览器会为这个新域名分配另外 6 个连接配额不会影响主站资源加载。但域名分片也有代价每个新域名都要做 DNS 解析、TCP 握手、TLS 握手如果用 HTTPS会增加延迟。而且如果页面里还有其他资源比如图片、API也做了分片整体连接数还是会爆。2.2 连接复用和队列管理更务实的做法是减少同时活跃的 SSE 连接数。比如非当前激活的页面或标签页主动关闭 SSE 连接监听visibilitychange事件。同一个用户的多条数据流合并成一个 SSE 连接在消息里用不同event字段区分业务类型。前端实现连接池排队等待可用连接。这些方法能缓解但治标不治本。真要在生产环境用 SSE迟早要上 HTTP/2。3. HTTP/2 多路复用到底怎么配怎么验证HTTP/2 的核心优势是“一个 TCP 连接多个流”。这意味着你开 10 个 SSE 连接浏览器也只占 1 个 TCP 连接不会爆连接数。3.1 服务端配置要点服务端要支持 HTTP/2通常需要Nginx 1.9.5 以上开启http2配置server { listen 443 ssl http2; server_name example.com; # ... SSL 配置 }Node.js 用spdy或http2模块。Spring Boot 2.0 内置支持 HTTP/2但要配 SSL。Go 1.6 的http.Server直接支持 HTTP/2。注意HTTP/2 必须跑在 HTTPS 上本地开发可以用http://localhost绕过但生产环境必须配证书。3.2 验证 HTTP/2 是否生效打开浏览器开发者工具到 Network 标签页看 Protocol 列应该显示h2HTTP/2或h3HTTP/3而不是http/1.1。同一个域名下的多个 SSE 连接应该共享同一个 TCP 连接。如果没生效先检查证书是不是有效、Nginx 配置有没有 reload、客户端是否支持 HTTP/2老旧浏览器或工具可能降级到 HTTP/1.1。3.3 注意反向代理的配置如果你的 SSE 服务前面有反向代理比如 Nginx要确保代理层也正确透传了 HTTP/2。有时候代理配了 HTTP/2但后端服务还是 HTTP/1.1这样浏览器到代理是 HTTP/2代理到后端是 HTTP/1.1连接数瓶颈会转移到代理与后端之间。4. 断线重连不能靠浏览器自动重试得自己管状态和补发浏览器会在 SSE 连接断开后自动重连但重连机制很基础默认重试间隔 3 秒服务器可以通过retry: 5000字段调整。重连时浏览器会带上次的Last-Event-ID头服务端要根据这个 ID 决定从哪开始补消息。问题在于网络抖动可能导致多次重连服务器可能重复补发消息。所以光靠浏览器机制不够要在应用层做更细粒度的控制。4.1 服务端记录客户端状态每个 SSE 连接建立时服务端应该生成一个唯一clientId通过第一个事件发给客户端。在服务端内存或 Redis 里记录这个客户端的最后接收消息 ID比如lastReceivedId。客户端重连时如果带了Last-Event-ID服务端要比较这个 ID 和服务器记录的lastReceivedId取更大的那个开始补发防止客户端时钟回拨或 ID 乱序。4.2 客户端主动上报接收进度浏览器重连只能带最后一个事件的 ID但如果客户端在处理消息时遇到错误比如业务逻辑失败可能某些消息没真正“消费”需要更精确的进度上报。可以在客户端收到消息并处理成功后主动发一个 POST 请求回服务器更新lastReceivedId。这样即使连接断连服务器也知道客户端实际处理到哪了。4.3 补发消息的边界处理补发消息不是无脑全量发要考虑设置补发窗口只补最近 5 分钟的消息太老的消息直接跳过让客户端从最新状态开始。消息队列长度限制内存中最多保留 1000 条最新消息避免内存泄漏。重要消息单独标记对于订单状态、支付结果这类关键消息即使过了窗口期也要有备用查询接口。5. 消息去重关键在客户端但服务端要提供幂等性消息去重主要防两种场景网络抖动导致重复推送客户端短时间内收到两条一模一样的消息。重连补发导致重复客户端已经处理过某条消息重连后服务器又补了一次。5.1 客户端去重方案最简单的是用消息 ID 做判重const receivedIds new Set(); eventSource.onmessage (event) { const message JSON.parse(event.data); if (receivedIds.has(message.id)) { return; // 已经处理过跳过 } receivedIds.add(message.id); // 处理业务逻辑 };但内存 Set 会一直增长需要定期清理老 ID比如只保留最近 1000 个 ID。对于长时间运行的页面可以用 LRU 缓存限制内存占用。5.2 服务端支持幂等消费去重不能只靠客户端服务端也要尽量保证消息幂等。比如消息带唯一 ID客户端处理成功后记录到 localStorage 或 IndexedDB页面刷新后也能读取。对于订单类操作服务端可以先查状态再执行避免重复处理。5.3 顺序问题怎么处理SSE 基于 TCP理论上能保证顺序。但如果重连补发时网络不稳定可能新消息先到补发的旧消息后到导致乱序。稳妥的做法是消息带时间戳或递增版本号客户端按顺序处理。如果收到旧消息直接忽略或更新为最新状态。对于严格依赖顺序的场景如日志回放客户端要缓冲排序后再处理。6. 不同技术栈的 SSE 实现要点6.1 Spring Boot SSESpring Boot 提供了SseEmitter类但要注意每个SseEmitter实例要单独管理生命周期及时complete()或completeWithError()避免内存泄漏。默认超时时间 30 秒可以通过SseEmitter.timeout()调整。集群环境下要用 Redis 或消息队列广播事件否则连接可能落在不同实例上。6.2 FastAPI SSEFastAPI 用生成器Generator实现 SSEapp.get(/stream) async def stream_events(): async def event_generator(): while True: if await event_available(): yield { event: update, data: json.dumps({id: 1, content: 新消息}) } await asyncio.sleep(0.1) return EventSourceResponse(event_generator())注意Python 的同步阻塞操作会卡住整个事件循环要用async/await或线程池。6.3 Go SSEGo 的net/http库直接写流func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set(Content-Type, text/event-stream) w.Header().Set(Cache-Control, no-cache) w.Header().Set(Connection, keep-alive) for { select { case event : -eventChan: fmt.Fprintf(w, id: %d\nevent: message\ndata: %s\n\n, event.ID, event.Data) w.(http.Flusher).Flush() case -r.Context().Done(): return // 客户端断开连接 } } }Go 的并发模型适合高并发 SSE但要自己处理连接管理、超时和错误恢复。6.4 UniApp 对接 SSEUniApp 里不能用标准的EventSource要用uni.request或uni.connectSocket模拟用uni.request设timeout为 0然后循环读流数据。或者用 WebSocket 模拟 SSE 行为但这样失去了 SSE 的简单性。更好的方案是封装一个跨平台的 SSE 组件根据环境选择实现。7. 生产环境部署的注意事项7.1 负载均衡和会话保持SSE 长连接需要会话保持Session Affinity否则重连可能落到不同后端实例拿不到连接状态。Nginx 可以用ip_hash或sticky模块。云服务商AWS ALB、GCP CLB通常支持基于 cookie 的会话保持。最好把连接状态外存到 Redis这样任何实例都能处理重连。7.2 监控和日志SSE 服务要监控活跃连接数突然暴涨可能是有客户端 bug 没关连接。重连频率短时间内频繁重连可能是网络问题或服务不稳定。消息堆积如果发送速度超过客户端处理能力服务端队列会积压。日志要记录连接建立、断开、重连、消息发送失败等事件方便排查问题。7.3 资源限制和清理每个 SSE 连接都会占用内存和文件描述符要设限制单个进程最大连接数。心跳机制检测僵尸连接定期清理。客户端离线超时时间比如 30 分钟无活动自动断开。7.4 安全考虑验证来源Origin Check防止跨站请求伪造。敏感消息加密或脱敏。控制消息频率防 DoS 攻击。8. 什么时候该用 SSE什么时候该换方案SSE 适合服务端向客户端推通知、日志、实时数据。客户端主要是浏览器且支持 SSE。数据流主要是单向的。考虑换 WebSocket 或长轮询的情况需要双向通信如聊天室、协作编辑。客户端环境不支持 SSE某些老旧浏览器或特殊环境。消息频率极高每秒上百条SSE 的 HTTP 头开销可能成为瓶颈。对于简单场景SSE 比 WebSocket 更轻量对于复杂交互WebSocket 更合适。选型关键看业务需求不是技术时髦度。最后提醒一点SSE 的断线重连和消息去重没有银弹要根据业务容忍度调整方案。先从小流量试起把监控和日志打全再逐步优化可靠性。