Spring Boot WebFlux 实现流式数据传输与断点续传

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 : 响应式编程库,提供FluxMono等核心概念
  • Redis: 作为数据存储,支持分布式部署

3. 核心实现思路

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=.%2F![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/4307ce69813649a5b33abd4251505146.png)

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. 线程安全处理

通过使用AtomicIntegerReactor的调度器,我们确保了多线程环境下的数据安全:

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.总结

通过以上实现,我们构建了一个功能完整的流式数据传输系统,具有以下特点:

  1. 支持断点续传: 用户刷新页面后可以从上次中断位置继续接收数据
  2. 异步非阻塞: 使用Reactor和WebFlux实现高性能响应式处理
  3. 线程安全: 通过原子操作和调度器确保并发安全性
  4. 易于扩展: 模块化设计便于功能扩展和维护

这套方案已经在实际项目中得到验证,能够有效提升用户体验和系统性能。希望对大家在构建流式传输系统时有所帮助。

博客地址

代码下载

下面的deepseek4j-flux

相关推荐
没有bug.的程序员2 小时前
微服务中的数据一致性困局
java·jvm·微服务·架构·wpf·电商
鸽鸽程序猿2 小时前
【Redis】Java客户端使用Redis
java·redis·github
悦悦子a啊2 小时前
使用 Java 集合类中的 LinkedList 模拟栈以此判断字符串是否是回文
java·开发语言
Lucky小小吴2 小时前
java代码审计入门篇——Hello-Java-Sec(完结)
java·开发语言
一个想打拳的程序员2 小时前
无需复杂配置!用%20docker-webtop%20打造跨设备通用%20Linux%20桌面,加载cpolar远程访问就这么简单
java·人工智能·docker·容器
一起养小猫2 小时前
LeetCode100天Day2-验证回文串与接雨水
java·leetcode
清晓粼溪2 小时前
Java登录认证解决方案
java·开发语言
小徐Chao努力2 小时前
Go语言核心知识点底层原理教程【变量、类型与常量】
开发语言·后端·golang
锥锋骚年2 小时前
go语言异常处理方案
开发语言·后端·golang