Spring Webflux下载GCS Blob时用flatMap+Mono还是直接使用map?
第一种写法是符合规范的正确实现,第二种确实属于冗余写法,你对flatMap的适用场景存在部分误解。
具体原因解释
第一种写法的合理性:
你已经通过runOn(Schedulers.boundedElastic())将包含阻塞IO逻辑的blob.getContent()操作切换到了专门适配阻塞调用的线程池执行,完全不会占用Webflux的事件循环线程,这也是Blockhound没有报阻塞调用的原因。map操作本身就是用来处理同步转换逻辑的,只要内部的同步/阻塞操作已经放在了正确的调度器上,用法完全合规。第二种写法的冗余点:
flatMap(blob -> Mono.just(blob.getContent(...)))和直接用map没有任何功能性差异:Mono.just()是立刻求值的,你写在lambda内部的blob.getContent()还是会在当前调度器线程上立刻执行,没有额外的异步收益,反而多了一层Publisher包装、拆箱的性能开销,完全没有必要。什么时候才需要把阻塞IO放在flatMap中?
只有当你需要为每个单独的阻塞操作指定调度器、没有提前通过runOn统一切换线程池的时候,才需要用flatMap封装,正确写法如下:.flatMap(blob -> Mono.fromCallable(() -> blob.getContent(BlobSourceOption.generationMatch())) .subscribeOn(Schedulers.boundedElastic()) )这种写法和你第一种用
runOn统一切换调度器的写法是等价的,只是适用场景不同:如果所有阻塞操作都属于同一类型、用同一个调度器,用runOn统一切换的写法更简洁。
额外优化建议
你当前代码中storage.list(BUCKET_NAME)也是同步阻塞的GCS调用,这段逻辑是在方法调用线程上直接执行的,如果这个方法是在事件循环线程上被触发,也会存在阻塞风险,建议把这部分也封装进响应式链:
public Flux<String> getImages() { long startMillis = System.currentTimeMillis(); return Mono.fromCallable(() -> { final Storage storage = StorageOptions.newBuilder().setProjectId(PROJECT_ID).build().getService(); return storage.list(BUCKET_NAME).iterateAll(); }) .subscribeOn(Schedulers.boundedElastic()) .flatMapMany(Flux::fromIterable) .parallel() .runOn(Schedulers.boundedElastic()) .map(blob -> blob.getContent(BlobSourceOption.generationMatch())) .map(PhotoUtils::processBytesToBase64) .sequential() .doOnTerminate(() -> LOGGER.info("ELAPSED {}", System.currentTimeMillis() - startMillis)); }
内容的提问来源于stack exchange,提问作者chd

