我目前正在netty和 jOOQ 上开发 SpringBoot 2 , spring-boot-starter-webflux 的应用程序。
下面是我经过数小时的研究和stackoverflow搜索后得出的代码。我已经建立了很多
记录以查看在哪个线程上发生了什么。
UserController中:
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Mono<ResponseEntity<Integer>> createUser(@RequestBody ImUser user) {
return Mono.just(user)
.map(it -> {
logger.debug("Receiving request on thread: " + Thread.currentThread().getName());
return it;
})
.map(userService::create)
.map(it -> {
logger.debug("Sending response on thread: " + Thread.currentThread().getName());
return ResponseEntity.status(HttpStatus.CREATED).body(it);
})
.mapError(DuplicateKeyException.class, e -> new SomeSpecialException(e.getMessage(), e));
}
UserService:
public int create(ImUser user) {
return Mono.just(user)
.subscribeOn(Schedulers.elastic())
.map(u -> {
logger.debug("UserService thread: " + Thread.currentThread().getName());
return imUserDao.insertUser(u);
})
.block();
}
userDAO的:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public int insertUser(ImUser user) {
logger.debug("Insert DB on thread: " + Thread.currentThread().getName());
return dsl.insertInto(IM_USER,IM_USER.VERSION, IM_USER.FIRST_NAME, IM_USER.LAST_NAME, IM_USER.BIRTHDATE, IM_USER.GENDER)
.values(1, user.getFirstName(), user.getLastName(), user.getBirthdate(), user.getGender())
.returning(IM_USER.ID)
.fetchOne()
.getId();
}
代码按预期工作,&#34;接收请求&#34;和&#34;发送回复&#34;两者都在同一个线程上运行( reactor-http-server-epoll-x ) 阻塞代码(对 imUserDao.insertUser(u)的调用)在弹性调度程序线程( elastic-x )上运行。 事务绑定到调用带注释方法的线程(这是弹性-x),因此按预期工作(我已测试过) 用不同的方法在这里发布,以保持简单)。
以下是日志示例:
20:57:21,384 DEBUG admin.UserController| Receiving request on thread: reactor-http-server-epoll-7
20:57:21,387 DEBUG admin.UserService| UserService thread: elastic-2
20:57:21,391 DEBUG admin.ExtendedUserDao| Insert DB on thread: elastic-2
20:57:21,393 DEBUG tools.LoggerListener| Executing query
...
20:57:21,401 DEBUG tools.StopWatch| Finishing : Total: 9.355ms, +3.355ms
20:57:21,409 DEBUG admin.UserController| Sending response on thread: reactor-http-server-epoll-7
我已经研究了反应式编程很长一段时间了,但是从来没有对任何反应性程序进行编程。既然我是,我想知道我是否正确地做到了。 所以这是我的问题:
1。上面的代码是处理传入HTTP请求,查询数据库然后响应的好方法吗? 为了我的理智,请忽略我内置的logger.debug(...)调用:)我希望有一个 Flux&lt; ImUser&gt; 作为控制器方法的参数,在某种意义上我有多个潜在请求的流 这将在某个时刻出现,并将以相同的方式处理。相反,每次发出请求时,我发现的示例都会创建一个 Mono.from(...); 。
2。在UserService中创建的第二个Mono( Mono.just(用户))感觉有点尴尬。我知道我需要启动一个新流才能够 在弹性调度程序上运行代码,但是没有运营商执行此操作吗?
3。从编写代码的方式来看,我理解UserService中的Mono将被阻止,直到数据库操作完成, 但是服务请求的原始流并未被阻止。它是否正确?
4. 我打算用并行调度程序替换 Schedulers.elastic(),我可以在其中配置工作线程数。这个想法是最大工作线程的数量应该与最大数据库连接相同。 当调度程序中的所有工作线程都忙时会发生什么?那是当背压跳进来的时候吗?
5. 我最初希望在我的控制器中包含此代码:
return userService.create(user)
.map(it -> ResponseEntity.status(HttpStatus.CREATED).body(it))
.mapError(DuplicateKeyException.class, e -> new SomeSpecialException(e.getMessage(), e));
但我无法实现这一点并保持正确的线程运行。有没有办法在我的代码中实现这个目标?
非常感谢任何帮助。谢谢!
答案 0 :(得分:3)
服务和控制器
您的服务阻塞的事实是有问题的,因为在控制器中您正在调用map
内部的阻塞方法,该方法不会在单独的线程上移动。这有可能阻止所有控制器。
您可以做的是从Mono
返回UserService#create
(最后删除block()
)。由于该服务确保Dao方法调用被隔离,因此问题较少。从那里开始,不需要在Controller中执行Mono.just(user)
:只需在生成的Mono上直接调用create和start chaining:
@RequestMapping(value = "/user", method = RequestMethod.POST)
public Mono<ResponseEntity<Integer>> createUser(@RequestBody ImUser user) {
//this log as you saw was executed in the same thread as the controller method
logger.debug("Receiving request on thread: " + Thread.currentThread().getName());
return userService.create(user)
.map(it -> {
logger.debug("Sending response on thread: " + Thread.currentThread().getName());
return ResponseEntity.status(HttpStatus.CREATED).body(it);
})
.mapError(DuplicateKeyException.class, e -> new SomeSpecialException(e.getMessage(), e));
}
<强>登录强>
请注意,如果您想记录某些内容,那么选择map
并返回it
会有更好的选择:
doOnNext
方法适合于:对一个反应信号做出反应(在这个例子中,onNext
:发出一个值)并执行一些非变异动作,离开输出序列与源序列完全相同。 doOn的“副作用”可以写入控制台或递增统计计数器,例如......还有doOnComplete,doOnError,doOnSubscribe,doOnCancel等......
log
只记录其上方序列中的所有事件。它将检测您是否使用SLF4J并在DEBUG级别使用配置的记录器(如果是)。否则它将使用JDK日志记录功能(因此您还需要配置它以显示DEBUG级别日志)。
关于交易的一句话或者更确切地说是依赖ThreadLocal
的任何事物
ThreadLocal和thread-stickyiness在反应式编程中可能会有问题,因为对整个序列中底层执行模型保持不变的保证较少。 Flux
可以分几个步骤执行,每个步骤都在不同的Scheduler
(以及线程或线程池中)。即使在特定的步骤中,一个值也可以由底层线程池的线程A处理,而下一个稍后到达的下一个值将在线程B上处理。
在这种背景下,依赖Thread Local并不那么简单,我们目前正积极致力于提供更适合被动世界的替代方案。
您创建连接池大小的池的想法是好的,但不一定足够,因为事务流量可能会使用多个线程,因此可能会通过事务污染某些线程。
当游泳池用完线程时会发生什么
如果您使用特定的Scheduler
隔离阻塞行为,就像这里一样,一旦线程用尽,就会抛出RejectedExecutionException
。