bigywater.tech
Spring · 62

响应式下的上下文传播:ThreadLocal 为什么失效,Reactor Context

WebFlux 里的 ThreadLocal(MDC、用户信息)为什么会丢?Reactor Context 怎么用、方向是什么?为什么手动 subscribe() 会丢上下文?怎样让 ThreadLocal 在响应式链里自动恢复?这篇用 Reactor 3.8.7 和 context-propagation 1.2.1 逐个验证。

null
publishOn / delay / subscribeOn 之后读 ThreadLocal 的结果
(无)
contextWrite 放在上游之前、或在 flatMap 里手动 subscribe() 一条新链时读到的 Context
2 者缺一
ThreadLocal 要自动恢复,必须同时注册 accessor 并开 Hook

ThreadLocal 为什么丢

实测 · 本机执行输出
  主线程直接读 = user-42
  Mono.just(1).map(读 TL).block()(没有切线程,在 main 上执行)= user-42
  Mono.just(1).publishOn(parallel).map(读 TL)= parallel-1 读到 null
  Mono.delay(10ms).map(读 TL)= parallel-2 读到 null
  Mono.fromCallable(读 TL).subscribeOn(boundedElastic)= boundedElastic-1 读到 null

响应式链会在多个线程上执行,ThreadLocal 只在当前线程有效。

Context 的方向

实测 · 本机执行输出
  contextWrite 放在链的末尾,上游的 deferContextual 读到 = alice
  contextWrite 放在 deferContextual 之前(上游)= (无)
  两个 contextWrite 写同一个 key,离订阅者近的先写、离源近的后写 —— 最终上游看到 = inner
  Context 经过 publishOn 切线程 = boundedElastic-1 读到 alice
  在 delay 之后的 map 里通过 transformDeferredContextual 读 = parallel-4 读到 alice
  Context 是不可变的:ctx.put 返回新对象,原来的不变 = 2 / 1

从订阅者往源头传,contextWrite 只影响上游。

Context 的边界

实测 · 本机执行输出
  flatMap 内层读外层的 Context = alice
  内层 flatMap 里手动 subscribe() 一个新链(读不到外层 Context)= (无)
  订阅者各自的 Context 互不影响:同一个 Mono 用两个 Context 订阅 = 读到 A / 读到 B
  cache() 之后:第一个订阅者的 Context 被缓存的结果带给了第二个 = 读到 A / 读到 A

手动 subscribe() 不继承;cache() 会把第一个订阅者的结果带给后面的订阅者。

自动恢复 ThreadLocal

实测 · 本机执行输出
  1) 什么都没配置:parallel-5 读到 null
  2) 只注册了 ThreadLocalAccessor,没开 Hook,也没 contextCapture:parallel-6 读到 null
  3) 注册了 + 链尾加 contextCapture(),但没开 Hook:parallel-7 读到 null
  4) 注册了 + 开了 Hook,没有 contextCapture:parallel-8 读到 user-42
  5) 注册了 + 开了 Hook + 链尾 contextCapture():parallel-9 读到 user-42
  6) 同上,再用 Mono.delay 和 subscribeOn(boundedElastic):parallel-10 读到 user-42 | boundedElastic-1 读到 user-42
  7) 订阅线程上没有设置 TL 时,下游读到 = parallel-1 读到 null
  8) 线程池里的任务:普通 Runnable / ContextSnapshot.wrap:
      普通提交 = my-pool 读到 null;ContextSnapshot.captureAll().wrap(...) 提交 = my-pool 读到 user-42

注册 accessor 加开 Hook,线程池任务要 wrap。

分步演示

上下文传播
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 reactor-core 3.8.7 与 io.micrometer:context-propagation 1.2.1 的单个 Java 程序,不依赖 Spring 容器;线程名 parallel-N 的序号随运行变化;第四节的 ThreadLocalAccessor 与 Hook 是全局的,按顺序一步步打开
    • 没有验证:Spring WebFlux 里 spring.reactor.context-propagation 属性与 Micrometer Tracing 的自动传播;MDC 的 ThreadLocalAccessor(micrometer-tracing 提供);Spring Security 的 ReactiveSecurityContextHolder;虚拟线程下的 ThreadLocal / ScopedValue;contextCapture 的性能开销;Reactor 的 Hooks.enableAutomaticContextPropagation 与 flatMap 内层线程切换的所有组合
    京ICP备2026067502号-1京公网安备11010502063487号