实测 Spring Boot 的静态资源:默认只有 Last-Modified 和 Accept-Ranges,没有 Cache-Control 和 ETag;If-Modified-Since 得 304,Range 得 206/416;cache.period 与 cachecontrol.* 对所有资源同一个值;内容哈希版本化让 /app.js 变成 /app-<md5>.js 并加 ETag,但开了长缓存之后没有哈希的原路径也被缓存一年;固定版本是路径前缀,ETag 都是 W/"v7";add-mappings=false 关掉静态资源与欢迎页。
实测 Spring Boot 4 的 HTTP 客户端:只有 starter-restclient 时 Boot 用 JDK HttpClient,new RestTemplate() 用 HttpURLConnection;三者顺序 20 次都只建 1 个连接、并发 20 个线程各建 1 个连接,没有默认上限;默认没有读超时和连接超时(连不可达地址挂 20 秒以上),旧属性名 spring.http.client.read-timeout 静默无效,要用 spring.http.clients.*;4xx/5xx 默认抛异常,HTTP Interface 返回 ResponseEntity 也一样抛;加上 Apache HttpClient 后默认每个路由只有 5 个连接。
实测 Spring MVC 里的请求日志与 MDC:Filter 放进 MDC 的 traceId 让 controller、service 的日志自动带上;换线程后 @Async、Callable、CompletableFuture、new Thread 的 MDC 都是空,ASYNC 重新派发后的拦截器日志也空;TaskDecorator Bean 只补上 Spring 自己的执行器(@Async、Callable);Filter 不在 finally 里清 MDC 会让下一个请求继承上一个的 id;CommonsRequestLoggingFilter 的 payload 只在 After request 才有且被截断,includeHeaders 会把 Authorization 和 Cookie 明文写进日志。
实测 Spring Boot 内嵌 Tomcat 线程池满了会怎样:默认 threads.max=200、max-connections=8192、accept-count=100;threads.max=2 时请求成对排队、不报错,一个几毫秒的 /fast 被拖到 3770 ms;accept-count 很小时,多余的连接在 1 ms 内被 ConnectionResetError(macOS 本机回环,成功个数每轮波动);connection-timeout 不会踢掉排队等线程的请求。
实测 Reactor 的惰性:fromSupplier / map / doOnNext 在订阅前不执行,但 Mono.just(expr) 的参数在构造时就算了;每次订阅重新执行源,cache() 与 share() 才共用;在 map 或 doOnNext 里调用返回 Mono 的方法并丢掉返回值,等于没有执行;热源按订阅时机决定收到什么(onBackpressureBuffer 缓存、directBestEffort 丢弃);CompletableFuture 包成 Mono 也不会变惰性;没有 error 回调的 subscribe() 会把错误吞到日志里。
实测 Reactor 背压:Flux.interval 下游跟不上就 OverflowException(处理了 4 个元素);五种溢出策略的取舍(无界缓冲不丢、DROP_OLDEST 收到 116 个并丢弃 4884 个、Drop 只收到 16 个);publishOn 默认预取 256 并按 192 补充、concatMap 每次只 request(1);无界缓冲让 20 万个元素堆在内存,而有背压时生产只领先消费几个;Sinks 缓冲满了 tryEmitNext 返回 FAIL_OVERFLOW,不检查就静默丢失。
实测 Reactor 的线程切换:subscribeOn 决定订阅与源所在线程且只有最靠近源的生效,publishOn 决定它之后的线程且可以多次切换;Mono.delay 之后在 parallel 线程,fromFuture 之后在完成它的线程;阻塞的源不加 subscribeOn 会卡住调用线程(subscribe() 用了 310 ms),加了之后 2 ms 返回;flatMap 里内层加 subscribeOn 才并行(1235 ms 降到 304 ms);parallel 线程数等于核数,阻塞操作该去 boundedElastic。
实测 WebFlux 的线程模型:Reactor Netty 默认 event loop 线程数等于逻辑核心数(10),连接轮流分配;把线程数设成 2 之后,在 event loop 上 Thread.sleep(无论 Mono.fromCallable 里还是控制器直接 sleep)会让别的请求(包括 /fast)等到 1022 ms,subscribeOn(boundedElastic) 则只有 9 ms;event loop 线程上 block() 会被 Reactor 直接拒绝(500);非阻塞等待让 2 个线程撑住 200 个并发请求。
实测 WebClient:默认共享连接池最多 500 个连接,自建 ConnectionProvider 不写上限只有 20;池满后请求排队,pendingAcquireTimeout 到期抛 PoolAcquireTimeoutException;默认没有响应超时(等到 2149 ms),要用 Mono.timeout 或 spring.http.clients.read-timeout;Retry.backoff 不加 filter 会把 404 也重试(服务端被打 4 次);非幂等 POST 加 timeout 重试会让服务端写入 3 次;retrieve() 对 4xx/5xx 抛异常,空响应体要 defaultIfEmpty。
同一份「每个请求调下游 100 ms」的负载,在 Spring Boot 4.0.8 上实测:MVC 默认 200 个平台线程时,并发 1000 只有约 2000 请求/秒(p50 550 ms);打开 spring.threads.virtual.enabled 后代码不改,并发 1000 达到约 9100 请求/秒(p50 约 110 ms),但 RSS 从 439 MB 涨到 688 MB;WebFlux 在并发 1000 时吞吐与虚拟线程版同量级(约 9100 请求/秒),RSS 只有 375 MB。并发 3000 时压测机和被测应用挤在同一台机器上,结果波动很大。
在本机 PostgreSQL 16 上实测 Spring Data R2DBC:repo.save 不订阅就不发 INSERT;没有事务时同一方法里的两条语句可能落在不同连接上(pid 不同),@Transactional 里则是同一个连接;失败回滚只在事务里生效,链外的 subscribe() 或 boundedElastic 上的 block() 写入的行不会回滚;默认连接池服务端最多同时 10 个连接,池满排队,max-acquire-time 到期抛 R2dbcTimeoutException。
实测响应式下的上下文传播:ThreadLocal 一旦经过 publishOn、delay、subscribeOn 就读到 null;Reactor Context 随订阅从订阅者向源头传,contextWrite 只影响上游,flatMap 内层继承而手动 subscribe() 的新链读不到,cache() 会把第一个订阅者的值带给后面的订阅者;注册 ThreadLocalAccessor 并开启 Hooks.enableAutomaticContextPropagation() 才能让 ThreadLocal 跟着走,缺一个都读到 null。