去年在重构公司实时数据推送系统时,我面临一个关键挑战:如何在不增加服务器负载的前提下,实现高并发的实时数据推送。传统轮询方案不仅效率低下,还造成了大量资源浪费。经过多轮技术选型,最终选择了Spring Boot + WebClient + SSE的组合方案,成功将服务器资源消耗降低了73%,同时保证了消息的实时性。
SSE(Server-Sent Events)本质上是一种基于HTTP的长连接技术,它允许服务端主动向客户端推送数据。与WebSocket相比,SSE具有以下优势:
我们的系统采用分层设计:
code复制客户端 → Spring MVC控制器 → 业务服务层 → 事件发布层 → WebClient → 第三方服务
关键组件交互流程:
text/event-stream响应创建高性能WebClient实例的推荐配置:
java复制@Bean
public WebClient webClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(10))
)
))
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
关键参数说明:
connectTimeoutMillis:连接超时设为5秒ReadTimeoutHandler:读超时10秒控制器典型实现:
java复制@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamData() {
return eventPublisher
.publishOn(Schedulers.boundedElastic())
.map(event -> ServerSentEvent.builder(event.getData())
.id(UUID.randomUUID().toString())
.event("message")
.build())
.timeout(Duration.ofMinutes(30))
.onErrorResume(e -> {
log.error("Stream error", e);
return Flux.empty();
});
}
关键注意事项:
produces = MediaType.TEXT_EVENT_STREAM_VALUEpublishOn指定调度器消费第三方SSE流的正确姿势:
java复制public Flux<String> consumeSSEStream() {
return webClient.get()
.uri("/external/stream")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
.doOnSubscribe(sub -> log.info("开始订阅流"))
.doOnComplete(() -> log.info("流结束"));
}
性能优化技巧:
retryWhen实现指数退避重试doOn*方法添加监控点bufferTimeout减少小包传输我们在生产环境中总结的连接管理最佳实践:
| 场景 | 策略 | 参数建议 |
|---|---|---|
| 高并发 | 连接池 | maxConnections=500 |
| 长连接 | 心跳检测 | heartbeatInterval=30s |
| 不稳定网络 | 重试机制 | maxAttempts=3, backoff=1s |
| 大数据量 | 分块传输 | chunkSize=8KB |
必须监控的关键指标:
reactor.netty.connections.activereactor.netty.http.client.durationreactor.netty.errors.countreactor.core.publisher.flux.requested推荐Prometheus配置示例:
yaml复制- pattern: reactor.netty.http.client.<name>.<tag>
name: "http_client_$name"
labels:
uri: "$tag{uri}"
症状:客户端频繁重连,日志中出现Connection prematurely closed错误
解决方案:
java复制Flux.interval(Duration.ofSeconds(30))
.map(i -> ServerSentEvent.builder().comment("hb").build())
预防内存泄漏的关键措施:
limitRate限制code复制-XX:NativeMemoryTracking=detail
-XX:+UnlockDiagnosticVMOptions
我们在4核8G的实例上进行的测试结果:
| 并发数 | 平均延迟 | 99分位延迟 | 内存占用 |
|---|---|---|---|
| 100 | 23ms | 45ms | 1.2GB |
| 500 | 47ms | 112ms | 2.8GB |
| 1000 | 89ms | 215ms | 4.5GB |
| 5000 | 312ms | 628ms | OOM风险 |
关键发现:
对于文本型事件,启用GZIP压缩可减少50%以上带宽:
java复制@Bean
public WebClient webClientWithCompression() {
return WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true)
)
.build())
.filter(ExchangeFilterFunctions.contentDecompressor())
.build();
}
扩展SSE协议支持二进制数据:
java复制public Flux<ServerSentEvent<byte[]>> streamBinary() {
return dataService.getBinaryStream()
.map(data -> ServerSentEvent.builder(data)
.event("binary")
.id(Base64.getEncoder().encodeToString(
DigestUtils.md5(data)))
.build());
}
客户端解码示例:
javascript复制eventSource.addEventListener('binary', e => {
const binaryData = atob(e.data);
// 处理二进制数据
});
必须实施的五大安全策略:
java复制@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("https://trusted.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/stream/**", config);
return new CorsFilter(source);
}
健壮的客户端代码应该包含:
javascript复制const eventSource = new EventSource('/stream');
let reconnectAttempts = 0;
eventSource.onopen = () => {
reconnectAttempts = 0;
};
eventSource.onerror = () => {
const delay = Math.min(++reconnectAttempts * 1000, 5000);
setTimeout(() => {
eventSource = new EventSource('/stream');
}, delay);
};
eventSource.addEventListener('message', (e) => {
try {
const data = JSON.parse(e.data);
// 处理业务逻辑
} catch (err) {
console.error('解析错误', err);
}
});
kotlin复制val sseClient = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS)
.build()
随着业务量增长,我们逐步演进出的分层架构:
关键演进点: