news 2026/10/16 13:31:24

Vert.x 4 EventLoop核心机制与高并发实践详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vert.x 4 EventLoop核心机制与高并发实践详解

把 Vert.x 4 的 EventLoop 吃透,高并发代码基本就入门了。这篇笔记是我在项目里把 Vert.x 4 落地时的思考与实践,涵盖原理、线程模型、参数配置、阻塞检测,以及我实际排查过的一些典型问题。适合刚接触 Vert.x、或者写了异步代码但总感觉哪里不对的 Java 开发者,看完你会知道 EventLoop 到底在干什么,以及写 Vert.x 代码时哪些事千万别做。

1. 为什么需要EventLoop:经典并发模型的困局

1.1 线程池阻塞模型的问题

先聊聊传统的线程池模型为什么在高并发下会吃力。

假设一个服务有 1 万个 TCP 连接,按照传统“每个请求一个线程”的思路,最直观的做法是开一个线程池,比如 200 个线程,每个线程去处理一个请求。问题在于,这些请求多半不是一直在 CPU 上计算,而是大量时间花在等待上:等数据库返回、等下游接口响应、等磁盘 IO 完成。于是线程池里的线程经常是“占着茅坑不拉屎”,一边阻塞等待,一边占用系统资源。

这里有个很典型的数字:默认情况下,Java 一个线程的栈空间在 512KB 到 1MB 之间。200 个线程不算多,但到了 2000 个线程,光栈内存就要吃掉几个 GB。再加上线程切换时要保存和恢复上下文,系统一忙起来,CPU 时间全耗在切换上了,真正干活的反而少。

很多人会想到用虚拟线程来解决,但 Vert.x 从诞生起走的是另一条路:不靠线程多,而是靠“线程少但不停干活”的事件循环模型。

1.2 事件驱动模型如何破局

事件循环的核心思想是:用极少数线程处理海量连接,当 IO 事件到达时,事件循环取出事件并分发到对应的回调函数,回调执行完继续循环,整个过程线程永远不会因为等待 IO 而阻塞。

可以打个比方:一个前台接待员同时接待多位客户。客户 A 说“我需要回办公室查个数据”,接待员不会站在那儿干等,而是继续接待客户 B。等客户 A 查完数据回来,接待员再接着处理他的后续请求。接待员全程没有“闲等”,一直在做事。

这个模型放到服务器上就是:

  • 线程不断循环处理就绪的 IO 事件、定时器事件、用户任务。
  • 事件到来后,执行对应的回调逻辑,一次只执行一个。
  • 回调必须执行得非常快,否则会卡住整个事件循环,导致后续所有事件都排队。

关键优势是,一个事件循环内部天然是串行执行的。也就是说,在同一个事件循环上跑的业务代码不需要加锁,很大程度上避开了多线程编程里最让人头疼的竞态问题。

1.3 Vert.x在选择EventLoop背后的考量

Vert.x 没有重新发明一套底层框架,它的 EventLoop 直接构建在 Netty 之上。Netty 本身已经有了非常成熟的事件循环模型和非阻塞 IO 能力,Vert.x 更像是在 Netty 外面包了一层更适合开发者使用的编程模型。

为什么 Vert.x 要选择“事件循环 + 多实例”而不是“大线程池”的模式?主要有三个原因。

第一,IO 密集场景下,事件循环模型的线程利用率最高。线程不因为等待 IO 而闲置,所以不需要那么多线程就能支撑大量连接。

第二,编程模型简单。同一个 Context 上的代码天然串行执行,不需要在每个方法里都考虑加锁、原子操作等问题。这个看似简单的保证,实际开发中价值很大。

第三,可以垂直扩展到多核。每核跑多个 EventLoop,多个事件循环并行处理不同的连接和请求,就能把多核机器的算力压出来,同时又不牺牲单一事件循环内部的简单性。

2. Vert.x 4中EventLoop的核心机制

2.1 底层与Netty的关系

Vert.x 的底层 TCP、UDP 监听、HTTP 解析、读写缓冲区等全部跑在 Netty 的 EventLoop 上。它没有把 Netty 包得严严实实,而是让开发者通过 Vert.x 的高层 API 去使用能力,但底层线程模型就是io.netty.channel.EventLoop。

