SSE(Server-Sent Events)快速入门:从零到部署一个实时推送服务

目录

[一、为什么需要 SSE?](#一、为什么需要 SSE?)

[传统 HTTP 的问题](#传统 HTTP 的问题)

[SSE 的解决方案](#SSE 的解决方案)

[二、SSE 是什么?底层怎么跑?](#二、SSE 是什么?底层怎么跑?)

[2.1 定义](#2.1 定义)

[2.2 协议格式](#2.2 协议格式)

[2.3 浏览器端 API](#2.3 浏览器端 API)

[三、SSE vs WebSocket vs 轮询:怎么选?](#三、SSE vs WebSocket vs 轮询:怎么选?)

[四、Spring Boot SSE 实战:实时消息推送](#四、Spring Boot SSE 实战:实时消息推送)

[4.1 项目结构](#4.1 项目结构)

[4.2 pom.xml(只用 spring-boot-starter-web)](#4.2 pom.xml(只用 spring-boot-starter-web))

[4.3 启动类](#4.3 启动类)

[4.4 SSE 连接管理服务(核心)](#4.4 SSE 连接管理服务(核心))

[4.5 SSE Controller(对外接口)](#4.5 SSE Controller(对外接口))

[4.6 前端页面(static/index.html)](#4.6 前端页面(static/index.html))

[4.7 application.yml](#4.7 application.yml)

五、部署与验证

[5.1 启动项目](#5.1 启动项目)

[5.2 打开浏览器](#5.2 打开浏览器)

[5.3 用 curl 验证(理解底层协议)](#5.3 用 curl 验证(理解底层协议))

六、进阶用法

[6.1 定时推送(模拟实时数据)](#6.1 定时推送(模拟实时数据))

[6.2 推送 JSON 对象](#6.2 推送 JSON 对象)

[6.3 带重试间隔的推送](#6.3 带重试间隔的推送)

[6.4 前端拿到断线重连的 Last-Event-ID](#6.4 前端拿到断线重连的 Last-Event-ID)

七、避坑清单

[Nginx 代理 SSE 的正确配置](#Nginx 代理 SSE 的正确配置)

八、总结

核心口诀

一句话速记

技术选型决策树

自检清单

下一步学习路线


🧩 一句话读懂 :SSE 是浏览器内置的"服务器单向推送"技术,比 WebSocket 轻量,比轮询高效,Spring Boot 一行代码就能用。 🎯 适合人群 :用过 HTTP 请求-响应模式,想了解实时推送但不想学 WebSocket 全套的 Java 开发者 📊 难度等级 :⭐⭐☆☆☆(入门) ⏱ 阅读时长 :约 15 分钟 💡 前置知识:Spring Boot 基础、HTTP 协议基本概念


一、为什么需要 SSE?

传统 HTTP 的问题

HTTP 是"请求-响应"模式:客户端不问,服务器就不说

复制代码
客户端:有新消息吗?    →  服务器:没有。
客户端:有新消息吗?    →  服务器:没有。
客户端:有新消息吗?    →  服务器:有!给你。  ← 第 3 次才问到
客户端:有新消息吗?    →  服务器:没有。
...

这叫轮询(Polling),问题很明显:

  • 大量请求是无效的("没有"占了 99%)

  • 浪费带宽和服务器资源

  • 实时性取决于轮询间隔(间隔短→浪费,间隔长→延迟)

SSE 的解决方案

SSE 让服务器主动推送,客户端只需要建立一次连接,之后服务器想发就发:

复制代码
客户端:我要建立 SSE 连接  →  服务器:好的,连接保持
                              服务器:给你一条消息
                              服务器:再给你一条
                              服务器:又来一条
...                           (连接一直保持着)

一句话:SSE = 服务器单向推送 + 自动重连 + 纯 HTTP 协议。


二、SSE 是什么?底层怎么跑?

2.1 定义

SSE(Server-Sent Events) 是 W3C 标准,HTML5 的一部分。浏览器通过 EventSource API 建立一个持久的 HTTP 连接,服务器通过这个连接持续推送文本数据。

2.2 协议格式

SSE 的底层就是 HTTP 响应头 Content-Type: text/event-stream,服务器返回的是一种简单的文本格式:

复制代码
data: 第一条消息\n
\n
data: 第二条消息\n
\n
event: close\ndata: 服务器要关闭了\n
\n

格式规则

字段 说明 示例
data: 消息内容(必须) data: Hello SSE
event: 事件类型(可选) event: notification
id: 消息 ID(可选,用于断线重连) id: 1001
retry: 重连间隔毫秒(可选) retry: 5000
\n\n 每条消息之间用两个换行分隔

多行数据 用多个 data: 字段:

复制代码
data: 第一行内容\n
data: 第二行内容\n
\n

浏览器收到后会用换行符拼接成一个字符串。

2.3 浏览器端 API

java 复制代码
// 创建 SSE 连接
const eventSource = new EventSource('/api/sse/stream');
​
// 监听默认消息
eventSource.onmessage = function(event) {
    console.log('收到消息:', event.data);
};
​
// 监听自定义事件
eventSource.addEventListener('notification', function(event) {
    console.log('通知:', event.data);
});
​
// 连接建立
eventSource.onopen = function() {
    console.log('SSE 连接已建立');
};
​
// 错误处理(浏览器会自动重连)
eventSource.onerror = function(event) {
    console.log('连接断开,浏览器会自动重连...');
};
​
// 关闭连接
eventSource.close();

关键特性

  • 自动重连:连接断了,浏览器自动重新连接(默认 3 秒间隔)

  • 基于 HTTP:不需要特殊协议,能走 HTTP 的地方就能用 SSE

  • 纯文本:只能传文本,不能传二进制(图片、文件等)

  • 浏览器内置:原生支持,不需要第三方库


三、SSE vs WebSocket vs 轮询:怎么选?

维度 轮询 (Polling) SSE (Server-Sent Events) WebSocket
通信方向 客户端 → 服务器 服务器 → 客户端(单向) 双向
协议 HTTP HTTP WS/WSS(独立协议)
连接数 每次请求一个 一个长连接 一个长连接
实时性 差(取决于间隔) 好(秒级) 最好(毫秒级)
浏览器支持 全部 全部(IE 除外) 全部
自动重连 不需要 ✅ 内置 ❌ 需要自己实现
二进制传输 ❌ 只支持文本
实现复杂度 ⭐ 最简单 ⭐⭐ 简单 ⭐⭐⭐ 较复杂
适合场景 低频查询 通知、进度、日志推送 聊天、游戏、协同编辑

一句话决策

复制代码
只需要服务器推送消息给客户端? → SSE ✅
需要客户端和服务器双向实时通信? → WebSocket
数据更新不频繁(分钟级)? → 轮询就够了

四、Spring Boot SSE 实战:实时消息推送

4.1 项目结构

复制代码
sse-demo/
├── src/main/java/com/example/sse/
│   ├── SseDemoApplication.java      // 启动类
│   ├── controller/SseController.java // SSE 接口
│   └── service/SseEmitterService.java // 连接管理
├── src/main/resources/
│   ├── application.yml
│   └── static/index.html            // 前端页面
└── pom.xml

4.2 pom.xml(只用 spring-boot-starter-web)

java 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
​
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>
​
    <groupId>com.example</groupId>
    <artifactId>sse-demo</artifactId>
    <version>1.0.0</version>
    <name>sse-demo</name>
    <description>SSE 快速入门示例</description>
​
    <dependencies>
        <!-- 就这一个依赖就够了 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
​
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

4.3 启动类

java 复制代码
package com.example.sse;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SseDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SseDemoApplication.class, args);
    }
}

4.4 SSE 连接管理服务(核心)

java 复制代码
package com.example.sse.service;

import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * SSE 连接管理器
 * 用 ConcurrentHashMap 管理所有客户端的 SseEmitter 连接
 */
@Service
public class SseEmitterService {

    // 存储所有客户端连接,key = 用户ID,value = SseEmitter
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();

    /**
     * 创建 SSE 连接
     * @param userId 用户唯一标识
     * @param timeout 超时时间(毫秒),0 表示不超时
     */
    public SseEmitter createEmitter(String userId, long timeout) {
        // 设置超时时间,0 表示永不超时
        SseEmitter emitter = new SseEmitter(timeout);

        // 注册回调:连接完成、超时、异常时移除连接
        emitter.onCompletion(() -> emitterMap.remove(userId));
        emitter.onTimeout(() -> emitterMap.remove(userId));
        emitter.onError(e -> emitterMap.remove(userId));

        // 存入连接池
        emitterMap.put(userId, emitter);

        System.out.println("用户 " + userId + " 建立 SSE 连接,当前在线: " + emitterMap.size());
        return emitter;
    }

    /**
     * 向指定用户推送消息
     */
    public void sendToUser(String userId, String eventName, Object data) {
        SseEmitter emitter = emitterMap.get(userId);
        if (emitter == null) {
            System.out.println("用户 " + userId + " 不在线,消息丢弃");
            return;
        }
        try {
            emitter.send(SseEmitter.event()
                    .name(eventName)      // 事件类型
                    .data(data));         // 消息内容
        } catch (IOException e) {
            System.out.println("推送给 " + userId + " 失败: " + e.getMessage());
            emitterMap.remove(userId);
        }
    }

    /**
     * 广播给所有在线用户
     */
    public void broadcast(String eventName, Object data) {
        emitterMap.forEach((userId, emitter) -> {
            try {
                emitter.send(SseEmitter.event()
                        .name(eventName)
                        .data(data));
            } catch (IOException e) {
                System.out.println("广播给 " + userId + " 失败: " + e.getMessage());
                emitterMap.remove(userId);
            }
        });
    }

    /**
     * 关闭指定用户的连接
     */
    public void closeEmitter(String userId) {
        SseEmitter emitter = emitterMap.get(userId);
        if (emitter != null) {
            emitter.complete();  // 正常关闭
            emitterMap.remove(userId);
        }
    }

    /**
     * 获取当前在线人数
     */
    public int getOnlineCount() {
        return emitterMap.size();
    }
}

4.5 SSE Controller(对外接口)

java 复制代码
package com.example.sse.controller;

import com.example.sse.service.SseEmitterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
@RequestMapping("/api/sse")
public class SseController {

    @Autowired
    private SseEmitterService sseEmitterService;

    /**
     * 建立 SSE 连接
     * GET /api/sse/connect?userId=user1
     *
     * produces = MediaType.TEXT_EVENT_STREAM_VALUE 是关键!
     * 告诉浏览器这是一个 SSE 流
     */
    @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter connect(@RequestParam String userId) {
        // 0 表示永不超时
        return sseEmitterService.createEmitter(userId, 0L);
    }

    /**
     * 向指定用户推送消息
     * POST /api/sse/send?userId=user1&message=你好
     */
    @PostMapping("/send")
    public String sendToUser(@RequestParam String userId,
                             @RequestParam String message) {
        sseEmitterService.sendToUser(userId, "message", message);
        return "消息已推送给 " + userId;
    }

    /**
     * 广播消息给所有在线用户
     * POST /api/sse/broadcast?message=全体通知
     */
    @PostMapping("/broadcast")
    public String broadcast(@RequestParam String message) {
        sseEmitterService.broadcast("notification", message);
        return "已广播给 " + sseEmitterService.getOnlineCount() + " 个用户";
    }

    /**
     * 关闭指定用户的连接
     * POST /api/sse/close?userId=user1
     */
    @PostMapping("/close")
    public String close(@RequestParam String userId) {
        sseEmitterService.closeEmitter(userId);
        return "已关闭 " + userId + " 的 SSE 连接";
    }

    /**
     * 查看当前在线人数
     * GET /api/sse/count
     */
    @GetMapping("/count")
    public int getOnlineCount() {
        return sseEmitterService.getOnlineCount();
    }
}

4.6 前端页面(static/index.html)

java 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>SSE 快速入门演示</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Microsoft YaHei', sans-serif; padding: 20px; background: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; }
        h1 { color: #333; margin-bottom: 20px; }
        .card { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        .card h3 { color: #666; margin-bottom: 12px; font-size: 14px; }
        .status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 13px; }
        .status.connected { background: #d4edda; color: #155724; }
        .status.disconnected { background: #f8d7da; color: #721c24; }
        input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; width: 300px; }
        button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-left: 8px; font-size: 14px; }
        .btn-primary { background: #007bff; color: #fff; }
        .btn-danger { background: #dc3545; color: #fff; }
        .btn-success { background: #28a745; color: #fff; }
        button:hover { opacity: 0.85; }
        #messages { max-height: 400px; overflow-y: auto; }
        .msg-item { padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 14px; }
        .msg-item .time { color: #999; margin-right: 8px; }
        .msg-item .type { color: #007bff; margin-right: 8px; font-weight: bold; }
    </style>
</head>
<body>
<div class="container">
    <h1>🔔 SSE 实时消息推送演示</h1>

    <!-- 连接控制 -->
    <div class="card">
        <h3>连接控制</h3>
        <div>
            <input type="text" id="userId" placeholder="输入用户ID(如 user1)" value="user1">
            <button class="btn-primary" onclick="connectSSE()">建立连接</button>
            <button class="btn-danger" onclick="closeSSE()">断开连接</button>
            <span id="status" class="status disconnected">未连接</span>
        </div>
    </div>

    <!-- 发送消息 -->
    <div class="card">
        <h3>推送消息</h3>
        <div style="margin-bottom: 8px;">
            <input type="text" id="targetUser" placeholder="目标用户ID" value="user1">
            <input type="text" id="message" placeholder="消息内容" value="你好,这是测试消息">
            <button class="btn-success" onclick="sendToUser()">推送</button>
        </div>
        <div>
            <button class="btn-primary" onclick="broadcast()">📢 广播给所有人</button>
        </div>
    </div>

    <!-- 消息列表 -->
    <div class="card">
        <h3>消息记录</h3>
        <div id="messages">
            <div class="msg-item" style="color:#999;">等待连接...</div>
        </div>
    </div>
</div>

<script>
    let eventSource = null;

    // 建立 SSE 连接
    function connectSSE() {
        const userId = document.getElementById('userId').value;
        if (!userId) { alert('请输入用户ID'); return; }
        if (eventSource) { eventSource.close(); }

        // 👇 关键:创建 EventSource 对象,指向后端 SSE 接口
        eventSource = new EventSource('/api/sse/connect?userId=' + userId);

        // 连接建立
        eventSource.onopen = function() {
            updateStatus(true);
            addMessage('系统', 'SSE 连接已建立');
        };

        // 监听默认 message 事件
        eventSource.onmessage = function(event) {
            addMessage('默认', event.data);
        };

        // 监听自定义 "message" 事件(对应后端的 .name("message"))
        eventSource.addEventListener('message', function(event) {
            addMessage('消息', event.data);
        });

        // 监听自定义 "notification" 事件(对应后端的 .name("notification"))
        eventSource.addEventListener('notification', function(event) {
            addMessage('📢 通知', event.data);
        });

        // 错误处理(浏览器会自动重连)
        eventSource.onerror = function() {
            updateStatus(false);
            addMessage('系统', '连接断开,正在自动重连...');
        };
    }

    // 断开连接
    function closeSSE() {
        if (eventSource) {
            eventSource.close();
            eventSource = null;
            updateStatus(false);
            addMessage('系统', '已主动断开连接');
        }
    }

    // 推送给指定用户
    function sendToUser() {
        const targetUser = document.getElementById('targetUser').value;
        const message = document.getElementById('message').value;
        fetch('/api/sse/send?userId=' + targetUser + '&message=' + encodeURIComponent(message), {
            method: 'POST'
        }).then(r => r.text()).then(t => addMessage('系统', t));
    }

    // 广播
    function broadcast() {
        const message = document.getElementById('message').value;
        fetch('/api/sse/broadcast?message=' + encodeURIComponent(message), {
            method: 'POST'
        }).then(r => r.text()).then(t => addMessage('系统', t));
    }

    // 更新连接状态
    function updateStatus(connected) {
        const el = document.getElementById('status');
        el.textContent = connected ? '已连接' : '未连接';
        el.className = 'status ' + (connected ? 'connected' : 'disconnected');
    }

    // 添加消息到列表
    function addMessage(type, content) {
        const container = document.getElementById('messages');
        const time = new Date().toLocaleTimeString();
        container.innerHTML = '<div class="msg-item"><span class="time">' + time +
            '</span><span class="type">[' + type + ']</span>' + content + '</div>' +
            container.innerHTML;
    }
</script>
</body>
</html>

4.7 application.yml

XML 复制代码
server:
  port: 8080

spring:
  application:
    name: sse-demo

五、部署与验证

5.1 启动项目

XML 复制代码
cd sse-demo
mvn spring-boot:run

5.2 打开浏览器

访问 http://localhost:8080/index.html

操作步骤

  1. 在页面输入 user1,点击「建立连接」→ 状态变为「已连接」

  2. 打开第二个浏览器标签页 ,输入 user2,也建立连接

  3. 在第一个标签页输入消息,点击「推送」→ user1 收到消息

  4. 点击「广播」→ 两个标签页都收到消息

5.3 用 curl 验证(理解底层协议)

XML 复制代码
# 建立 SSE 连接
curl -N http://localhost:8080/api/sse/connect?userId=test

# 另开一个终端,推消息
curl -X POST "http://localhost:8080/api/sse/send?userId=test&message=hello"

第一个终端会看到:

复制代码
event:message
data:hello

这就是 SSE 的原始协议格式------event: + data: + 两个换行分隔。


六、进阶用法

6.1 定时推送(模拟实时数据)

SseEmitterService 中加一个定时推送方法:

java 复制代码
/**
 * 模拟定时推送(比如每 3 秒推送一次系统状态)
 * 配合 @Scheduled 使用
 */
@Scheduled(fixedRate = 3000)
public void pushSystemStatus() {
    String status = "在线用户: " + emitterMap.size() + ", 时间: " + LocalDateTime.now();
    broadcast("system-status", status);
}

启动类加 @EnableScheduling

java 复制代码
@SpringBootApplication
@EnableScheduling  // 启用定时任务
public class SseDemoApplication { ... }

6.2 推送 JSON 对象

java 复制代码
// 推送复杂对象
Map<String, Object> data = new HashMap<>();
data.put("orderId", "ORD-20260616-001");
data.put("status", "已发货");
data.put("expressNo", "SF1234567890");

sseEmitterService.sendToUser("user1", "order-update", data);
// 自动序列化为 JSON:{"orderId":"ORD-20260616-001","status":"已发货",...}

前端接收:

java 复制代码
eventSource.addEventListener('order-update', function(event) {
    const order = JSON.parse(event.data);  // 解析 JSON
    console.log('订单更新:', order.orderId, order.status);
});

6.3 带重试间隔的推送

java 复制代码
emitter.send(SseEmitter.event()
        .name("message")
        .id("msg-1001")          // 消息 ID(浏览器断线重连时会带上 Last-Event-ID)
        .reconnectTime(5000)     // 断线后 5 秒重连
        .data("Hello"));

6.4 前端拿到断线重连的 Last-Event-ID

浏览器自动重连时会在请求头里带上 Last-Event-ID,后端可以据此补发漏掉的消息:

java 复制代码
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId,
                          @RequestHeader(value = "Last-Event-ID", required = false) String lastId) {
    System.out.println("用户 " + userId + " 重连,上次收到的消息ID: " + lastId);
    // 可以根据 lastId 补发消息
    return sseEmitterService.createEmitter(userId, 0L);
}

七、避坑清单

# 坑点 现象 原因 ✅ 避坑方案
1 连接数打满 浏览器同域名最多 6 个 SSE 连接 HTTP/1.1 浏览器限制同域并发连接数为 6 多用户场景用 WebSocket,或用 HTTP/2(无此限制)
2 Nginx 代理超时 推送几分钟后连接断开 Nginx 默认 proxy_read_timeout 60s 配置 proxy_read_timeout 3600s; 或按需调整
3 Spring 返回 406 建立连接报 406 Not Acceptable 忘了加 produces = MediaType.TEXT_EVENT_STREAM_VALUE Controller 方法加 produces 属性
4 中文乱码 消息中文变成 ??? 没指定编码 data 方法加 charset:.data("中文", MediaType.APPLICATION_JSON)
5 连接不释放 用户关闭页面后连接还在 服务端不知道客户端走了 配合心跳检测,或监听 emitter.onCompletion
6 生产环境连接数爆满 大量用户导致服务器线程耗尽 SSE 是长连接,每个连接占一个线程 用 WebFlux(响应式)替代 Servlet 模式
7 浏览器自动重连导致雪崩 服务器挂了,所有客户端同时重连 没有设置 retry 间隔 服务端设置合理的 reconnectTime,客户端加随机延迟

Nginx 代理 SSE 的正确配置

java 复制代码
location /api/sse/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection '';           # 清除 Connection 头
    proxy_buffering off;                      # 关闭缓冲!否则消息会攒一批才发
    proxy_cache off;                          # 关闭缓存
    proxy_read_timeout 3600s;                 # 超时时间拉长
    chunked_transfer_encoding off;            # 关闭分块传输
}

八、总结

核心口诀

SSE 是服务器单向推,一条 HTTP 连接永不关。浏览器内置自动重连,Spring 一个注解就能用。

一句话速记

复制代码
SSE = Content-Type: text/event-stream
    + EventSource API
    + 自动重连
    = 最轻量的服务器推送方案

技术选型决策树

复制代码
需要服务器实时推送数据?
├─ 只需要服务器 → 客户端推送?
│  ├─ 是 → SSE ✅(通知、进度条、日志流、实时数据)
│  └─ 需要双向通信?
│     ├─ 是 → WebSocket(聊天、游戏、协同编辑)
│     └─ 否 → SSE
└─ 数据更新频率?
   ├─ 分钟级 → 普通轮询就够了
   ├─ 秒级 → SSE ✅
   └─ 毫秒级 → WebSocket

自检清单

  • 我能说清楚 SSE 和 WebSocket 的区别
  • 我知道 SSE 底层就是 Content-Type: text/event-stream
  • 我能用 Spring Boot 的 SseEmitter 写一个推送接口
  • 我知道浏览器同域最多 6 个 SSE 连接(HTTP/1.1)
  • 我知道 Nginx 代理 SSE 必须关 proxy_buffering
  • 我知道生产环境推荐用 WebFlux 避免线程耗尽

下一步学习路线

复制代码
阶段一(入门):本文 → 跑通 Spring Boot SSE demo
阶段二(实战):在 RuoYi 项目中用 SSE 实现订单状态推送 / 导出进度通知
阶段三(进阶):学习 WebFlux 响应式 SSE(高并发场景)
阶段四(扩展):了解 SSE + 消息队列(Redis Stream / RabbitMQ)的架构