Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(三)

完整设计:Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(一)

代码示例:Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(二)

配置与测试部署:Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(三)

文章目录

  • [Spring Boot 3 SSE 配置、测试、部署配置和运维指南](#Spring Boot 3 SSE 配置、测试、部署配置和运维指南)
    • [1. 配置文件详解](#1. 配置文件详解)
      • [1.1 Redis 消息监听器配置](#1.1 Redis 消息监听器配置)
      • [1.2 心跳调度器配置](#1.2 心跳调度器配置)
      • [1.3 安全配置](#1.3 安全配置)
      • [1.4 异步线程池配置](#1.4 异步线程池配置)
      • [1.5 限流过滤器(防止滥用)](#1.5 限流过滤器(防止滥用))
      • [1.6 自定义 Actuator 端点](#1.6 自定义 Actuator 端点)
    • [2. 业务场景实现示例](#2. 业务场景实现示例)
      • [2.1 实时通知服务](#2.1 实时通知服务)
      • [2.2 文件上传进度监控](#2.2 文件上传进度监控)
      • [2.3 数据大屏实时刷新](#2.3 数据大屏实时刷新)
    • [3. 前端客户端示例](#3. 前端客户端示例)
      • [3.1 原生 JavaScript 实现](#3.1 原生 JavaScript 实现)
      • [3.2 React 客户端示例](#3.2 React 客户端示例)
    • [1. 测试方案](#1. 测试方案)
      • [1.1 单元测试](#1.1 单元测试)
        • [ConnectionManager 测试](#ConnectionManager 测试)
        • [MessageService 测试](#MessageService 测试)
      • [1.2 集成测试](#1.2 集成测试)
      • [1.3 压力测试](#1.3 压力测试)
        • [JMeter 测试计划](#JMeter 测试计划)
        • [Gatling 压力测试脚本](#Gatling 压力测试脚本)
      • [1.4 性能测试报告模板](#1.4 性能测试报告模板)
    • [2. Docker 部署](#2. Docker 部署)
      • [2.1 Dockerfile](#2.1 Dockerfile)
      • [2.2 docker-compose.yml](#2.2 docker-compose.yml)
      • [2.3 Prometheus 配置](#2.3 Prometheus 配置)
    • [3. Kubernetes 部署](#3. Kubernetes 部署)
      • [3.1 Deployment](#3.1 Deployment)
      • [3.2 Service](#3.2 Service)
      • [3.3 Ingress](#3.3 Ingress)
      • [3.4 ConfigMap](#3.4 ConfigMap)
      • [3.5 HorizontalPodAutoscaler](#3.5 HorizontalPodAutoscaler)
    • [4. Nginx 负载均衡配置](#4. Nginx 负载均衡配置)
      • [4.1 完整 Nginx 配置](#4.1 完整 Nginx 配置)
    • [5. 监控和告警](#5. 监控和告警)
      • [5.1 Grafana Dashboard JSON](#5.1 Grafana Dashboard JSON)
      • [5.2 Prometheus 告警规则](#5.2 Prometheus 告警规则)
    • [6. 常见问题排查](#6. 常见问题排查)
      • [6.1 问题诊断清单](#6.1 问题诊断清单)
      • [6.2 日志分析](#6.2 日志分析)

Spring Boot 3 SSE 配置、测试、部署配置和运维指南

本文档包含配置文件、测试方案、部署配置和运维指南。


1. 配置文件详解

1.1 Redis 消息监听器配置

java 复制代码
package com.enterprise.sse.config;

import com.enterprise.sse.manager.SseConnectionManager;
import com.enterprise.sse.model.SseEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

/**
 * Redis 消息监听器 - 集群消息分发
 */
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(name = "sse.redis.enabled", havingValue = "true", matchIfMissing = true)
public class RedisMessageListenerConfig {
    
    private final SseConnectionManager connectionManager;
    private final ObjectMapper objectMapper;
    
    @Value("${sse.redis.channel:sse:message}")
    private String redisChannel;
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            MessageListenerAdapter messageListenerAdapter) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(messageListenerAdapter, new PatternTopic(redisChannel));
        
        log.info("Redis 消息监听器已启动,监听频道: {}", redisChannel);
        return container;
    }
    
    @Bean
    public MessageListenerAdapter messageListenerAdapter() {
        return new MessageListenerAdapter(new RedisMessageListener(), "onMessage");
    }
    
    public class RedisMessageListener {
        
        public void onMessage(String message, String pattern) {
            try {
                SseEvent event = objectMapper.readValue(message, SseEvent.class);
                
                log.debug("收到Redis消息: eventType={}, targetUser={}", 
                        event.getEvent(), event.getTargetUserId());
                
                // 根据目标类型分发
                if (event.getTargetUserId() != null) {
                    connectionManager.sendToUser(event.getTargetUserId(), event);
                } else if (event.getTargetGroupId() != null) {
                    connectionManager.sendToGroup(event.getTargetGroupId(), event);
                } else {
                    connectionManager.broadcast(event);
                }
                
            } catch (Exception e) {
                log.error("处理Redis消息失败: message={}", message, e);
            }
        }
    }
}

1.2 心跳调度器配置

java 复制代码
package com.enterprise.sse.scheduler;

import com.enterprise.sse.manager.SseConnectionManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * SSE 心跳调度器
 */
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "sse.heartbeat.enabled", havingValue = "true", matchIfMissing = true)
public class SseHeartbeatScheduler {
    
    private final SseConnectionManager connectionManager;
    
    /**
     * 发送心跳 - 每30秒
     */
    @Scheduled(fixedDelayString = "${sse.heartbeat.interval-seconds:30}000")
    public void sendHeartbeat() {
        connectionManager.getOnlineUsers().forEach(userId -> {
            try {
                connectionManager.sendHeartbeat(userId);
            } catch (Exception e) {
                log.warn("发送心跳失败: userId={}", userId);
            }
        });
    }
    
    /**
     * 清理超时连接 - 每分钟
     */
    @Scheduled(fixedDelay = 60000, initialDelay = 60000)
    public void cleanupTimeoutConnections() {
        int cleaned = connectionManager.cleanupTimeoutConnections();
        if (cleaned > 0) {
            log.info("清理超时连接: {} 个", cleaned);
        }
    }
    
    /**
     * 打印统计 - 每5分钟
     */
    @Scheduled(fixedDelay = 300000, initialDelay = 60000)
    public void printStats() {
        SseConnectionManager.ConnectionStats stats = connectionManager.getStats();
        log.info("SSE统计: 活跃={}, 总消息={}", 
                stats.activeConnections(), stats.totalMessagesSent());
    }
}

1.3 安全配置

java 复制代码
package com.enterprise.sse.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

/**
 * Spring Security 配置
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/sse/connect").authenticated()
                .requestMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic();
        
        return http.build();
    }
}

1.4 异步线程池配置

java 复制代码
package com.enterprise.sse.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 异步任务线程池
 */
@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("sse-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

1.5 限流过滤器(防止滥用)

java 复制代码
package com.enterprise.sse.filter;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

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

/**
 * SSE 连接限流过滤器
 */
@Slf4j
@Component
public class SseRateLimitFilter implements Filter {
    
    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        if (!httpRequest.getRequestURI().contains("/api/sse/connect")) {
            chain.doFilter(request, response);
            return;
        }
        
        String userId = getUserId(httpRequest);
        
        if (userId == null) {
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "未认证");
            return;
        }
        
        Bucket bucket = buckets.computeIfAbsent(userId, k -> createBucket());
        
        if (bucket.tryConsume(1)) {
            chain.doFilter(request, response);
        } else {
            log.warn("用户连接频率过高: userId={}", userId);
            httpResponse.sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS, 
                    "连接请求过于频繁,请稍后再试");
        }
    }
    
    private Bucket createBucket() {
        // 每分钟5个令牌
        Bandwidth limit = Bandwidth.classic(5, Refill.intervally(5, Duration.ofMinutes(1)));
        return Bucket.builder().addLimit(limit).build();
    }
    
    private String getUserId(HttpServletRequest request) {
        String userId = request.getParameter("userId");
        if (userId == null && request.getUserPrincipal() != null) {
            userId = request.getUserPrincipal().getName();
        }
        return userId;
    }
}

1.6 自定义 Actuator 端点

java 复制代码
package com.enterprise.sse.actuator;

import com.enterprise.sse.manager.SseConnectionManager;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * SSE 监控端点
 * 访问: /actuator/sse
 */
@Component
@Endpoint(id = "sse")
@RequiredArgsConstructor
public class SseActuatorEndpoint {
    
    private final SseConnectionManager connectionManager;
    
    @ReadOperation
    public Map<String, Object> getSseInfo() {
        SseConnectionManager.ConnectionStats stats = connectionManager.getStats();
        
        return Map.of(
                "stats", stats,
                "onlineUsers", connectionManager.getOnlineUsers(),
                "onlineCount", stats.activeConnections()
        );
    }
}

2. 业务场景实现示例

2.1 实时通知服务

java 复制代码
package com.enterprise.sse.demo;

import com.enterprise.sse.service.SseMessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * 通知服务
 */
@Service
@RequiredArgsConstructor
public class NotificationService {
    
    private final SseMessageService sseMessageService;
    
    /**
     * 订单状态更新通知
     */
    public void notifyOrderStatus(String userId, String orderId, String status) {
        String title = "订单状态更新";
        String content = String.format("您的订单 %s 已%s", orderId, status);
        
        sseMessageService.sendNotification(userId, title, content);
    }
    
    /**
     * 系统维护通知(广播)
     */
    public void notifySystemMaintenance(String message) {
        sseMessageService.broadcast(
                SseEvent.systemBroadcast("系统维护: " + message)
        );
    }
}

2.2 文件上传进度监控

java 复制代码
package com.enterprise.sse.demo;

import com.enterprise.sse.service.SseMessageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;

/**
 * 文件上传服务(带进度推送)
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class FileUploadService {
    
    private final SseMessageService sseMessageService;
    
    public String uploadWithProgress(String userId, String taskId, MultipartFile file) 
            throws IOException {
        
        long fileSize = file.getSize();
        long uploadedBytes = 0;
        
        try (InputStream inputStream = file.getInputStream()) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                // 模拟上传
                uploadChunk(buffer, bytesRead);
                
                uploadedBytes += bytesRead;
                int percentage = (int) ((uploadedBytes * 100) / fileSize);
                
                // 推送进度
                sseMessageService.sendProgress(userId, taskId, percentage, 
                        String.format("已上传 %d%%", percentage));
                
                if (percentage % 10 == 0) {
                    log.info("上传进度: taskId={}, progress={}%", taskId, percentage);
                }
            }
        }
        
        sseMessageService.sendProgress(userId, taskId, 100, "上传完成");
        return "file-url-" + taskId;
    }
    
    private void uploadChunk(byte[] data, int length) {
        try {
            Thread.sleep(50); // 模拟延迟
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

2.3 数据大屏实时刷新

java 复制代码
package com.enterprise.sse.demo;

import com.enterprise.sse.model.SseEvent;
import com.enterprise.sse.service.SseMessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Random;

/**
 * 数据大屏服务
 */
@Service
@RequiredArgsConstructor
public class DashboardService {
    
    private final SseMessageService sseMessageService;
    private final Random random = new Random();
    
    /**
     * 每5秒推送实时数据
     */
    @Scheduled(fixedDelay = 5000)
    public void pushRealtimeData() {
        Map<String, Object> data = Map.of(
                "销售额", random.nextInt(100000),
                "访问量", random.nextInt(10000),
                "在线用户", random.nextInt(1000),
                "timestamp", System.currentTimeMillis()
        );
        
        sseMessageService.sendToGroup("dashboard", SseEvent.builder()
                .id(String.valueOf(System.currentTimeMillis()))
                .event("dashboard-data")
                .data(data)
                .build());
    }
}

3. 前端客户端示例

3.1 原生 JavaScript 实现

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>SSE Client Demo</title>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 20px auto;
            padding: 20px;
        }
        #status { font-size: 18px; font-weight: bold; margin: 10px 0; }
        #messages {
            border: 1px solid #ddd;
            padding: 10px;
            max-height: 500px;
            overflow-y: auto;
        }
        .message {
            padding: 8px;
            margin: 5px 0;
            border-radius: 4px;
            background: #f5f5f5;
        }
        .message.notification {
            background: #e3f2fd;
            border-left: 4px solid #2196f3;
        }
        .message.system {
            background: #fff3e0;
            border-left: 4px solid #ff9800;
        }
    </style>
</head>
<body>
    <h1>SSE 实时通知演示</h1>
    <div id="status">未连接</div>
    <div id="messages"></div>
    
    <script>
        const SSE_URL = 'http://localhost:8080/api/sse/connect';
        const USER_ID = 'user123';
        const AUTH_TOKEN = btoa('admin:admin123');
        
        let eventSource;
        let reconnectAttempts = 0;
        const MAX_RECONNECT_ATTEMPTS = 5;
        
        function connect() {
            const url = `${SSE_URL}?userId=${USER_ID}`;
            eventSource = new EventSource(url);
            
            eventSource.onopen = function(event) {
                console.log('SSE 连接已建立', event);
                document.getElementById('status').textContent = '已连接';
                document.getElementById('status').style.color = 'green';
                reconnectAttempts = 0;
            };
            
            eventSource.onmessage = function(event) {
                console.log('收到消息:', event.data);
                addMessage('默认消息', event.data);
            };
            
            eventSource.addEventListener('connected', function(event) {
                const data = JSON.parse(event.data);
                addMessage('系统', `欢迎!连接ID: ${event.lastEventId}`);
            });
            
            eventSource.addEventListener('notification', function(event) {
                const data = JSON.parse(event.data);
                addMessage('通知', JSON.stringify(data), 'notification');
                
                if (Notification.permission === 'granted') {
                    new Notification(data.title || '新通知', {
                        body: data.content || data
                    });
                }
            });
            
            eventSource.addEventListener('progress', function(event) {
                const data = JSON.parse(event.data);
                updateProgress(data);
            });
            
            eventSource.addEventListener('system', function(event) {
                const data = JSON.parse(event.data);
                addMessage('系统广播', data, 'system');
            });
            
            eventSource.onerror = function(event) {
                console.error('SSE 连接错误:', event);
                document.getElementById('status').textContent = '连接错误';
                document.getElementById('status').style.color = 'red';
                
                if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
                    reconnectAttempts++;
                    console.log(`尝试重连 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`);
                    setTimeout(() => {
                        eventSource.close();
                        connect();
                    }, 3000);
                }
            };
        }
        
        function addMessage(type, content, className = '') {
            const messagesDiv = document.getElementById('messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${className}`;
            messageDiv.innerHTML = `
                <strong>[${new Date().toLocaleTimeString()}] ${type}:</strong>
                <span>${content}</span>
            `;
            messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild);
            
            if (messagesDiv.children.length > 50) {
                messagesDiv.removeChild(messagesDiv.lastChild);
            }
        }
        
        function updateProgress(data) {
            let progressBar = document.getElementById('progress-bar');
            if (!progressBar) {
                progressBar = document.createElement('div');
                progressBar.id = 'progress-bar';
                progressBar.style.cssText = 'width: 100%; background: #ddd; height: 20px; margin: 10px 0;';
                progressBar.innerHTML = '<div id="progress-fill" style="height: 100%; background: #4caf50; width: 0%;"></div>';
                document.body.insertBefore(progressBar, document.getElementById('messages'));
            }
            
            const fill = document.getElementById('progress-fill');
            fill.style.width = data.percentage + '%';
            fill.textContent = data.percentage + '%';
            
            if (data.percentage === 100) {
                setTimeout(() => progressBar.remove(), 2000);
            }
        }
        
        if (Notification.permission === 'default') {
            Notification.requestPermission();
        }
        
        connect();
        
        window.addEventListener('beforeunload', () => {
            if (eventSource) {
                eventSource.close();
            }
        });
    </script>
</body>
</html>

3.2 React 客户端示例

jsx 复制代码
import { useEffect, useState, useRef } from 'react';

function SseClient() {
    const [status, setStatus] = useState('未连接');
    const [messages, setMessages] = useState([]);
    const eventSourceRef = useRef(null);
    
    useEffect(() => {
        const userId = 'user123';
        const url = `http://localhost:8080/api/sse/connect?userId=${userId}`;
        
        eventSourceRef.current = new EventSource(url);
        
        eventSourceRef.current.onopen = () => {
            setStatus('已连接');
        };
        
        eventSourceRef.current.addEventListener('notification', (event) => {
            const data = JSON.parse(event.data);
            addMessage('通知', data.content);
        });
        
        eventSourceRef.current.onerror = () => {
            setStatus('连接错误');
        };
        
        return () => {
            eventSourceRef.current?.close();
        };
    }, []);
    
    const addMessage = (type, content) => {
        setMessages(prev => [{
            type,
            content,
            time: new Date().toLocaleTimeString()
        }, ...prev].slice(0, 50));
    };
    
    return (
        <div>
            <h1>SSE 实时通知</h1>
            <div>状态: {status}</div>
            <div>
                {messages.map((msg, index) => (
                    <div key={index}>
                        [{msg.time}] {msg.type}: {msg.content}
                    </div>
                ))}
            </div>
        </div>
    );
}

export default SseClient;

前端实现要点

  • 使用原生 EventSource API
  • 实现自动重连机制
  • 监听不同类型的事件
  • 浏览器通知集成
  • 进度条动态更新

1. 测试方案

1.1 单元测试

ConnectionManager 测试
java 复制代码
package com.enterprise.sse.manager;

import com.enterprise.sse.model.SseEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 连接管理器单元测试
 */
class SseConnectionManagerTest {
    
    private SseConnectionManager connectionManager;
    
    @BeforeEach
    void setUp() {
        connectionManager = new SseConnectionManager(
                new ObjectMapper(),
                new SimpleMeterRegistry()
        );
    }
    
    @Test
    void testCreateConnection() {
        String userId = "test-user";
        SseEmitter emitter = connectionManager.createConnection(
                userId, "127.0.0.1", "Test-Agent"
        );
        
        assertNotNull(emitter);
        assertEquals(1, connectionManager.getStats().activeConnections());
    }
    
    @Test
    void testSendToUser() {
        String userId = "test-user";
        connectionManager.createConnection(userId, "127.0.0.1", "Test");
        
        SseEvent event = SseEvent.notification(userId, "测试消息");
        boolean result = connectionManager.sendToUser(userId, event);
        
        assertTrue(result);
    }
    
    @Test
    void testGroupMessaging() {
        String groupId = "test-group";
        String user1 = "user1";
        String user2 = "user2";
        
        connectionManager.createConnection(user1, "127.0.0.1", "Test");
        connectionManager.createConnection(user2, "127.0.0.1", "Test");
        
        connectionManager.addToGroup(user1, groupId);
        connectionManager.addToGroup(user2, groupId);
        
        SseEvent event = SseEvent.builder()
                .event("test")
                .data("group message")
                .build();
        
        int sent = connectionManager.sendToGroup(groupId, event);
        assertEquals(2, sent);
    }
    
    @Test
    void testConnectionLimit() {
        String userId = "test-user";
        
        // 创建最大允许数量的连接
        for (int i = 0; i < 3; i++) {
            connectionManager.createConnection(
                    userId + i, "127.0.0.1", "Test"
            );
        }
        
        // 超过限制应该抛出异常
        assertThrows(IllegalStateException.class, () -> {
            connectionManager.createConnection(userId, "127.0.0.1", "Test");
        });
    }
    
    @Test
    void testCleanupTimeoutConnections() throws InterruptedException {
        String userId = "test-user";
        connectionManager.createConnection(userId, "127.0.0.1", "Test");
        
        // 模拟超时(实际场景需要等待超时时间)
        Thread.sleep(100);
        
        int cleaned = connectionManager.cleanupTimeoutConnections();
        assertTrue(cleaned >= 0);
    }
    
    @Test
    void testBroadcast() {
        connectionManager.createConnection("user1", "127.0.0.1", "Test");
        connectionManager.createConnection("user2", "127.0.0.1", "Test");
        connectionManager.createConnection("user3", "127.0.0.1", "Test");
        
        SseEvent event = SseEvent.systemBroadcast("系统通知");
        int sent = connectionManager.broadcast(event);
        
        assertEquals(3, sent);
    }
}
MessageService 测试
java 复制代码
package com.enterprise.sse.service;

import com.enterprise.sse.manager.SseConnectionManager;
import com.enterprise.sse.model.SseEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Arrays;
import java.util.concurrent.Executor;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * 消息服务测试
 */
@ExtendWith(MockitoExtension.class)
class SseMessageServiceTest {
    
    @Mock
    private SseConnectionManager connectionManager;
    
    @Mock
    private StringRedisTemplate redisTemplate;
    
    @Mock
    private ObjectMapper objectMapper;
    
    @Mock
    private Executor taskExecutor;
    
    @InjectMocks
    private SseMessageService messageService;
    
    @Test
    void testSendToUser() {
        String userId = "test-user";
        SseEvent event = SseEvent.notification(userId, "测试");
        
        when(connectionManager.sendToUser(eq(userId), any())).thenReturn(true);
        
        boolean result = messageService.sendToUser(userId, event);
        
        assertTrue(result);
        verify(connectionManager, times(1)).sendToUser(eq(userId), any());
    }
    
    @Test
    void testSendNotification() {
        String userId = "test-user";
        
        when(connectionManager.sendToUser(eq(userId), any())).thenReturn(true);
        
        messageService.sendNotification(userId, "标题", "内容");
        
        verify(connectionManager, times(1)).sendToUser(eq(userId), any());
    }
    
    @Test
    void testSendProgress() {
        String userId = "test-user";
        
        when(connectionManager.sendToUser(eq(userId), any())).thenReturn(true);
        
        messageService.sendProgress(userId, "task-1", 50, "处理中");
        
        verify(connectionManager, times(1)).sendToUser(eq(userId), any());
    }
    
    @Test
    void testBatchSend() {
        when(connectionManager.sendToUser(anyString(), any())).thenReturn(true);
        
        int sent = messageService.sendBatch(
                Arrays.asList("user1", "user2", "user3"),
                SseEvent.systemBroadcast("测试")
        );
        
        assertEquals(3, sent);
    }
}

1.2 集成测试

java 复制代码
package com.enterprise.sse.integration;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * SSE 集成测试
 */
@SpringBootTest
@AutoConfigureMockMvc
class SseIntegrationTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithMockUser(username = "testuser")
    void testSseConnect() throws Exception {
        mockMvc.perform(get("/api/sse/connect")
                .param("userId", "testuser"))
                .andExpect(status().isOk())
                .andExpect(header().string("Content-Type", "text/event-stream"));
    }
    
    @Test
    void testSseConnectUnauthorized() throws Exception {
        mockMvc.perform(get("/api/sse/connect")
                .param("userId", "testuser"))
                .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(username = "testuser")
    void testJoinGroup() throws Exception {
        mockMvc.perform(post("/api/sse/group/test-group/join"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));
    }
    
    @Test
    @WithMockUser(username = "testuser")
    void testGetStats() throws Exception {
        mockMvc.perform(get("/api/sse/stats"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.activeConnections").exists());
    }
    
    @Test
    @WithMockUser(username = "testuser")
    void testGetOnlineUsers() throws Exception {
        mockMvc.perform(get("/api/sse/online-users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.users").isArray());
    }
}

1.3 压力测试

JMeter 测试计划
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
  <hashTree>
    <TestPlan>
      <stringProp name="TestPlan.comments">SSE 压力测试</stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
    </TestPlan>
    <hashTree>
      <!-- 线程组:模拟1000个并发连接 -->
      <ThreadGroup>
        <stringProp name="ThreadGroup.num_threads">1000</stringProp>
        <stringProp name="ThreadGroup.ramp_time">60</stringProp>
        <stringProp name="ThreadGroup.duration">300</stringProp>
      </ThreadGroup>
      <hashTree>
        <!-- HTTP 请求:建立 SSE 连接 -->
        <HTTPSamplerProxy>
          <stringProp name="HTTPSampler.domain">localhost</stringProp>
          <stringProp name="HTTPSampler.port">8080</stringProp>
          <stringProp name="HTTPSampler.path">/api/sse/connect</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
        </HTTPSamplerProxy>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
Gatling 压力测试脚本
scala 复制代码
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class SseLoadTest extends Simulation {
  
  val httpProtocol = http
    .baseUrl("http://localhost:8080")
    .acceptHeader("text/event-stream")
    .basicAuth("admin", "admin123")
  
  val scn = scenario("SSE Load Test")
    .exec(
      sse("Connect").get("/api/sse/connect?userId=${userId}")
        .await(300.seconds)(
          sse.checkMessage("check").check(regex(".*"))
        )
    )
  
  setUp(
    scn.inject(
      rampUsers(1000) during (60.seconds)
    ).protocols(httpProtocol)
  )
}

1.4 性能测试报告模板

markdown 复制代码
# SSE 性能测试报告

## 测试环境
- 服务器: AWS EC2 t3.large (2 vCPU, 8GB RAM)
- 数据库: Redis 6.2
- 网络: 1Gbps

## 测试场景
- 并发连接数: 5000
- 测试时长: 5分钟
- 消息推送频率: 1条/秒/连接

## 测试结果

| 指标 | 数值 |
|------|------|
| 最大并发连接数 | 5000 |
| 平均响应时间 | 45ms |
| 95% 响应时间 | 120ms |
| 99% 响应时间 | 250ms |
| 错误率 | 0.01% |
| CPU 使用率 | 65% |
| 内存使用率 | 4.2GB |

## 结论
系统在5000并发连接下运行稳定,满足生产环境要求。

2. Docker 部署

2.1 Dockerfile

dockerfile 复制代码
FROM eclipse-temurin:17-jre-alpine

# 设置工作目录
WORKDIR /app

# 安装必要工具
RUN apk add --no-cache curl

# 复制 JAR 文件
COPY target/sse-demo-1.0.0.jar app.jar

# 创建日志目录
RUN mkdir -p /app/logs

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

# 暴露端口
EXPOSE 8080

# JVM 参数优化
ENV JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 启动应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

2.2 docker-compose.yml

yaml 复制代码
version: '3.8'

services:
  # Redis
  redis:
    image: redis:7-alpine
    container_name: sse-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    networks:
      - sse-network

  # SSE 应用
  sse-app:
    build: .
    container_name: sse-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - SERVER_PORT=8080
    depends_on:
      redis:
        condition: service_healthy
    volumes:
      - ./logs:/app/logs
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - sse-network
    restart: unless-stopped

  # Prometheus
  prometheus:
    image: prom/prometheus:latest
    container_name: sse-prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    networks:
      - sse-network

  # Grafana
  grafana:
    image: grafana/grafana:latest
    container_name: sse-grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on:
      - prometheus
    networks:
      - sse-network

networks:
  sse-network:
    driver: bridge

volumes:
  redis-data:
  prometheus-data:
  grafana-data:

2.3 Prometheus 配置

yaml 复制代码
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'sse-app'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['sse-app:8080']

3. Kubernetes 部署

3.1 Deployment

yaml 复制代码
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sse-service
  labels:
    app: sse-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: sse-service
  template:
    metadata:
      labels:
        app: sse-service
    spec:
      containers:
      - name: sse-service
        image: your-registry/sse-service:1.0.0
        ports:
        - containerPort: 8080
          name: http
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: "prod"
        - name: REDIS_HOST
          value: "redis-service"
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: redis-secret
              key: password
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "2000m"
        livenessProbe:
          httpGet:
            path: /actuator/health/liveness
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        volumeMounts:
        - name: logs
          mountPath: /app/logs
      volumes:
      - name: logs
        emptyDir: {}

3.2 Service

yaml 复制代码
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sse-service
spec:
  type: ClusterIP
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800  # 3小时
  ports:
  - port: 8080
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: sse-service

3.3 Ingress

yaml 复制代码
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: sse-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "86400"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "sse-route"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "10800"
spec:
  ingressClassName: nginx
  rules:
  - host: sse.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: sse-service
            port:
              number: 8080
  tls:
  - hosts:
    - sse.example.com
    secretName: sse-tls-secret

3.4 ConfigMap

yaml 复制代码
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: sse-config
data:
  application.yml: |
    spring:
      application:
        name: sse-service
      data:
        redis:
          host: ${REDIS_HOST}
          port: 6379
    sse:
      connection:
        max-per-user: 3
        global-limit: 10000
        timeout-seconds: 3600
      heartbeat:
        enabled: true
        interval-seconds: 30

3.5 HorizontalPodAutoscaler

yaml 复制代码
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: sse-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: sse-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15

4. Nginx 负载均衡配置

4.1 完整 Nginx 配置

nginx 复制代码
# nginx.conf
upstream sse_backend {
    # IP Hash 保证粘性会话
    ip_hash;
    
    server 192.168.1.101:8080 max_fails=3 fail_timeout=30s weight=1;
    server 192.168.1.102:8080 max_fails=3 fail_timeout=30s weight=1;
    server 192.168.1.103:8080 max_fails=3 fail_timeout=30s weight=1;
    
    keepalive 32;
}

server {
    listen 80;
    server_name sse.example.com;
    
    # 强制 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name sse.example.com;
    
    # SSL 证书
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    # 日志
    access_log /var/log/nginx/sse-access.log;
    error_log /var/log/nginx/sse-error.log;
    
    # SSE 连接端点
    location /api/sse/connect {
        proxy_pass http://sse_backend;
        
        # SSE 必需配置
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        proxy_buffering off;
        proxy_cache off;
        
        # 超时配置
        proxy_read_timeout 24h;
        proxy_connect_timeout 5s;
        proxy_send_timeout 24h;
        
        # 传递客户端信息
        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;
        
        # 禁用压缩
        proxy_set_header Accept-Encoding "";
        
        # 关闭代理缓冲
        proxy_set_header X-Accel-Buffering no;
    }
    
    # 其他 API 端点
    location /api/ {
        proxy_pass http://sse_backend;
        
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;
        
        # 标准超时
        proxy_read_timeout 60s;
        proxy_connect_timeout 5s;
    }
    
    # 健康检查
    location /actuator/health {
        proxy_pass http://sse_backend;
        access_log off;
    }
    
    # 静态资源
    location /static/ {
        alias /var/www/sse/static/;
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
}

5. 监控和告警

5.1 Grafana Dashboard JSON

json 复制代码
{
  "dashboard": {
    "title": "SSE Service Monitoring",
    "panels": [
      {
        "title": "Active Connections",
        "targets": [
          {
            "expr": "sse_connections_created_total - sse_connections_closed_total"
          }
        ]
      },
      {
        "title": "Message Send Rate",
        "targets": [
          {
            "expr": "rate(sse_messages_sent_total[1m])"
          }
        ]
      },
      {
        "title": "Message Failure Rate",
        "targets": [
          {
            "expr": "rate(sse_messages_failed_total[1m]) / rate(sse_messages_sent_total[1m])"
          }
        ]
      }
    ]
  }
}

5.2 Prometheus 告警规则

yaml 复制代码
# alert-rules.yml
groups:
  - name: sse_alerts
    interval: 30s
    rules:
      # 连接数过高告警
      - alert: HighConnectionCount
        expr: sse_connections_created_total - sse_connections_closed_total > 8000
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "SSE连接数过高"
          description: "当前连接数 {{ $value }}, 超过阈值8000"
      
      # 消息发送失败率告警
      - alert: HighMessageFailureRate
        expr: rate(sse_messages_failed_total[5m]) / rate(sse_messages_sent_total[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "消息发送失败率过高"
          description: "失败率 {{ $value | humanizePercentage }}"
      
      # 服务不可用告警
      - alert: ServiceDown
        expr: up{job="sse-app"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "SSE服务不可用"
          description: "实例 {{ $labels.instance }} 已下线"

6. 常见问题排查

6.1 问题诊断清单

问题 检查项 解决方案
连接立即断开 防火墙、Nginx配置 检查端口开放,配置正确
消息未收到 Redis连接、订阅状态 验证Redis可用性
内存持续增长 僵尸连接、内存泄漏 启用定时清理任务
CPU使用率高 线程池、消息频率 调整线程池大小
网络延迟高 带宽、网络拓扑 优化网络配置

6.2 日志分析

bash 复制代码
# 查看错误日志
tail -f logs/sse-service.log | grep ERROR

# 统计连接数
grep "创建 SSE 连接" logs/sse-service.log | wc -l

# 查看最近的异常
grep -A 10 "Exception" logs/sse-service.log | tail -20

# 分析慢查询
grep "slow" logs/sse-service.log

相关推荐
梵得儿SHI12 小时前
(第十篇)Spring AI 核心技术攻坚全梳理:企业级能力矩阵 + 四大技术栈攻坚 + 性能优化 Checklist + 实战项目预告
java·人工智能·spring·rag·企业级ai应用·springai技术体系·多模态和安全防护
qq_2975746713 小时前
SpringBoot项目长时间未访问,Tomcat临时文件夹被删除?解决方案来了
spring boot·后端·tomcat
摇滚侠13 小时前
macbook shell 客户端推荐 Electerm macbook 版本下载链接
java·开发语言
一个有梦有戏的人13 小时前
Python3基础:函数基础,解锁模块化编程新技能
后端·python
程序员布吉岛13 小时前
Java 后端定时任务怎么选:@Scheduled、Quartz 还是 XXL-Job?(对比 + 避坑 + 选型)
java·开发语言
是阿楷啊13 小时前
Java大厂面试场景:音视频场景中的Spring Boot与微服务实战
spring boot·redis·spring cloud·微服务·grafana·prometheus·java面试
知无不研13 小时前
lambda表达式的原理和由来
java·开发语言·c++·lambda表达式
逍遥德13 小时前
Sring事务详解之02.如何使用编程式事务?
java·服务器·数据库·后端·sql·spring