一个非常直观的验证方式是打印线程名。在 Vert.x 的默认回调里打印Thread.currentThread().getName(),你会看到类似这样的输出:

vert.x-eventloop-thread-0 vert.x-eventloop-thread-1 vert.x-eventloop-thread-2

这些线程就是 Vert.x 的核心线程,也就是 EventLoop 线程。Netty 和 Vert.x 的关联在这里体现得最为明显:线程数量的默认算法、线程命名规则、线程状态检查,都和 Netty 的SingleThreadEventExecutor机制紧密相关。

2.2 EventLoop线程池大小计算与配置

Vert.x 默认的 EventLoop 线程池大小是 CPU 核心数的两倍。比如在一台 4 核机器上,默认会有 8 个 EventLoop 线程;8 核 16G 的机器默认 16 个。

计算公式可以理解为:

事件循环线程数 = Runtime.getRuntime().availableProcessors() * 2

为什么要乘以 2?因为 Vert.x 定位的场景高度偏向 IO 密集。在 IO 密集场景下,线程大部分时间在处理已就绪的事件、切换网络读写,有空闲能力承接第二个事件循环,让 CPU 核心更饱满。当然,如果业务本身的 CPU 计算量很大,可以调小这个数值,甚至调到 CPU 核心数。

手动配置的代码如下:

VertxOptions options = new VertxOptions() .setEventLoopPoolSize(8); Vertx vertx = Vertx.vertx(options);

需要特别注意,这个配置一定要在创建 Vertx 实例之前完成,并且配置是对整个 Vertx 实例生效的。后面部署 Verticle 时,无论部署多少个 Verticle,默认都会复用这同一个 EventLoop 线程池。

2.3 EventLoop的循环流程与任务调度

从微观角度看,一个 EventLoop 线程其实就是一个不断运行的事件循环,核心逻辑可以简化为:

while (true) { 从任务队列中取出一个目标任务或 IO 事件 执行对应的处理器或用户回调 回到循环起点继续取下一个任务 }

那么任务到底从哪里来?我梳理一下主要来源。

  • 网络 IO 事件。连接上的数据可读、可写、连接建立、连接关闭等,都由 Netty 的 Channel 触发。
  • 定时器任务。在 Vert.x 里调用setTimer或setPeriodic,相应时间到了会生成一个任务放到某个 EventLoop 上执行。
  • executeBlocking的返回结果。阻塞操作在 Worker 线程池执行完后,结果需要通过异步回调送回到原来的 EventLoop 上继续处理。
  • 从其他线程提交的任务。比如调用context.runOnContext(...)时,实际上是向目标 EventLoop 的任务队列塞入一个任务。
  • Verticle 的部署、启动、停止等生命周期事件。

这里有一个容易忽略的点:一个 Verticle 实例一旦被分配到某个 EventLoop 上,它就会一直固定在这个 EventLoop 上执行,不会被动态迁移到另一个 EventLoop。也就是说,Verticle 内部代码看起来就像单线程一样执行,不需要考虑多个线程同时进入同一个 Verticle 的状态问题。

2.4 上下文Context与EventLoop绑定的意义

在 Vert.x 中,Context 是一个非常重要的概念。你可以把它理解为“一段执行流程所在的线程环境”。每一次请求进来,Vert.x 会确保这个请求的完整链路都运行在同一个 Context 上,而同一个 Context 只会绑定一个 EventLoop。

这个设计带来两个结论。

第一,同一个 Context 上的代码永远串行执行。你不需要担心同一份可变状态被两个线程同时访问,前提是访问这份状态的代码始终落在同一个 Context 上。

第二,如果要从外部线程进入某个 Context,必须通过context.runOnContext(...)来投递任务。手动直接调用对象方法修改共享状态,并不在 EventLoop 的管辖范围内,这时候就需要你自己保证线程安全。

实际开发中经常犯的错误,就是在一个普通的第三方回调线程里直接修改了 Vert.x Verticle 的成员变量,结果发现数据错乱、偶发异常。正确做法是把修改操作通过runOnContext送回 EventLoop 去执行。

3. EventLoop实操:从入门到写不阻塞的代码

3.1 最小HttpServer跑起来

先写一个最简单的 HTTP 服务,感受一下 Vert.x 4 的代码风格。

