背景
日常开发中我们经常需要处理各种系统问题,而这些系统问题通常由一些非预期的因素引起(比如非预期的输入,内存不够,网络波动等)。此时就需要知道
- 本次系统问题影响了谁?
- 如果本次系统问题是因为非预期的输入而导致的,那么这个非预期的输入是什么?
上述两点在同步编程里可以通过全局try-catch来实现。但在异步编程里该怎么办呢?
思路
我的想法是绑定一个唯一id到一次完整的请求调用中(这个完整取决于我们想要监控的范围),无论程序执行到何处,我们总能拿到与请求一一对应的调用上下文
的id。
我将分两种场景进行讨论。一种是同步或基于jut下的线程池类进行异步调用的应用程序(如基于spring5之前的应用程序)。另外一种基于vertx(底层基于netty)的应用程序。
技术栈
vert.x 4
、netty 4
、alibaba TransmittableThreadLocal
、jboss javassist
、hotspot类加载&方法调用
1. 同步or基于jdk的异步
关于这种场景,有以下几种实现:
-
程序中的函数调用是完全同步的
则可通过
java.lang.ThreadLocal
绑定一个唯一id到当前线程。入口处生成,出口处销毁。
如果我们用的是logback,也可通过org.slf4j.MDC
来实现这个功能。其内部也是基于ThreadLocal。 -
程序中函数调用是异步的,异步通过新建
java.lang.Thread
的方式实现。这里可通过
java.lang.InheritableThreadLocal
绑定一个唯一id到当前线程。Thread
的构造函数里会自动把通过InheritableThreadLocal
绑定到当前线程的数据拷贝到这个正在创建的Thread
里。所以,只要这个Thread
是在我们需要监控的区域(当前线程通过InheritableThreadLocal
**绑定了id后)**创建的,就能实现这个唯一id的跨线程传递。 -
程序中函数调用是异步的,异步通过
java.util.concurrent
下的线程池类实现。由于线程池中的线程是可以复用的,所以,如果我们往线程池中丢任务时,有两种情况:
- 线程池创建新的线程来执行该任务(比如线程池中的线程数<coreSize)。这种情况下
InheritableThreadLocal
依然是有效的。 - 线程池把任务分配给了已有线程(比如线程池中的线程数≥coreSize并且待执行任务队列没有填满)。这种情况下
InheritableThreadLocal
是无法生效的(线程池中的存活线程会循环拉取等待任务队列中的task去执行,而这个过程是没有InheritableThreadLocal
拷贝的)。所以,这里可以用阿里的[TransmittableThreadLocal](https://github.com/alibaba/transmittable-thread-local)
组件来实现这个唯一id在线程池中的传递(其核心原理是在Runnable
中用一个变量来存储当前线程的ThreadLocal值,当线程执行此Runnable时再拿出来)。
- 线程池创建新的线程来执行该任务(比如线程池中的线程数<coreSize)。这种情况下
实现思路总结
异步环境下的关键在于如何跨线程传递ThreadLocal的值
2. Vertx 中的实现
vertx
是一个类似于spring
系列的用于构建分布式微服务的框架。基于vertx
构建的应用程序最大的特点是无阻塞&全异步。
vertx里的异步主要分两种。一种是在eventbus上传递消息的异步。另一种是基于netty的io操作异步
1. eventbus上异步传递消息
先来看一个简单的示例程序
static final String ADDRESS_1 = "address1";
static final String MESSAGE_1 = "message1";
static final String REPLY_MESSAGE_1 = "replyMessage1";
public static void sendMsgByEventbus() {
//初始化一个具有默认设置的vertx实例
Vertx vertx = Vertx.vertx();
//注册一个handler/consumer到eventbus
vertx.eventBus().consumer(ADDRESS_1, event -> {
log.info("receive msg:{}", event.body());
event.reply(REPLY_MESSAGE_1);