ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从Servlet阻塞模型到WebFlux响应式:线程池打满背后的架构选择

从Servlet阻塞模型到WebFlux响应式:线程池打满背后的架构选择 1. 线程池被打满之后我才回头理解Servlet的阻塞模型1.1 一次事故复盘Tomcat 200个线程瞬间耗尽先说一件让我改变技术选型思路的事。前几年维护一个基于 Spring Boot 的旧系统Tomcat 配置了默认的max-threads200平时日均请求量也不算大系统一直很稳。直到有一天合作的供应商接口突然从平均 300ms 变成 15 秒超时结果那 200 个工作线程全部堵在等待上游返回的路上其它所有接口的响应从 30ms 直接飙到 10 秒以上监控面板一片红服务基本处于不可用状态。当时的应急操作很简单粗暴把 max-threads 调到 1000重启确实缓解了几个小时。但供应商接口再慢一次同样的故障就再来一遍。加线程只是把容量问题往后推并没有解决线程被阻塞这个本质。后来我把目光转向 Spring WebFlux才真正意识到传统 Servlet 模型的瓶颈不在于并发量本身而在于每个请求独占一个线程且这个线程大部分时间都在无意义的等待。那 Tomcat 线程池打满具体意味着什么每个工作线程一旦被分配到一个请求它会从头跟到尾从 Socket 读取请求行和请求头解析参数经过拦截器链进入 DispatcherServlet执行业务逻辑写出响应最后才把线程归还给线程池。问题就出在执行业务逻辑这一环节——只要里面有一次数据库查询、一次远程调用、一次文件读取而底层又是同步阻塞 IO线程就只能挂起在那里等内核把数据准备好再拷到用户态。这段时间 CPU 完全不参与但线程资源被白白占用了。在高并发 IO 密集场景下这种模型的天花板非常明显。1.2 阻塞到底发生在哪一层ServletInputStream的read()在等什么很多人把问题归结为 Tomcat 配置不当其实根源在 Servlet 规范本身。Servlet 的service()方法签名是同步的ServletRequest的输入流read()会阻塞当前线程直到读到数据或抛异常ServletResponse的write()同样如此。你写下的 Controller 方法从请求进入到响应返回天然是一个占着茅坑不拉屎的过程——真正耗时的是下游数据库、远程服务、缓存等而不是 Controller 里那几行代码。我说一个最直观的比喻传统 Servlet 就像银行柜台一个柜员从头到尾只服务一个客户。哪怕客户只是在那里填单子、打电话、左顾右盼柜员也只能等着。客户少的时候没问题体验还很好客户一多柜员的服务效率就成了硬天花板。响应式模型换了一套逻辑柜员不再一对一守着一个客户客户把资料递进去之后就去等候区坐着柜员立刻服务下一位等前一个客户的单子处理完了再通知他到指定窗口取结果。这样一来柜员不再绑定到某个客户而是绑定到事件——什么任务就绪了就去处理什么任务。这看起来是现代高并发服务的必然选择但代价是编程模型完全变了。下面我会从线程模型、API 风格、数据访问层一步步拆解Servlet 到 WebFlux 的差异到底有多大以及迁移过程中那些手册里不会写的问题。2. 响应式模型的核心机制少量线程如何撑起高并发2.1 事件循环从Node.js到NettyWebFlux站在了同一条路上WebFlux 的底层运行在 Netty 上而 Netty 的核心是一个叫 EventLoop 的东西。EventLoop 本质上是一个单线程的循环不断从队列里取出事件执行对应的回调。关键点在于这个线程不能阻塞一旦阻塞所有注册在这个 EventLoop 上的连接都会停摆。你可以把它理解成一个非常高效的办事大厅引导员。传统模式下每个引导员盯一个客户响应式模式下引导员把所有客户的事记下来谁的资料先准备好就先办谁的。引导员手里有一个事件队列每一个网络连接、每次数据到达、每个响应可写都是一个事件。这个模型在 Node.js 上已经被验证了很多年Netty 在 Java 生态里也早就用在了网关、RPC 框架中Spring WebFlux 只是把这个模型正式带进了 Web 开发层。WebFlux 默认配置下Netty 的 EventLoop 线程数是 CPU 核数的两倍。一台 8 核机器就只有 16 个 IO 线程却能撑起成千上万的并发连接。这个数量级听上去很夸张但原理上完全说得通绝大部分连接在某个时刻并没有数据传输它们的状态只是挂起。Servlet 模型里挂起也占用一条工作线程响应式模型里挂起的连接只是一个存放在内存里的对象不占用线程。2.2 Reactor的Publisher与Subscriber数据流不是返回结果要用好 WebFlux必须忘掉方法返回一个对象的思维。传统 Controller 里你写return userService.getUserById(id);结果拿到了一个User对象。WebFlux 里你写的是return userService.getUserById(id);但返回类型变成了MonoUser。Mono代表将来某个时刻会产生 0 到 1 个结果Flux代表将来会产生 0 到 N 个结果。这两个类型都实现了 Reactor 的Publisher接口。Publisher 是一种声明式的数据流你定义好数据从哪里来、经过哪些变换、最终怎么消费但这个定义在订阅发生之前不会执行任何逻辑。这个机制非常重要我见过很多初学者在Mono里打印日志结果发现根本没执行就是因为定义数据流和触发数据流是两件事。// 这一段代码不会真正执行任何查询 MonoUser userMono userRepository.findById(1L); // 必须调用 subscribe() 或者由框架完成订阅查询才会真正发生 userMono.subscribe(user - System.out.println(user.getName()));在 WebFlux 的 Controller 里返回的Mono或Flux会由框架完成订阅你不需要自己调subscribe。数据流经过一系列的 operatormap、flatMap、filter等变换最终把结果推给响应流。这里有个非常重要的细节背压。订阅者可以通过request(n)告诉发布者我一次只能处理 n 条数据发布者就得按这个节奏生产。这解决了传统模型中下游处理不过来上游还在拼命灌数据的问题。背压在 WebFlux 里是内建的你不用手动实现但理解它有助于解释为什么响应式系统对慢消费更友好。2.3 非阻塞IO在操作系统层的真相非阻塞 IO 并不是什么魔法它依赖的是操作系统提供的 IO 多路复用能力。Linux 上的epoll是典型代表一个线程可以同时监视成千上万个 Socket 描述符哪个 Socket 的数据到了内核就通知用户态程序然后程序只需要处理就绪的那些连接。Netty 封装了这些底层 API对外呈现统一的 Channel 抽象。当你在 WebFlux 里配置server.port8080Netty 会创建一个 boss EventLoop 专门接受新连接然后注册到 worker EventLoop 上。每一条连接的数据读写都是非阻塞的调用channel.read()时如果没有数据立刻返回不会把线程挂起数据到达后再通过回调通知上层应用。这也是为什么在响应式代码里绝对不能用Thread.sleep()、不能用阻塞队列、不能用 JDBC 同步查询——任何一个阻塞操作都会把 EventLoop 线程按死殃及所有注册在它上面的连接。所以非阻塞不只是一个框架特性它更是一种约束整个调用链上所有环节都得是非阻塞的。3. 三层对比WebFlux与Servlet到底差在哪3.1 线程模型一个请求一个线程 vs N个请求共享线程Servlet 容器采用的是一个请求一条线程模型。Tomcat 的 Acceptor 线程负责接收新连接然后从工作线程池里取一个线程来处理。后续整个请求生命周期内这条线程都是独占的不管它是真的在计算还是在等待 IO。WebFlux 的线程模型则完全不同。事件循环线程不会block等待某个 IO而是在 IO 事件就绪时被唤醒执行回调。一个请求从进入到响应可能会在不同的线程上执行不同的回调阶段——这就是为什么响应式编程中在哪个线程上执行是个让人头疼的问题。Reactor 提供了publishOn和subscribeOn来控制执行线程池userRepository.findById(id) .map(UserVO::from) .publishOn(Schedulers.boundedElastic()); // 把后续操作切换到弹性线程池这里补充一个重要知识点R2DBC 目前还无法像 JDBC 那样完全无阻塞所以在集成 R2DBC 之前很多人会使用blocking适配器把阻塞的 JDBC 调用包到一个独立线程池里。但block()方法在 WebFlux 的 IO 线程上是绝对禁止的一旦调用等于直接把事件循环卡死。如果非要兼容旧代码可以用subscribeOn(Schedulers.boundedElastic())把阻塞操作隔离到弹性线程池中。3.2 API形态相似的Controller本质不同的请求处理WebFlux 支持Controller、RequestMapping这组注解从代码表面看和 Spring MVC 几乎一样RestController RequestMapping(/users) public class UserController { GetMapping(/{id}) public MonoUser getUser(PathVariable Long id) { return userRepository.findById(id); } }但注解只是一种外观上的兼容底层处理链路已经换掉了。Spring MVC 里实际干活的是 DispatcherServletWebFlux 里干活的是 DispatcherHandler。这俩类名相似职责却不同后面我会单独拆。再说说新引入的函数式路由。当你需要在同一个路径的不同场景下做复杂路由时WebFlux 的RouterFunction会比注解更灵活Bean public RouterFunctionServerResponse routes(UserHandler handler) { return RouterFunctions.route() .GET(/users/{id}, handler::getUser) .POST(/users, handler::createUser) .build(); }这里的方法入参不是HttpServletRequest/HttpServletResponse而是ServerRequest/ServerResponse。ServerRequest是围绕响应式流重新设计的请求抽象请求体的读取返回Mono或Flux而不是一个已经解析好的InputStream。很多 Servlet 时代的老代码比如直接用request.getParameter()、request.getAttribute()的地方都无法直接迁移到 WebFlux 里。3.3 数据访问JDBC阻塞与R2DBC非阻塞带来的连锁反应数据访问层是迁移到 WebFlux 时最痛的一环。传统的 Spring Data JPA、MyBatis 都是基于 JDBC 的而 JDBC 从诞生起就是同步阻塞模型。在响应式世界里SQL 查询变得异步化才行所以 Spring 生态里出现了 R2DBCReactive Relational Database Connectivity。R2DBC 的 Repository 写法public interface UserRepository extends ReactiveCrudRepositoryUser, Long { FluxUser findByAgeGreaterThan(int age); }注意返回类型从ListUser变成了FluxUserfindById从OptionalUser变成了MonoUser。这套 API 和 Spring Data JPA 很像实际使用中坑不少。比如R2DBC 查询返回的是数据库的行流事务管理方式和 JDBC 完全不同在响应式编程里不能再靠ThreadLocal绑定事务连接而是用TransactionalOperator手工控制事务边界TransactionOperator tx TransactionOperator.create(connectionFactory); tx.execute(status - userRepository.deleteById(1L)) .thenMany(userRepository.findAll()) .subscribe();大多数项目里业务逻辑已经和事务深度耦合如果只是把数据层换成 R2DBC而业务层还写着try { ... } Transactional那套同步代码基本上要动整个架构。所以我的经验是只有当你真的需要高并发 IO 密集服务并且业务链路本身可以接受异步化改造时才值得动数据层。4. 请求处理链路拆解DispatcherServlet与DispatcherHandler4.1 DispatcherServlet的九大组件与请求流转先回忆一下 Servlet 里的核心分发器 DispatcherServlet。它是一个标准的HttpServlet在service()方法里接收请求然后委托给一组策略组件处理。Spring MVC 文档里常说的九大组件包括 HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver、LocaleResolver、ThemeResolver、MultipartResolver、FlashMapManager、RequestToViewNameTranslator。处理流程大致是请求路径匹配到某个 HandlerMapping → 拿到对应的 Handler 执行链 → 用 HandlerAdapter 执行 Controller 方法 → 如果有异常交给 HandlerExceptionResolver → 返回值经 ViewResolver 渲染 → 写出响应。这套链路的问题在于每个环节都是同步调用。比如 HandlerAdapter 调用 Controller 方法后必须等返回结果视图渲染必须等模型数据齐了才能做。DispatcherServlet 运行在 Servlet 容器内它的生命周期由容器负责。容器启动时创建 DispatcherServlet 实例并调用init()在这里 Spring 会初始化自身的 WebApplicationContext。4.2 WebFlux的DispatcherHandler唯一的核心入口换成了一组函数WebFlux 里没有servlet这个桥接层了请求直接由 Netty 接入然后转到DispatcherHandler。它只有一个handle(ServerWebExchange exchange)方法返回MonoVoidpublic class DispatcherHandler implements WebHandler { Override public MonoVoid handle(ServerWebExchange exchange) { // 依次交给 handlerMappings, handlerAdapters, handlerResultHandlers 处理 return Flux.fromIterable(handlerMappings) .concatMap(mapping - mapping.getHandler(exchange)) .next() .flatMap(handler - invokeHandler(exchange, handler)) .flatMap(result - handleResult(exchange, result)); } }看到没有handle()方法返回的是MonoVoid而不是直接生成一个HttpServletResponse。整套流程是异步的请求先被路由到某个 Handler执行逻辑时返回MonoObject结果处理器再异步写入响应。这就意味着请求线程在 handler 执行期间可以被释放去处理别的请求。这种设计第一个影响是拦截器机制变了。原来在 Servlet 里做登录校验的HandlerInterceptorpreHandle返回boolean的方式在响应式世界里行不通了因为登录校验本身可能就是异步的要查库、调接口。WebFlux 里对应的是WebFilter签名变成了public class AuthFilter implements WebFilter { Override public MonoVoid filter(ServerWebExchange exchange, WebFilterChain chain) { return checkToken(exchange.getRequest()) .flatMap(passed - passed ? chain.filter(exchange) : Mono.error(new UnauthorizedException())); } }第二个影响是ThreadLocal全面失效。Servlet 模型里同一个线程从头处理到尾用ThreadLocal存登录用户信息非常顺手。WebFlux 的同一个请求可能跨多个线程ThreadLocal根本传不过去。RequestContextHolder.currentRequestAttributes()这类工具方法也会在事件循环线程里直接抛异常。我见过比较典型的问题是团队把旧的BaseContext静态类带着走里面存用户信息结果线上并发后出现A请求的登录用户跑到了B请求日志里。正确做法是用ServerWebExchange的 attribute 或 Reactor 的Context传递请求级数据。4.3 两类ApplicationContextServlet WebApplicationContext与Root WebApplicationContext顺手提一个网上问得很多的点Servlet WebApplicationContext 和 Root WebApplicationContext 到底是什么关系传统 SSM 项目中ContextLoaderListener会创建一个 Root WebApplicationContext负责加载数据源、Service、DAO 等底层 beanDispatcherServlet 启动时会再创建一个 Servlet WebApplicationContext作为 Root 的子容器负责加载 Controller、HandlerMapping、ViewResolver 等 web 层 bean。子容器可以引用父容器的 bean但父容器看不到子容器的 bean。这个机制带来了很多经典问题。比如 Autowired 一个 Service 到 Controller 里没问题但如果你想在 Service 里注入 Controller 的 bean就会报找不到 bean 或者出现循环依赖。再比如用了 Spring Security 的人都知道这两个 context 的边界ContextLoaderListener加载的安全配置和 DispatcherServlet 加载的安全配置如果不一致极容易出现登录认证生效但授权不生效的灵异现象。WebFlux 简化了一个层级大多数情况下只有一个 ApplicationContext由WebFluxAutoConfiguration自动装配。所以从 Servlet 迁移过来时如果你还固执地在 web 层和 service 层之间划分父子的 bean 配置反而会把自己绕晕。Spring Boot 本身也在弱化父子容器的概念官方文档明确建议非必要不要自建多层 context。4.4 容器差异Tomcat vs Netty启动方式与部署方式都变了Servlet 应用通常打一个 WAR 包丢进 Tomcat 的 webapps 目录或者用 Spring Boot 内嵌 Tomcat 打成可执行 jar。WebFlux 默认使用 Netty没有 Servlet 容器的概念。这段差异最直接的影响体现在配置项上。原来写的server.tomcat.max-threads、server.tomcat.accept-count这些配置在 WebFlux 下都不生效了取而代之的是server.port8080 server.netty.connection-timeout5000 server.netty.max-initial-line-length4096如果你的代码里有对 Servlet API 的强引用比如自己写implements ServletContextListener、javax.servlet.http.HttpServlet在切换为 Netty 后这些类都要处理掉。很多人直接把 spring-boot-starter-web 替换成 webflux 后编译期报出一堆Cannot resolve symbol HttpServletRequest原因就是 Servlet API 的 jar 不在 classpath 了。5. 将Servlet项目迁往WebFlux一次真实的改造记录5.1 连接池、线程池与ThreadLocal第一批报错名单我在做某个老项目改造时第一轮代码 review 下来发现改动最大的不是 Controller而是各种基础设施代码。以下是出问题最高的几个点排名不分先后HttpServletRequestServlet 时代的 Controller 方法普遍直接注入它来取参数、读 header、拿 session。WebFlux 里这个参数没有了必须改成ServerWebExchange。ThreadLocal记录登录用户、记录 traceId 的工具类几乎全部踩雷。RestTemplateSpring 5 之后仍然可用但它是阻塞式的不能直接在事件循环线程里用。正确姿势是换WebClient。阻塞队列LinkedBlockingQueue的take()方法会把事件循环线程阻塞必须换Flux.create之类的响应式队列模型。// 旧代码 PostMapping(/upload) public Result upload(HttpServletRequest request) throws Exception { User loginUser UserContext.get(); // ThreadLocal MultipartFile file ... // request 里取文件 return service.upload(file, loginUser); } // 新代码 PostMapping(/upload) public MonoResult upload(ServerWebExchange exchange) throws Exception { User loginUser exchange.getAttribute(AuthConstants.LOGIN_USER); return exchange.getMultipartData() .flatMap(data - service.upload(data.getFirst(file), loginUser)); }5.2 SpringBootServletInitializer NoClassDefFoundError一个高频迁移报错网上有一个高频错误Error: 找不到或无法加载主类 org.jeecg.JeecgSystemApplication原因: java.lang.NoClassDefFoundError: org/springframework/boot/web/servlet/support/SpringBootServletInitializer。这个错误我在换依赖的时候也碰到过。场景是这样的项目原来基于spring-boot-starter-web主启动类继承了SpringBootServletInitializer用来支持打 WAR 包部署到外部容器。为了改造 WebFlux我把依赖换成了spring-boot-starter-webflux并且把启动类上的继承也删了但 IDE 里重启时报的就是上面这个错。追查下来根因是编译缓存和依赖残留。SpringBootServletInitializer这个类定义在 spring-boot-web 模块中当 WebFlux 依赖下没有 spring-boot-web 时JVM 加载启动类就找不到这个父类。还有一个可能你的项目里某个 jar 仍然间接依赖了spring-boot-starter-webMaven 把它拉进了 classpath但启动时对应的 servlet-api 版本冲突导致类加载失败。处理办法分两步。第一检查 porm 依赖树mvn dependency:tree -Dincludesorg.springframework.boot:spring-boot-starter-web如果发现 web 依赖依然在就用排除项把它剔除。第二清理 IDE 的缓存和本地的编译产物有时候target/classes里残留了旧的.class文件就会导致这种不可思议的 NoClassDefFoundError。最直接的是执行mvn clean然后重启 IDE。比如 pom 中这样处理dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupId*/groupId artifactId*/artifactId /exclusion /exclusions /dependency还有个相关点Eclipse 里创建基于 Maven 的 Servlet 项目时默认会勾选 Dynamic Web Module 3.1并生成org.eclipse.wst.common.component等文件。迁移到 WebFlux 后如果还保留这些 web facet 配置启动时会反复尝试找 Servlet 容器跟 Netty 抢端口也容易出现奇怪的类加载问题。我的建议是一刀切把 eclipse 的.settings和.classpath删掉用 Maven 重新导入项目。5.3 在响应式服务中调用大模型API接口正确的异步姿势最近很多人问怎么用 Java 后端调用大模型 API这个问题放到 Servlet 和 WebFlux 下会有完全不同的答案。在传统 Servlet 里一个请求打到你的服务你的服务再转发给大模型接口中间有一次接近秒级的等待。模型推理越快这种阻塞的影响越小但推理慢的时候Tomcat 线程直接被打光。WebFlux 里我们使用WebClient来调用大模型 API。它是一个基于 Reactor 的非阻塞 HTTP 客户端整个调用过程不会占用工作线程而是在异步回调里处理响应Service public class LlmService { private final WebClient webClient; public LlmService(WebClient.Builder builder) { this.webClient builder .baseUrl(https://api.llm.example.com) .defaultHeader(HttpHeaders.AUTHORIZATION, Bearer sk-xxxx) .build(); } public MonoString chat(String prompt) { MapString, Object body Map.of( model, your-model-name, messages, List.of(Map.of(role, user, content, prompt)), stream, false ); return webClient.post() .uri(/v1/chat/completions) .bodyValue(body) .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(30)) .onErrorResume(e - Mono.just(服务暂时不可用请稍后再试)); } }注意几点。第一timeout()一定要写不然当上游模型服务不返回时你的请求会一直挂在那里虽然不占线程但会占连接和内存。第二大模型的流式输出用FluxString接收是最舒服的直接对接 SSE 事件流前端就能实现打字机效果public FluxString chatStream(String prompt) { return webClient.post() .uri(/v1/chat/completions) .bodyValue(Map.of(model, your-model-name, stream, true)) .retrieve() .bodyToFlux(String.class) .map(s - s) .timeout(Duration.ofSeconds(60)); }第三Spring 也提供了RestClient这种同步式工具但它在 WebFlux 环境下同样不推荐因为底层仍是阻塞式 HTTP。你可以在 Controller 里注册一个Schedulers.boundedElastic()线程池专门跑阻塞任务但那样做是打了一个补丁不是响应式该有的样子。如果团队还没有掌握响应式编程为了接入一个大模型 API 就全量切 WebFlux性价比很低。5.4 周边组件过滤器、监听器、拦截器一个都不能少Servlet 时代常用的三件套在 WebFlux 里都有对应物但用法完全不同ServletFilter对应 WebFlux 的WebFilter。HandlerInterceptor对应WebFlux 拦截器但实现方式更推荐通过WebFilter或给 RouterFunction 添加过滤器。ServletContextListener、HttpSessionListener在 WebFlux 中没有直接等价物。WebFlux 有ApplicationListener可以监听ApplicationStartedEvent这类 Spring 事件但关于 Session 的监听就要换思路——WebFlux 的会话机制由WebSessionManager管理它基于响应式WebSession不再有 Servlet 的HttpSession。事务也是一个硬骨头。Transactional注解在 WebFlux 下不能直接使用于返回Mono/Flux的方法。Spring 官方提供了一个Transactional的响应式变体但要求你用 R2DBC 且通过TransactionalOperator手动控制。如果业务是先插入订单再扣库存再发消息这种强事务链改成响应式后的复杂度会显著上升。我的经验是如果一个链路上超过三个事务操作建议先保留 Servlet 模型或者用Transactional的替代方案比如在 service 层改用函数式封装。6. 选型判断什么时候上WebFlux什么时候留在Servlet6.1 适合WebFlux的场景特征IO密集、长连接、高并发我总结一下 WebFlux 真正适合的场景这些条件可以帮你规避盲目迁移大量长连接比如 WebSocket、SSE 推送、实时协作。单个请求需要串行调用多个上游服务比如网关、聚合服务、BFF 层。并发连接数很高但每个连接的 CPU 计算量不大典型的 IO 密集型场景。你需要非常精确地控制线程资源传统 Tomcat 的线程配置已经无法满足性能目标。在我负责过的项目里网关类服务切到 WebFlux 后收益是最明显的。网关本身不承载业务逻辑做的是路由、鉴权、限流、转发每一件都涉及 IO且请求量极大。用 WebFlux 重写后单实例支撑的并发连接数提升了一个量级GC 压力反而变小了因为线程少了上下文切换少了。6.2 不适合WebFlux的场景计算密集、团队经验不足时慎入响应式不是银弹。如果业务本身是 CPU 密集型——比如图像处理、规则引擎、大量数值计算——事件循环这套机制帮不上什么忙反而因为引入异步复杂度让代码更难维护。还有一个容易被忽略的问题响应式链路中任何一个环节不小心用了阻塞 API爽的是当前线程坑的是整个事件循环上所有请求。排查这类问题比排查普通死锁还痛苦因为故障现象是整体变慢、偶发超时没有明确的异常堆栈。团队经验也是很现实的门槛。WebFlux 的调试方式完全不同。传统 Servlet 的调用栈在 IDE 里层层可见断点打上去变量一目了然。响应式异步链路的断点经常跳到某个MonoXxxOperator内部你根本看不出业务逻辑和当前断点有什么关系。我见过有团队因为上线后几个诡异的并发问题排查了整整一周最后回滚到 Servlet 架构的事。如果你所在团队的成员对 CompletableFuture 都还比较生疏贸然上 WebFlux 风险很大。6.3 我的个人经验不要一上来就推翻旧架构如果项目是全新启动业务模型也天然适合异步我建议一步到位用 WebFlux。如果是已经跑了好几年的 Spring MVC 项目我不建议整体切换。更稳妥的办法是在同一个 Spring Boot 应用里混合使用对外提供的接口用 WebFlux 的 WebClient 调用新服务新服务内部用 WebFlux 部署老服务继续运行在 Servlet 容器里通过网关做协议转换。Spring Boot 也支持同时依赖 webflux 和 web 的混合模式但要显式指定主启动方式避免两边同时抢端口。我自己的体会是响应式编程的学习曲线不在 API而在思维。Mono和Flux的 operator 看文档就能学会难的是你在写业务代码时能时刻意识到这段代码最终会在哪个线程上执行、这个副作用是响应式安全的吗。如果你能把这两个问题想清楚WebFlux 会让你看到一个完全不同的高并发世界。最后分享一个排查小技巧遇到 WebFlux 下的诡异超时第一件事不是怀疑框架而是全局搜代码里有没有.block()、Thread.sleep()、ObjectMapper的同步调用、以及被Async注解污染的线程池。十个 WebFlux 生产事故九个是阻塞操作埋的雷。这招我屡试不爽。
返回列表