import io.vertx.core.Vertx; public class HttpDemoServer { public static void main(String[] args) { Vertx vertx = Vertx.vertx(); vertx.createHttpServer() .requestHandler(req -> { req.response() .putHeader("content-type", "text/plain") .end("Hello from Vert.x 4!"); }) .listen(8080, http -> { if (http.succeeded()) { System.out.println("Server started on port " + http.result().actualPort()); } }); } }

requestHandler里注册的这个回调,默认就是跑在 EventLoop 线程上的。也就是说,每当一个请求进来,Netty 的 IO 线程事件会转成 Vert.x 的任务,最终交给某个 EventLoop 去执行这个 Lambda。

这个服务看起来简单,但它在单个机器上能撑的连接数远超传统线程池模型,前提有一点:回调里绝不能阻塞。

3.2 定时器、Future与Promise的正确打开方式

在 Vert.x 中,定时器也是事件循环模型的一部分。调用定时器不会新建线程,而是把定时任务挂到某个 EventLoop 上,时间到了就执行。

vertx.setTimer(3000, id -> { System.out.println("3秒后执行,线程:" + Thread.currentThread().getName()); });

除了定时器,Vert.x 4 的异步结果处理主要基于 Future 和 Promise。理解这两个概念有个简单粗暴的方式:

  • Promise 是结果的写入方,可以主动complete或fail。
  • Future 是结果的读取方,通过onSuccess、onFailure挂回调。
Promise<String> promise = Promise.promise(); Future<String> future = promise.future(); future.onSuccess(result -> { System.out.println("成功:" + result); }).onFailure(err -> { System.out.println("失败:" + err.getMessage()); }); // 模拟某个异步操作完成后写入结果 promise.complete("data");

在 Vert.x 4 中,Future 的 API 比 3.x 时代干净很多,支持链式组合,后面的章节我会专门讲怎么避免回调地狱。

3.3 executeBlocking:阻塞调用的合法绕行方案

业务里不可能完全没有阻塞操作,比如同步 JDBC 查询、同步文件写入、调用某个老的 SDK。Vert.x 没有让你硬着头皮在 EventLoop 上执行,它提供executeBlocking,把这些阻塞操作扔到专门的 Worker 线程池里,执行完再通过回调回到 EventLoop。

看一个典型场景:

// 错误写法:在EventLoop上直接同步等待 requestHandler(req -> { String result = someBlockingDatabaseCall(); // 这会卡死EventLoop req.response().end(result); });

上面这种写法在压测时会立刻暴露问题:请求并发一上来,所有请求开始排队,EventLoop 线程全部卡在数据库等待上,日志里一片阻塞告警。

用executeBlocking改造后:

