虚拟线程
打开
spring.threads.virtual.enabled=true之后,DDK 的上下文、锁和定时任务各自是什么语义。下面每一条都有测试对应,不只是「能启动」。
结论
| 关注点 | 开启虚拟线程后 | 依据 |
|---|---|---|
| 请求里的操作者 | 不变:OperatorContextFilter 在请求开始时设置,结束时在 finally 里清掉;虚拟线程不复用,也就不会把上一个请求的操作者留给下一个 | Web starter 的过滤器测试 |
@Async 里的操作者和租户 | 照样跟过去。Spring Boot 把任务执行器换成每个任务一个虚拟线程的 SimpleAsyncTaskExecutor,Tracer starter 注册的 ContextPropagatingTaskDecorator 仍然用在它上面 | AsyncOperatorPropagationTest;关掉 ddk.tracer.async-propagation.enabled 后操作者就拿不到了 |
@Async 里的链路 | 照样跟过去,前提是链路由 Observation 开启(Web 请求、消息消费都是)。只用 Tracer.withSpan 手动放进作用域的 span 不会被带过去,平台线程池上也一样 | 同上 |
聚合锁 @AggregateLock / AggregateLocks | 互斥不变。Redisson 按线程区分锁的持有者,每个虚拟线程是不同的持有者;加锁、释放必须在同一个线程上,和平台线程一样 | ConcurrencyRedisIntegrationTest:32 个虚拟线程竞争同一把锁 |
| 等锁时被中断 | 0.11 起抛 CancellationException,中断标志保留,操作不执行。之前报的是 AggregateBusyException(「正在被其他操作处理」),会被当成可以重试的冲突 | 同上;商城的进程内库存锁同样处理 |
| 一次锁多个聚合 | 不变:按锁名排序后依次加锁,与线程类型无关 | executeAll 的测试 |
| 定时任务 | 会重叠:调度器给每次执行一个新线程,固定频率和 cron 的任务上一次没跑完,下一次就可能开始。平台线程的默认调度器只有一个线程,不会重叠。固定延迟(fixed-delay)的任务仍然串行 | VirtualThreadSchedulingTest;固定延迟见 Spring SimpleAsyncTaskScheduler 的文档 |
加了 @SchedulerLock 的定时任务 | 不重叠:锁挡住了同名任务的下一次执行,进程内的锁存储也一样 | 同上 |
| 应用关闭时的定时任务 | 调度器停止触发新的执行,但已经开始的执行不会被停下,会继续跑完;任务要能在应用关闭的过程中安全结束 | Spring SimpleAsyncTaskScheduler 的文档;测试里上一个上下文关闭后,它的任务仍在运行 |
| 集成事件的投递顺序 | 失去 spring.task.execution.pool.core-size=1 这个手段:没有线程池了,每次投递一个线程 | Event Starter 的「顺序」 |
| 投影刷新、事件维护、本地投递 | 不受影响:这些后台工作跑在 DDK 自己创建的单线程执行器上,用的是平台线程,不随 spring.threads.virtual.enabled 改变。投影刷新照旧以系统操作者 ddk-projection 加标记里的租户执行 | RefreshWorker、EventMaintenance、LocalEventDelivery 的实现 |
需要做的
- 定时任务要么加锁,要么能并行。 开启虚拟线程前,检查没有
@SchedulerLock的固定频率和 cron 任务:执行时间可能超过间隔的,加上锁,或者改成固定延迟。 - 需要按提交顺序投递事件的,不要开虚拟线程,或者让消费方不依赖顺序(Event Starter 推荐后者)。
- 自己创建的线程和执行器(
Thread.ofVirtual()、Executors.newVirtualThreadPerTaskExecutor())不经过 Spring 的装饰器,用OperatorContext.wrap包一下任务,链路用 Observation 的ContextSnapshot传递。 - 处理
CancellationException。 应用关闭或任务被取消时,等锁的操作会以它结束;不要把它映射成「请稍后重试」。
JDK 版本
JDK 21 上,线程在 synchronized 块里阻塞会占住载体线程(pinning),并发量大时虚拟线程的优势会被吃掉。DDK 自己的主代码里没有 synchronized,但 JDBC 驱动、连接池、Redisson 等依赖里可能有。JDK 24 起(JEP 491)synchronized 不再占住载体线程。CI 在 JDK 21 和 25 上运行上面的测试;打算在生产上大量使用虚拟线程的,建议用 JDK 25。