Spring Boot WebFlux 实现流式数据传输与断点续传
今天我想分享一个基于Spring Boot WebFlux实现的流式数据传输解决方案,支持断点续传和会话恢复功能。
具体案例是前端AI大模型对话助手开发中,AI消息输出过程中,刷新页面,是怎么实现页面刷新后,对话还能够持续输出的
文章目录
- [Spring Boot WebFlux 实现流式数据传输与断点续传](#Spring Boot WebFlux 实现流式数据传输与断点续传)
-
- [1. 背景介绍](#1. 背景介绍)
- [2. 技术选型](#2. 技术选型)
- [3. 核心实现思路](#3. 核心实现思路)
-
- [3.1. 数据存储设计](#3.1. 数据存储设计)
- [3.2. 流式传输实现](#3.2. 流式传输实现)
- [3.3. 断点续传机制](#3.3. 断点续传机制)
- [3.4. 异步数据生成](#3.4. 异步数据生成)
- [4. 关键技术点解析](#4. 关键技术点解析)
-
- [4.1. Reactor的使用](#4.1. Reactor的使用)
- [4.2. 线程安全处理](#4.2. 线程安全处理)
- [4.3. 异常处理和资源回收](#4.3. 异常处理和资源回收)
- 5.使用示例
- [6. 完整代码解读](#6. 完整代码解读)
-
- [6.1. RedisStreamController](#6.1. RedisStreamController)
- [6.2. StreamService](#6.2. StreamService)
- [6.3. RedisStorageService](#6.3. RedisStorageService)
- [6.4. redisIndex.html(前端测试)](#6.4. redisIndex.html(前端测试))
- [6.5. 测试](#6.5. 测试)
- 7.总结
1. 背景介绍
在现代Web应用中,用户常面临响应延迟问题,尤其在处理大文件下载、AI生成式响应或实时数据推送等高负载场景时,传统同步请求-响应模式会导致明显的卡顿体验。流式传输技术通过分块逐步推送数据,有效提升了交互流畅性。然而,现有方案存在关键缺陷:页面刷新后,用户之前进行的AI对话上下文会丢失 ,需重新发起请求,这不仅浪费计算资源 (增加token消耗),更破坏了用户体验的连贯性。
针对这一痛点,本文将深入探讨如何基于Spring Boot WebFlux框架,构建支持断点续传的智能流式数据传输系统,实现页面刷新后无缝续传对话内容,同时优化资源利用率。
2. 技术选型
- Spring Boot WebFlux: 响应式编程框架,支持非阻塞异步处理
- Reactor : 响应式编程库,提供
Flux和Mono等核心概念 - Redis: 作为数据存储,支持分布式部署
3. 核心实现思路

3.1. 数据存储设计
我们设计了一个基于Redis的数据存储结构,主要包括:
AI:PRODUCER:LIST:{sessionId}: 存储已生成的数据片段列表AI:HISTORY:PROGRESS:{sessionId}: 记录客户端已接收的数据进度AI:PRODUCER:COMPLETED:{sessionId}: 标记数据生成是否完成AI:HISTORY:COMPLETED:{sessionId}: 标记客户端接收是否完成
3.2. 流式传输实现
java
@Override
public Flux<Object> streamText(String sessionId) {
// 初始化历史数据和索引
StreamContext context = initializeStreamContext(sessionId);
// 1. 获取历史响应片段(页面刷新后先返回)
Flux<Object> historyFlux = Flux.fromIterable(context.historyList);
// 2. 若响应已完成,仅返回历史
if (redisStorageService.isHistoryCompleted(sessionId)) {
log.debug("Session {} 已完成,直接返回历史数据", sessionId);
return historyFlux;
}
// 3. 如果还没有数据,则启动内容生成
if (redisStorageService.getResponse(sessionId, 0) == null) {
log.debug("Session {} 开始生成新内容", sessionId);
createContent(sessionId);
}
// 4. 创建实时数据流
Flux<Object> liveFlux = createLiveStream(sessionId, context.lastIndex);
// 5. 合并历史和实时流式响应
return Flux.concat(historyFlux, liveFlux);
}
3.3. 断点续传机制
通过记录客户端接收进度和数据生成进度,我们可以实现断点续传:
java
private StreamContext initializeStreamContext(String sessionId) {
StreamContext context = new StreamContext();
Integer progress = redisStorageService.getHistoryProgress(sessionId);
if (progress != null) {
context.lastIndex = new AtomicInteger(progress + 1);
context.historyList = redisStorageService.getHistoryResponse(sessionId);
} else {
context.lastIndex = new AtomicInteger(0);
context.historyList = new ArrayList<>();
}
return context;
}
3.4. 异步数据生成
为了避免阻塞主线程,我们使用Reactor的调度器来异步生成数据:
java
@Override
public void createContent(String sessionId) {
// 使用Reactor调度器而不是手动创建线程 模拟数据是AI对话生成
Schedulers.boundedElastic().schedule(() -> {
try {
Random random = new Random();
AtomicInteger currentPosition = new AtomicInteger(0);
// 使用while循环逐步生成内容
while (currentPosition.get() < FULL_TEXT.length()) {
try {
// 随机决定本次输出的字符数(2-5个)
int chunkSize = random.nextInt(4) + 2;
int endPosition = Math.min(currentPosition.get() + chunkSize, FULL_TEXT.length());
String chunk = FULL_TEXT.substring(currentPosition.get(), endPosition);
redisStorageService.saveResponse(sessionId, chunk);
// 更新位置
currentPosition.set(endPosition);
// 延迟模拟流式输出
Thread.sleep(OUTPUT_DELAY_MS);
} catch (Exception e) {
log.error("内容生成过程中发生错误,sessionId={}", sessionId, e);
redisStorageService.markProducerCompleted(sessionId);
break;
}
}
// 标记响应完成
log.info("Session {} 内容生成完成", sessionId);
redisStorageService.markProducerCompleted(sessionId);
} catch (Exception e) {
log.error("内容生成发生严重错误,sessionId={}", sessionId, e);
redisStorageService.saveResponse(sessionId, "服务器异常");
redisStorageService.markProducerCompleted(sessionId);
}
});
}
4. 关键技术点解析
4.1. Reactor的使用
我们使用Flux.create()方法创建自定义的响应式流:
java
private Flux<Object> createLiveStream(String sessionId, AtomicInteger lastIndex) {
return Flux.create(sink -> {
// 使用Reactor调度器而不是手动创建线程
Schedulers.boundedElastic().schedule(() -> {
pollAndEmitData(sessionId, lastIndex, sink);
});
// 注册事件处理器
registerEventHandlers(sink, sessionId);
}, FluxSink.OverflowStrategy.BUFFER);
}
4.2. 线程安全处理
通过使用AtomicInteger和Reactor的调度器,我们确保了多线程环境下的数据安全:
java
private void pollAndEmitData(String sessionId, AtomicInteger lastIndex, FluxSink<Object> sink) {
AtomicInteger newIndex = new AtomicInteger(lastIndex.get());
while (!Thread.currentThread().isInterrupted() && !sink.isCancelled()) {
try {
Object response = redisStorageService.getResponse(sessionId, newIndex.get());
if (response != null) {
// 推送新增的元素
sink.next(String.valueOf(response));
redisStorageService.saveHistoryProgress(sessionId, newIndex.get());
newIndex.incrementAndGet();
} else {
// 检查是否停止增长
if (redisStorageService.isProducerCompleted(sessionId)) {
handleCompletion(sessionId, sink);
break;
}
// 短暂休眠后继续轮询
Thread.sleep(POLLING_INTERVAL_MS);
}
} catch (InterruptedException e) {
log.warn("轮询线程被中断,sessionId={}", sessionId);
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("轮询过程中发生错误,sessionId={}", sessionId, e);
sink.error(e);
break;
}
}
}
4.3. 异常处理和资源回收
完善的异常处理机制确保系统稳定性:
java
private void registerEventHandlers(FluxSink<Object> sink, String sessionId) {
// 记录前端接收的进度
sink.onRequest(n -> {
Integer currentIndex = redisStorageService.getHistoryProgress(sessionId);
int newIndex = (currentIndex == null) ? Math.toIntExact(n) :
currentIndex + Math.toIntExact(n);
redisStorageService.saveHistoryProgress(sessionId, newIndex);
log.debug("更新进度,sessionId={}, newIndex={}", sessionId, newIndex);
});
// 处理取消事件
sink.onCancel(() -> log.info("Session {} 流式输出被取消", sessionId));
// 处理完成事件
sink.onDispose(() -> log.info("Session {} 流式输出结束", sessionId));
}
5.使用示例
前端可以通过简单的HTTP请求获取流式数据:
javascript
const eventSource = new EventSource('/redis/stream?sessionId=12345');
eventSource.onmessage = function(event) {
console.log('Received: ' + event.data);
};
6. 完整代码解读
6.1. RedisStreamController
java
import com.liuhm.service.StreamService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
/**
* Redis流控制器,提供基于Redis的文本流式输出功能,支持断点续传和会话恢复
*
* @author liuhaomin
* @since 2025/11/27
*/
@RestController
@RequestMapping("/redis")
@Slf4j
public class RedisStreamController {
@Autowired
private StreamService streamService;
/**
* 流式输出文本,支持断点续传
*
* @param sessionId 会话ID,用于标识不同的流式会话
* @return 文本流,包含历史数据和新生成的数据
*/
@GetMapping("/stream")
public Flux<Object> streamText(@RequestParam String sessionId) {
return streamService.streamText(sessionId);
}
}
6.2. StreamService
java
package com.liuhm.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.scheduler.Schedulers;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ClassName:StreamServiceImpl
* @Description: TODO
* @Author: liuhaomin
* @Date: 2025/11/28 14:01
*/
@Service
@Slf4j
public class StreamService {
@Autowired
private RedisStorageService redisStorageService;
private static final String FULL_TEXT = "在这个快速发展的数字时代,技术创新正在以前所未有的速度改变我们的生活和工作方式。"
+ "人工智能、大数据、云计算和物联网等前沿技术正在重塑各个行业,为企业和社会创造新的机遇和挑战。"
+ "从智能家居到自动驾驶,从远程医疗到虚拟现实,科技正在让我们的世界变得更加智能、高效和互联。"
+ "面对这些变革,我们需要不断学习和适应,才能在这个充满活力的时代中保持竞争力并把握未来发展的主动权。";
private static final long POLLING_INTERVAL_MS = 200L;
private static final long OUTPUT_DELAY_MS = 200L;
public Flux<Object> streamText(String sessionId) {
// 初始化历史数据和索引
StreamContext context = initializeStreamContext(sessionId);
// 1. 获取历史响应片段(页面刷新后先返回)
Flux<Object> historyFlux = Flux.fromIterable(context.historyList);
// 2. 若响应已完成,仅返回历史
if (redisStorageService.isHistoryCompleted(sessionId)) {
log.debug("Session {} 已完成,直接返回历史数据", sessionId);
return historyFlux;
}
// 3. 如果还没有数据,则启动内容生成
if (redisStorageService.getResponse(sessionId, 0) == null) {
log.debug("Session {} 开始生成新内容", sessionId);
context.lastIndex = new AtomicInteger(0);
context.historyList = new ArrayList<>();
historyFlux = Flux.fromIterable(context.historyList);
createContent(sessionId);
}
// 4. 创建实时数据流
Flux<Object> liveFlux = createLiveStream(sessionId, context.lastIndex);
// 5. 合并历史和实时流式响应
return Flux.concat(historyFlux, liveFlux);
}
public void createContent(String sessionId) {
// 使用Reactor调度器而不是手动创建线程 模拟数据是AI对话生成
Schedulers.boundedElastic().schedule(() -> {
try {
Random random = new Random();
AtomicInteger currentPosition = new AtomicInteger(0);
// 使用while循环逐步生成内容
while (currentPosition.get() < FULL_TEXT.length()) {
try {
// 随机决定本次输出的字符数(2-5个)
int chunkSize = random.nextInt(4) + 2;
int endPosition = Math.min(currentPosition.get() + chunkSize, FULL_TEXT.length());
String chunk = FULL_TEXT.substring(currentPosition.get(), endPosition);
redisStorageService.saveResponse(sessionId, chunk);
// 更新位置
currentPosition.set(endPosition);
// 延迟模拟流式输出
Thread.sleep(OUTPUT_DELAY_MS);
} catch (InterruptedException e) {
log.warn("内容生成线程被中断,sessionId={}", sessionId);
Thread.currentThread().interrupt();
redisStorageService.markProducerCompleted(sessionId);
break;
} catch (Exception e) {
log.error("内容生成过程中发生错误,sessionId={}", sessionId, e);
redisStorageService.markProducerCompleted(sessionId);
break;
}
}
// 标记响应完成
log.info("Session {} 内容生成完成", sessionId);
redisStorageService.markProducerCompleted(sessionId);
} catch (Exception e) {
log.error("内容生成发生严重错误,sessionId={}", sessionId, e);
redisStorageService.saveResponse(sessionId, "服务器异常");
redisStorageService.markProducerCompleted(sessionId);
}
});
}
/**
* 初始化流上下文,包括历史数据和起始索引
*
* @param sessionId 会话ID
* @return 流上下文对象
*/
private StreamContext initializeStreamContext(String sessionId) {
StreamContext context = new StreamContext();
Integer progress = redisStorageService.getHistoryProgress(sessionId);
if (progress != null) {
context.lastIndex = new AtomicInteger(progress + 1);
context.historyList = redisStorageService.getHistoryResponse(sessionId);
} else {
context.lastIndex = new AtomicInteger(0);
context.historyList = new ArrayList<>();
}
log.debug("初始化流上下文,sessionId={}, progress={}, historySize={}",
sessionId, progress, context.historyList.size());
return context;
}
/**
* 创建实时数据流
*
* @param sessionId 会话ID
* @param lastIndex 最后处理的索引
* @return 实时数据流
*/
private Flux<Object> createLiveStream(String sessionId, AtomicInteger lastIndex) {
return Flux.create(sink -> {
// 使用Reactor调度器而不是手动创建线程
Schedulers.boundedElastic().schedule(() -> {
pollAndEmitData(sessionId, lastIndex, sink);
});
// 注册事件处理器
registerEventHandlers(sink, sessionId);
}, FluxSink.OverflowStrategy.BUFFER);
}
/**
* 轮询并发送数据
*
* @param sessionId 会话ID
* @param lastIndex 最后处理的索引
* @param sink Flux Sink
*/
private void pollAndEmitData(String sessionId, AtomicInteger lastIndex, FluxSink<Object> sink) {
AtomicInteger newIndex = new AtomicInteger(lastIndex.get());
while (!Thread.currentThread().isInterrupted() && !sink.isCancelled()) {
try {
Object response = redisStorageService.getResponse(sessionId, newIndex.get());
if (response != null) {
// 推送新增的元素
sink.next(String.valueOf(response));
redisStorageService.saveHistoryProgress(sessionId, newIndex.get());
newIndex.incrementAndGet();
} else {
// 检查是否停止增长
if (redisStorageService.isProducerCompleted(sessionId)) {
handleCompletion(sessionId, sink);
break;
}
// 短暂休眠后继续轮询
Thread.sleep(POLLING_INTERVAL_MS);
}
} catch (InterruptedException e) {
log.warn("轮询线程被中断,sessionId={}", sessionId);
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("轮询过程中发生错误,sessionId={}", sessionId, e);
sink.error(e);
break;
}
}
}
/**
* 处理流完成逻辑
*
* @param sessionId 会话ID
* @param sink Flux Sink
*/
private void handleCompletion(String sessionId, FluxSink<Object> sink) {
log.info("Session {} 列表已停止增长,输出结束!", sessionId);
redisStorageService.markHistoryCompleted(sessionId);
redisStorageService.saveHistoryProgress(sessionId, redisStorageService.getProducerSize(sessionId));
sink.complete();
}
/**
* 注册Flux事件处理器
*
* @param sink Flux Sink
* @param sessionId 会话ID
*/
private void registerEventHandlers(FluxSink<Object> sink, String sessionId) {
// 记录前端接收的进度
sink.onRequest(n -> {
/* Integer currentIndex = redisStorageService.getHistoryProgress(sessionId);
int newIndex = (currentIndex == null) ? Math.toIntExact(n) :
currentIndex + Math.toIntExact(n);
redisStorageService.saveHistoryProgress(sessionId, newIndex);
log.debug("更新进度,sessionId={}, newIndex={}", sessionId, newIndex);*/
});
// 处理取消事件
sink.onCancel(() -> log.info("Session {} 流式输出被取消", sessionId));
// 处理完成事件
sink.onDispose(() -> log.info("Session {} 流式输出结束", sessionId));
}
/**
* 流上下文,封装流处理所需的上下文信息
*/
private static class StreamContext {
List<Object> historyList;
AtomicInteger lastIndex;
}
}
6.3. RedisStorageService
java
package com.liuhm.service;
import com.liuhm.vo.ChatResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
/**
* @ClassName:AIResponseStorageService
* @Description: Redis存储服务,用于管理AI响应的流式数据
* @Author: liuhaomin
* @Date: 2025/11/27 11:02
*/
@Service
public class RedisStorageService {
@Autowired
private RedisTemplate redisTemplate;
// 统一管理Redis键前缀
private static final String PREFIX = "AI:";
private static final String HISTORY_PROGRESS_KEY = PREFIX + "HISTORY:PROGRESS:";
private static final String HISTORY_COMPLETED_KEY = PREFIX + "HISTORY:COMPLETED:";
private static final String PRODUCER_LIST_KEY = PREFIX + "PRODUCER:LIST:";
private static final String PRODUCER_COMPLETED_KEY = PREFIX + "PRODUCER:COMPLETED:";
// 统一过期时间配置
private static final Duration DEFAULT_EXPIRE_TIME = Duration.ofSeconds(60);
/**
* 构建Redis键
* @param baseKey 基础键
* @param sessionId 会话ID
* @return 完整的Redis键
*/
private String buildKey(String baseKey, String sessionId) {
return baseKey + sessionId;
}
/**
* 设置键的过期时间
* @param key Redis键
*/
private void setExpire(String key) {
redisTemplate.expire(key, DEFAULT_EXPIRE_TIME);
}
/**
* 保存AI响应片段(String类型)
* @param sessionId 会话ID
* @param response 响应片段
*/
public void saveResponse(String sessionId, Object response) {
String key = buildKey(PRODUCER_LIST_KEY, sessionId);
redisTemplate.opsForList().rightPush(key, response);
setExpire(key);
}
/**
* 保存历史进度
* @param sessionId 会话ID
* @param index 进度索引
*/
public void saveHistoryProgress(String sessionId, Integer index) {
String key = buildKey(HISTORY_PROGRESS_KEY, sessionId);
redisTemplate.opsForValue().set(key, index);
setExpire(key);
}
/**
* 获取历史进度
* @param sessionId 会话ID
* @return 进度索引
*/
public Integer getHistoryProgress(String sessionId) {
String key = buildKey(HISTORY_PROGRESS_KEY, sessionId);
return (Integer) redisTemplate.opsForValue().get(key);
}
/**
* 获取生产者队列大小
* @param sessionId 会话ID
* @return 队列大小
*/
public Integer getProducerSize(String sessionId) {
String key = buildKey(PRODUCER_LIST_KEY, sessionId);
Long size = redisTemplate.opsForList().size(key);
return size != null ? Math.toIntExact(size) : 0;
}
/**
* 获取历史响应片段
* @param sessionId 会话ID
* @return 响应片段列表
*/
public List<Object> getHistoryResponse(String sessionId) {
try {
String key = buildKey(PRODUCER_LIST_KEY, sessionId);
Integer historyProgress = getHistoryProgress(sessionId);
// 如果没有设置进度,则获取所有元素
if (historyProgress == null) {
Long size = redisTemplate.opsForList().size(key);
historyProgress = size != null ? Math.toIntExact(size) - 1 : -1;
}
return redisTemplate.opsForList().range(key, 0, historyProgress);
} catch (Exception e) {
// 发生异常时返回空列表而不是null
return Collections.emptyList();
}
}
/**
* 根据索引获取响应
* @param sessionId 会话ID
* @param startIndex 索引位置
* @return 响应对象
*/
public Object getResponse(String sessionId, Integer startIndex) {
String key = buildKey(PRODUCER_LIST_KEY, sessionId);
return redisTemplate.opsForList().index(key, startIndex);
}
/**
* 标记生产者完成状态
* @param sessionId 会话ID
*/
public void markProducerCompleted(String sessionId) {
String key = buildKey(PRODUCER_COMPLETED_KEY, sessionId);
redisTemplate.opsForValue().set(key, true);
setExpire(key);
}
/**
* 标记历史完成状态
* @param sessionId 会话ID
*/
public void markHistoryCompleted(String sessionId) {
String key = buildKey(HISTORY_COMPLETED_KEY, sessionId);
redisTemplate.opsForValue().set(key, true);
setExpire(key);
}
/**
* 检查生产者是否完成
* @param sessionId 会话ID
* @return 是否完成
*/
public boolean isProducerCompleted(String sessionId) {
String key = buildKey(PRODUCER_COMPLETED_KEY, sessionId);
return Boolean.TRUE.equals(redisTemplate.opsForValue().get(key));
}
/**
* 检查历史是否完成
* @param sessionId 会话ID
* @return 是否完成
*/
public boolean isHistoryCompleted(String sessionId) {
String key = buildKey(HISTORY_COMPLETED_KEY, sessionId);
return Boolean.TRUE.equals(redisTemplate.opsForValue().get(key));
}
}
6.4. redisIndex.html(前端测试)
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Stream请求展示历史+续传</title>
</head>
<body>
<div id="content" style="width: 800px; height: 500px; border: 1px solid #ccc; padding: 10px; overflow-y: auto;"></div>
<script>
// 会话ID持久化
let sessionId = localStorage.getItem('flux_session');
if (!sessionId) {
sessionId = 'session_' + Date.now();
localStorage.setItem('flux_session', sessionId);
}
const contentDiv = document.getElementById('content');
// 单一SSE连接:接收历史+新内容
function connectStream() {
const eventSource = new EventSource(`http://localhost:8080/redis/stream?sessionId=${sessionId}`);
// 接收所有推送内容(历史+新内容)
eventSource.onmessage = function(e) {
contentDiv.innerHTML += e.data ;
contentDiv.scrollTop = contentDiv.scrollHeight; // 滚动到底部
};
// 断开重连(异常处理)
eventSource.onerror = function() {
eventSource.close();
// setTimeout(connectStream, 1000);
};
}
// 页面加载时启动单一Stream连接
window.onload = connectStream;
</script>
</body>
</html>
6.5. 测试
启动服务,访问redisIndex.html,页面多次刷新,也会持续输出内容


7.总结
通过以上实现,我们构建了一个功能完整的流式数据传输系统,具有以下特点:
- 支持断点续传: 用户刷新页面后可以从上次中断位置继续接收数据
- 异步非阻塞: 使用Reactor和WebFlux实现高性能响应式处理
- 线程安全: 通过原子操作和调度器确保并发安全性
- 易于扩展: 模块化设计便于功能扩展和维护
这套方案已经在实际项目中得到验证,能够有效提升用户体验和系统性能。希望对大家在构建流式传输系统时有所帮助。
下面的deepseek4j-flux