You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Spring缓存含StreamingOutput的REST服务的技术问询

如何给返回StreamingOutput的REST服务实现Spring缓存

我之前处理过类似的场景,直接给@Cacheable加在返回Response的方法上绝对会踩坑——因为StreamingOutput里的输出流是一次性消费的,缓存之后再调用接口,流早就被关闭或者数据已经被写过了,根本没法正常返回数据。下面给你几个实用的解决方案,你可以根据自己的业务场景来选:

方案一:缓存原始数据而非Response/StreamingOutput

这是最稳妥、最容易维护的方案。核心思路是:缓存要流式输出的原始业务数据(比如你的区域列表),而不是缓存已经包装好的Response或StreamingOutput。每次请求时,从缓存中取出原始数据,再重新包装成新的StreamingOutput和Response。

这样做的好处是,每次请求都会生成全新的输出流,完全避免了流重复读取的问题,同时也达到了缓存数据、减少数据源查询的目的。

代码示例如下:

// 单独抽离出获取原始数据的方法,给这个方法加缓存注解
@Cacheable(value = "params", key = "#params")
public List<AdminArea> getAreasRawData(String params) {
    // 这里写你原来获取数据的逻辑:比如从数据库查询、调用其他服务等
    return fetchAdminAreasFromDataSource(params);
}

// REST接口方法,不再直接缓存,而是调用缓存的方法生成流
public Response RetrieveAreas(String params) {
    // 从缓存获取原始数据
    List<AdminArea> areas = getAreasRawData(params);
    
    StreamingOutput adminAreaStream = new StreamingOutput() {
        ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
        
        @Override
        public void write(OutputStream output) throws IOException, WebApplicationException {
            // 这里可以复用你原来的循环写入逻辑,遍历areas逐个写入即可
            for (AdminArea area : areas) {
                ow.writeValue(output, area);
                // 如果需要分隔符(比如JSON数组),可以在这里添加逗号等分隔符
                output.write(",\n".getBytes(StandardCharsets.UTF_8));
            }
        }
    };
    
    return Response.ok(adminAreaStream).build();
}

方案二:缓存数据生成器(适合超大数据量场景)

如果你的原始数据量极大,直接缓存整个集合会占用过多内存,可以考虑缓存一个数据生成器(比如Supplier或Callable),每次需要输出流时,调用生成器获取数据并写入流。

这个方案本质上和方案一类似,但延迟了数据的加载时机,只有在真正需要写流的时候才会从数据源获取(或从缓存的生成器触发获取)数据。

代码示例:

@Cacheable(value = "params", key = "#params")
public Supplier<List<AdminArea>> getAreasDataSupplier(String params) {
    // 返回一个Supplier,只有调用get()时才会执行数据查询逻辑
    return () -> fetchAdminAreasFromDataSource(params);
}

public Response RetrieveAreas(String params) {
    Supplier<List<AdminArea>> areasSupplier = getAreasDataSupplier(params);
    
    StreamingOutput adminAreaStream = new StreamingOutput() {
        ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
        
        @Override
        public void write(OutputStream output) throws IOException, WebApplicationException {
            // 只有在写流时才会触发数据获取
            List<AdminArea> areas = areasSupplier.get();
            for (AdminArea area : areas) {
                ow.writeValue(output, area);
                output.write(",\n".getBytes(StandardCharsets.UTF_8));
            }
        }
    };
    
    return Response.ok(adminAreaStream).build();
}

不推荐的方案:直接缓存Response

如果你非要尝试直接缓存Response,需要自定义缓存的序列化和反序列化逻辑——因为默认的Spring缓存(比如ConcurrentMapCache、Redis缓存)无法正确处理StreamingOutput这种带状态的对象,即使序列化成功,反序列化后的StreamingOutput的输出流已经失效,根本无法正常使用。

这个方案复杂度极高,维护成本大,很容易引发各种奇怪的问题(比如流关闭、数据丢失),所以非常不推荐。

一些额外的注意事项

  • 缓存Key的正确性:确保@Cacheable的key能唯一标识不同的请求参数。如果params是复杂字符串或对象,建议用SpEL表达式明确指定key,比如@Cacheable(value = "params", key = "#params"),避免默认的key生成逻辑出现冲突。
  • 缓存更新与失效:当原始数据发生变化时,一定要用@CacheEvict清除对应缓存,或者用@CachePut更新缓存,避免返回过期数据。
  • 超大数据量的优化:如果数据量实在太大,缓存原始数据会占用过多内存,可以考虑用分布式缓存(比如Redis)配合高效的序列化方式(如Jackson2JsonRedisSerializer),或者拆分数据进行分段缓存。

内容的提问来源于stack exchange,提问作者Hiro Protagonist

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:10:29