任何体系结构决策都需要权衡。 如果您决定采用反应式,也没有什么不同,例如,一方面使用反应式流实现几乎可以立即获得更好的资源利用率,但另一方面会使调试更加困难。 引入反应式库也对您的域产生巨大影响,您的域将不再仅在Payment
, Order
或Customer
方面说话,反应式术语将破解Flux<Payment>
, Flux<Order>
, Mono<Customer>
(或Observable<Payment>
, Flowable<Order>
, Single<Customer>
或您选择的库提供的任何Reactive Streams发布者)。 这种折衷很快就变得很明显,但是您可能会猜想并非所有的折衷都会如此明显– 泄漏抽象定律保证了这一点。
反应性库使更改线程上下文变得轻而易举。 您可以轻松地订阅一个调度程序,然后在另一个调度程序上执行一部分操作员链,最后跳到完全不同的调度程序上。 只要不涉及线程局部状态,这种从一个线程到另一个线程的跳转就可以工作-尽管它支持服务的关键部分(例如安全性,事务),但通常不会每天处理该状态, 多租户)。 当您的技术堆栈中隐藏良好的部分取决于线程局部状态时,更改线程上下文会导致棘手的错误定位。
让我通过一个简单的示例演示该问题:
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String SESSION_ID = "session-id";@GetMapping("documents/{id}")
Mono<String> getDocument(@PathVariable("id") String documentId) {MDC.put(SESSION_ID, UUID.randomUUID().toString());LOG.info("Requested document[id={}]", documentId);return Mono.just("Lorem ipsum").map(doc -> {LOG.debug("Sanitizing document[id={}]", documentId);return doc.trim();});
}
使用MDC.put(SESSION_ID, UUID.randomUUID().toString())
我们将会话session-id
放入基础日志库的映射诊断上下文中 ,以便稍后进行登录。
让我们以自动为我们记录session-id
的方式配置记录模式:
logging.pattern.console=[%-28thread] [%-36mdc{session-id}] - %-5level - %msg%n
当我们通过请求( curl localhost:8080/documents/42
)访问公开的服务时,我们将看到session-id
出现在日志条目中:
[reactor-http-server-epoll-10] [00c4b05f-a6ee-4a7d-9f92-d9d53dbbb9d0] - INFO - Requested document[id=42]
[reactor-http-server-epoll-10] [00c4b05f-a6ee-4a7d-9f92-d9d53dbbb9d0] - DEBUG - Sanitizing document[id=42]
如果在将session-id
放入MDC之后切换执行上下文(例如,通过预订不同的调度程序),情况将发生变化:
@GetMapping("documents/{id}")
Mono<String> getDocument(@PathVariable("id") String documentId) {MDC.put(SESSION_ID, UUID.randomUUID().toString());LOG.info("Requested document[id={}]", documentId);return Mono.just("Lorem ipsum").map(doc -> {LOG.debug("Sanitizing document[id={}]", documentId);return doc.trim();}).subscribeOn(Schedulers.elastic()); // don't use schedulers with unbounded thread pool in production
}
执行上下文更改后,我们将注意到该调度程序调度的操作员记录的日志条目中缺少session-id
:
[reactor-http-server-epoll-10] [c2ceae03-593e-4fb3-bbfa-bc4970322e44] - INFO - Requested document[id=42]
[elastic-2 ] [ ] - DEBUG - Sanitizing document[id=42]
您可能会猜到,我们正在使用的日志记录库内部深处隐藏着一些ThreadLocal
。
一些Reactive Streams实现提供了允许将上下文数据提供给操作员的机制(例如Project Reactor提供订户上下文 ):
@GetMapping("documents/{id}")
Mono<String> getDocument4(@PathVariable("id") String documentId) {String sessionId = UUID.randomUUID().toString();MDC.put(SESSION_ID, sessionId);LOG.info("Requested document[id={}]", documentId);return Mono.just("Lorem ipsum").zipWith(Mono.subscriberContext()).map(docAndCtxTuple -> {try(MDC.MDCCloseable mdc = MDC.putCloseable(SESSION_ID, docAndCtxTuple.getT2().get(SESSION_ID))) {LOG.debug("Sanitizing document[id={}]", documentId);return docAndCtxTuple.getT1().trim();}}).subscriberContext(Context.of(SESSION_ID, sessionId)).subscribeOn(Schedulers.elastic()); // don't use schedulers with unbounded thread pool in production
}
当然,使数据可用只是故事的一部分。 一旦我们提供了session-id
( subscriberContext(Context.of(SESSION_ID, sessionId))
),我们不仅必须检索它,还必须将其附加到线程上下文中,并且由于调度程序可以自由地记住自己,所以请自己清理一下。重用线程。
提出的实现会带回session-id
:
[reactor-http-server-epoll-10] [24351524-f105-4746-8e06-b165036d02e6] - INFO - Requested document[id=42]
[elastic-2 ] [24351524-f105-4746-8e06-b165036d02e6] - DEBUG - Sanitizing document[id=42]
但是,使它起作用的代码太复杂,太具有侵入性,以致于在大多数代码库中都不会张开双臂来欢迎它,尤其是当它最终散布在整个代码库中时。
我很乐意通过为该问题提供一个简单的解决方案来结束本博文,但我尚未偶然发现这样的问题(现在,我们需要使用这样更复杂,更具侵入性的解决方案,同时还要尝试解决这种复杂性从以业务为中心的软件部分到基础设施部分,如果可能,还可以直接到库本身)。
翻译自: https://www.javacodegeeks.com/2018/09/thread-local-state-availability-in-reactive-services.html