requestHandler(req -> { vertx.executeBlocking(() -> { // 这个Lambda跑在Worker线程池 return someBlockingDatabaseCall(); }, res -> { // 这个回调回到EventLoop线程 if (res.succeeded()) { req.response().end(res.result()); } else { req.response().setStatusCode(500).end("error"); } }); });

这里有个细节:executeBlocking的第二个参数ordered默认是true,表示如果提交多个任务,这些任务会严格按照提交顺序执行。但ordered=true会带来额外的队列排序开销,而且独立任务之间根本不关心顺序。所以 Vert.x 4 推荐在不需要保证顺序时显式传入false。

vertx.executeBlocking(() -> blockingCall(), false, res -> { // 处理结果 });

需要注意的是,executeBlocking使用的是 Worker 线程池,默认大小为 20。如果业务里有大量阻塞调用同时进来,Worker 池也会排长队。可以通过VertxOptions调整:

VertxOptions options = new VertxOptions() .setWorkerPoolSize(100);

不过我建议先思考阻塞是不是真的无法避免,而不是盲目调大线程池。

3.4 共享状态与线程安全的边界在哪里

我在项目里见过不少同学习惯用静态 Map 在不同 Verticle 之间传数据,结果在高并发下出现各种诡异问题。原因很简单:多个 Verticle 实例被部署到不同的 EventLoop 上,静态变量等于被多个线程同时读写,竞态就出现了。

事件循环模型的安全边界在于:同一个 Verticle 实例固定在同一个 EventLoop 上执行。这个边界内不加锁是安全的,跨越这个边界就不安全。

常见的处理方式有四种。

第一种,使用不可变对象。这是最推荐的,完全不担心并发问题。

第二种,把修改操作送回目标 Context 执行。比如有一个共享状态由某个 Context 管理,其他 Context 想改它,不能直接改,而是通过runOnContext把修改动作投递过去。

Context context = vertx.getOrCreateContext(); context.runOnContext(v -> { sharedState.setValue("new"); });

第三种,使用外部存储。比如 Redis、数据库,或者 Vert.x 的sharedDataAPI。

第四种,加锁。这是下策,因为 EventLoop 上任何加锁等待都可能阻塞线程,和事件循环模型的目标相悖。

知友们一定要养成一个习惯:写 Vert.x 代码时先想清楚这段代码落在哪个 EventLoop 上,访问的状态是只属于这个 EventLoop 还是被多个 EventLoop 共享。

4. BlockedThreadChecker与阻塞检测机制

4.1 阻塞检测工作原理

Vert.x 内置了一个线程阻塞检测器,叫BlockedThreadChecker。它会定时检查每个 EventLoop 线程是否还在正常工作。如果某个任务在 EventLoop 上执行时间超过了预设阈值,它就会在日志里打印警告,告诉你哪个线程被阻塞了多久。

检测机制可以这样理解:一个后台守护线程定期轮询所有 EventLoop 线程的状态和上次采样记录,如果发现当前的执行任务和上次采样时的任务是同一个,而且已经超过了检查间隔的一定倍数,就判定线程被阻塞。

默认配置是:

blockedThreadCheckInterval = 1000ms

也就是说大约每 1000ms 检查一次,如果某个 EventLoop 被阻塞的时间超过约 2000ms,就会输出告警日志。实际生产环境里,我见过最夸张的日志长这样:

Thread vert.x-eventloop-thread-2 has been blocked for 20458 ms, time limit is 2000 ms

这行日志意味着这个 EventLoop 线程已经被卡了 20 多秒,这就是一个非常危险的状态了。

4.2 参数调节与日志解读

阻塞检测参数可以通过VertxOptions调整。比如你想更快地发现阻塞问题,可以把检查间隔调小:

VertxOptions options = new VertxOptions() .setBlockedThreadCheckInterval(500) .setBlockedThreadCheckIntervalUnit(TimeUnit.MILLISECONDS);

需要说明的是,调小间隔会稍微增加检测的 CPU 开销,但这点开销换来的问题定位效率很划算。

有些人觉得这个日志太吵,想直接把检查关掉,比如把间隔设为 -1。我个人不推荐这样做。阻塞检测不是用来消除告警的,而是用来暴露问题的。一个 EventLoop 被阻塞,影响的不是某一个请求,而是整批请求。日志难听一点没关系,能帮你把问题揪出来就好。

另外要注意,阻塞检测不只是针对 EventLoop 线程,Worker 线程池同样有检测。只是实际开发中,Worker 线程上的阻塞不如 EventLoop 上那么致命,所以大家在排查时优先关注 eventloop 线程即可。

4.3 真实案例:一个sleep引发的灾难

我印象最深刻的一个案例,是一个内部网关服务。当时有人在requestHandler里为了模拟第三方系统的耗时逻辑,直接写了个Thread.sleep(5000),然后才返回响应。

开发环境一切正常,因为请求量小,卡一个 EventLoop 5 秒看不出什么。到了压测环境,并发 500 也还好,毕竟有 8 个 EventLoop,每个卡 5 秒也能扛一段时间。但并发到了 2000,日志里开始大量出现:

Thread vert.x-eventloop-thread-0 has been blocked for 5210 ms Thread vert.x-eventloop-thread-1 has been blocked for 5223 ms Thread vert.x-eventloop-thread-2 has been blocked for 5198 ms

几乎每个 EventLoop 都被卡住,请求延迟从几十毫秒飙升到几十秒,最终网关整体不可用。排查过程其实就是盯着线程名日志逐行看,定位到requestHandler里的 sleep 后,所有人倒吸一口凉气。

这个案例告诉我们一个道理:在事件循环模型里,单个回调慢 5 秒,代价是整个事件循环线程 5 秒内失去服务能力。这不是“一个请求慢了一点”的问题,而是“整批请求都在排队”的问题。

5. 常见问题与排查技巧实录

5.1 如何快速判断EventLoop被阻塞

在 Vert.x 应用卡顿、高延迟、吞吐暴跌时,第一步永远是看线程到底在干什么。我整理了一套快速排查路径。

先看日志。如果出现is blocked for这类告警,说明 EventLoop 线程确实被长时间任务卡住了。日志里会带出线程名,比如vert.x-eventloop-thread-1,你基本可以锁定是哪条链路上出了问题。

再看线程 dump。使用 JDK 自带的jstack,或者 arthas 这类工具抓取线程快照。EventLoop 线程的名字非常有特征,都是vert.x-eventloop-thread-N开头的。如果发现这些线程长时间处于 RUNNABLE 状态但执行栈一直停在某个业务方法上,那就说明这个业务方法就是罪魁祸首。

还可以看 CPU 占用。如果某个核的 CPU 被一个 EventLoop 线程打满,需要怀疑事件循环里跑了 CPU 密集操作或死循环。如果线程处于 WAITING 状态,则更可能是等锁、等 IO、等网络响应。

注意一个细节:如果 EventLoop 线程大量处于 WAITING,不一定是处理代码阻塞,也有可能是这个 EventLoop 压根没什么任务,处于空闲状态。要结合请求量和线程栈综合判断,别看到 WAITING 就慌。

5.2 跨EventLoop访问共享变量的竞态问题

症状表现:偶发的数据覆盖、NullPointerException、值刚更新又变回旧值。这些现象往往在低并发时不出现,高并发时频繁出现。

排查思路分三步。

第一步,确认变量读写的代码都落在哪个 Context 上。最简单的方式是在读写处打印线程名,看看是否始终是同一个 EventLoop 线程。

第二步,确认变量本身是不是被多个 Verticle 实例共享。静态变量、Spring 单例 Bean 的成员变量、传到多个 Verticle 的可变对象,都可能是共享边界。

第三步,根据场景选择合适的解决方案。如果可以,优先改成不可变对象;如果不行,把修改操作投递到负责该状态的 Context;再不行,用外部存储。

我见过一个项目用静态 Map 缓存用户的登录状态,多个 Verticle 同时读写,每次上线后才想起来要清理缓存,结果就是偶尔有用户登录状态错乱。后来改成把缓存放到 Redis,问题才彻底解决。

5.3 回调地狱与异常丢失

Vert.x 4 的异步风格如果掌控不好,很容易写出嵌套五六层的回调代码,维护起来非常痛苦。

比如下面这种:

serviceA.call(rs1 -> { serviceB.call(rs1, rs2 -> { serviceC.call(rs2, rs3 -> { // 终于可以做正经事了 }); }); });

这还算简单的,真实业务里再加上条件分支、超时、异常处理,代码几乎没法读。

Vert.x 4 推荐的方式是使用 Future 链式组合:

serviceA() .compose(rs1 -> serviceB(rs1)) .compose(rs2 -> serviceC(rs2)) .onSuccess(rs -> req.response().end(rs.toString())) .onFailure(err -> { req.response().setStatusCode(500).end(err.getMessage()); });

用链式compose之后,代码的可读性和可维护性好了很多,异常也能统一在onFailure里兜住。这里有一个容易踩的坑:compose的每个环节都可能失败,如果链路中间漏掉了异常处理,最终失败可能没地方接收。所以一定要保证链条末端有onFailure。

另外,Vert.x 4 里回调里抛出的未处理异常,会打印一段堆栈然后被捕获,不会导致 JVM 崩溃,但这不代表可以放任不管。未捕获的异常是潜在 bug 的信号,排查时一定要找到根因,而不是只看堆栈位置。

5.4 Worker线程池耗尽的隐患

executeBlocking用多了之后,还可能遇到另一个隐蔽的问题:EventLoop 没有阻塞,但 Worker 线程池满了。

症状是请求延迟慢慢变大,但 EventLoop 线程的 CPU 占用并不高,线程 dump 里可以看到大量任务在 Worker 线程池里排队。此时如果继续往里提交executeBlocking任务,任务会一直排队等待空闲 Worker 线程。

解决方向有两个。一个是调大 Worker 池,比如从默认 20 调到 100 以上。另一个是优化阻塞调用本身,比如把同步调用改成异步客户端,或者把耗时操作拆批处理。

这里有个比较实用的参考原则:尽量先优化阻塞时间,再考虑调大线程池。因为调大线程池只是提升了并发处理能力,并没有减少 CPU 等待时间,如果阻塞任务本身需要等下游 3 秒,池再大也只是让更多任务同时卡在下游。

6. EventLoop性能调优与扩展实践

6.1 EventLoop线程池大小如何权衡

EventLoop 线程池的大小没有一个绝对最优值,只有基于场景相对合理的取值。我的建议是,以默认值2 * CPU核数为起点,观察一段时间后再决定要不要调整。

如果服务是典型的 IO 密集型,请求处理中频繁访问 Redis、数据库、下游 HTTP 服务,但每个回调都不阻塞,那默认值通常就足够了。

如果服务里有较多 CPU 计算,比如加解密、大数据量序列化,EventLoop 线程的 CPU 利用率很高,那可以考虑把线程数调低到 CPU 核数,减少上下文切换。

如果服务里偶尔不得不执行一些较重的阻塞调用,但已经用executeBlocking处理,此时要关注的不是 EventLoop 大小,而是 Worker 池大小。

调试时可以用压测工具对比不同配置下的吞吐和延迟指标,选一个最符合业务场景的组合。不要凭感觉把 EventLoop 线程数调到 64、128,那只会增加线程切换开销,并不会带来额外收益。

6.2 垂直扩展:多EventLoop实例与Verticle部署

多核机器上充分利用算力的方式是部署多个 Verticle 实例。每个实例会被分配到不同的 EventLoop 上,不同连接、不同请求可以在不同的 EventLoop 上并行处理。

代码层面很简单:

DeploymentOptions options = new DeploymentOptions() .setInstances(4); vertx.deployVerticle("com.example.MyVerticle", options);

部署 4 个实例,等价于把流量负载均衡到 4 个事件循环上。这里需要特别说明:同一个 HTTP 连接上的请求会保持在同一个 EventLoop 上,但不同的连接可能被分到不同的 EventLoop,所以整体并发能力是 4 个事件循环之和。

这种垂直扩展的收益非常明显,一个原本只用到单核能力的 Verticle,部署 4 个实例后,就能轻松吃满 4 核。

补充一个小技巧:Verticle 内部的成员变量状态,在多个实例部署时会被复制多份,也就是说每个实例都有自己的状态。如果这些状态需要全局一致,就必须使用外部存储或者集群共享。

6.3 水平扩展的局限性

很多人一听说 Vert.x 性能好,第一个想法就是集群部署一堆节点。但需要注意,如果业务状态都保存在本机内存里,水平扩展之后各节点状态不一致,问题会非常难查。

EventLoop 模型本身适合“无共享”的设计,所以最好的做法是把共享状态往外移,比如 Redis、数据库。这样无论水平扩展多少节点,状态都是同一份,不会有分叉问题。

如果短期内不想引入外部存储,也可以利用 Vert.x 的sharedData在集群模式下做分布式 Map,但它的使用场景和性能边界需要提前评估。

6.4 压测时的关键指标与调优经验

压测时重点观察三个指标:

  • 吞吐量,也就是每秒完成多少请求。
  • 延迟分布,包括平均延迟和 TP99、TP999。
  • EventLoop 线程的忙碌程度和队列长度。

Vert.x 提供了一个可观测性方案:引入 Vert.x Micrometer Metrics 后,可以输出 EventLoop 队列大小、线程忙碌比例等指标,配合 Grafana 看板能直观看到性能瓶颈。

如果压测发现吞吐先涨后跌,大概率是某个资源达到了瓶颈。此时优先看 EventLoop 线程状态和 Worker 线程池排队情况,用线程 dump 确认阻塞位置。

我个人的调优顺序是:先通过线程 dump 和阻塞日志定位性能损耗点,确保没有阻塞后再调整线程池和队列参数,最后才考虑加机器扩展。反过来调参经常会发现,调了半天线程数,其实只是掩盖了一个阻塞调用的问题。

说到调参,还有一个小细节容易被忽略:日志框架也可能成为 EventLoop 的隐形杀手。如果日志输出到磁盘时是同步写且没有异步缓冲,在高并发下每次打日志都可能伴随文件 IO,EventLoop 线程会被拖慢。建议生产环境使用异步日志组件,或者把日志写入操作放到独立线程。

写在最后的实操体会

从我开始用 Vert.x 4 到现在,EventLoop 带给我的最大体验是“确定性”。同一个 Context 上的代码永远串行执行,不用像传统多线程模型那样反复去思考加锁、原子操作、内存可见性。这种确定性在高并发服务开发里非常值钱,它把一部分并发复杂度从应用层转移到了框架层,让我能把精力放在业务逻辑本身。

但确定性也有代价。代价就是 EventLoop 线程异常珍贵,任何阻塞都会放大成整批请求的排队。我现在写 Vert.x 回调代码前,都会习惯性问自己一句:这段代码会不会在 EventLoop 上等什么东西?如果会,就换成异步客户端,或者扔到executeBlocking里。

最后分享一个排查技巧:遇到 Vert.x 性能问题,第一时间打印线程 dump,过滤vert.x-eventloop-thread-开头的线程。只要看到它们长时间停在某个业务代码栈上,问题基本就锁定了一半。剩下要做的,就是从那个栈一路追下去,找到具体哪一行在阻塞。熟练掌握这个套路,Vert.x 应用的大部分性能问题都能快速定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 13:31:11

毕业论文降AI后如何验证效果?免费方法全攻略

毕业论文降AI后怎么检验效果&#xff1f;免费验证方法教程每年毕业季&#xff0c;最让同学头疼的其实不一定是论文本身&#xff0c;而是论文写完之后那一关——AI检测。有人用AI工具辅助写作&#xff0c;有人用AI润色&#xff0c;结果交上去一查&#xff0c;AIGC率直接飙到百分…

作者头像 李华
网站建设 2026/10/16 13:31:03

PyTorch实战指南:从张量计算到模型训练全流程解析

这几年做深度学习相关项目&#xff0c;接触最多、也最离不开的框架就是PyTorch。不管你是刚入门的算法爱好者&#xff0c;还是已经跑过不少实验的老手&#xff0c;应该都听过它的名字&#xff1a;简洁、直观、调试友好&#xff0c;几乎是研究和工程社区里的默认语言。我最早从另…

作者头像 李华
网站建设 2026/10/16 13:30:53

亿级数据TopK高效解法:堆与Python heapq实战指南

接到“亿级数据里找Top100”这个需求时&#xff0c;很多人的第一反应是 data.sort(reverseTrue)[:100] 。小数据量跑起来毫无压力&#xff0c;可真到了天级别的日志、埋点、用户行为数据&#xff0c;这行代码能把机器内存直接打满&#xff0c;甚至让进程被系统杀掉。我见过不…

作者头像 李华
网站建设 2026/10/16 13:28:36

STM32按键状态机:抖动消除与单击/双击/长按的确定性实现

简介&#xff1a;本资源是一份面向嵌入式初学者与STM32进阶开发者的按键状态机实战项目&#xff0c;聚焦单片机中高频难点——精准识别单击、双击与长按操作。针对传统延时消抖易阻塞、定时扫描逻辑混乱等问题&#xff0c;项目基于STM32F103C8T6平台&#xff0c;采用定时器3中断…

作者头像 李华
网站建设 2026/10/16 13:27:18

原生JS九宫格抽奖:状态机+帧同步+防作弊实战

1. 为什么九宫格抽奖不能只靠“转圈动画”糊弄过去“原生JS实现九宫格抽奖功能”——这行标题在前端技术社区里出现频率极高&#xff0c;但真正能在线上活动页稳定跑满7天、扛住3000人并发点击、不卡顿不跳奖、不被用户截图举报“黑幕”的&#xff0c;不到两成。我参与过某电商…

作者头像 李华
网站建设 2026/10/16 13:27:13

文本模型不生成视频?代码视频五步流水线实战解析

经常有人拿着一条文字描述来找我&#xff0c;开口就是“帮我用 Claude Opus 5.5 生成一条视频”。我每次都得先纠正一句&#xff1a;Claude Opus 5.5 并不会“生成”视频&#xff0c;它生成的是代码&#xff0c;视频是代码跑出来的。这个区别看起来只是措辞问题&#xff0c;实际…

作者头像 李华