了解Spring的Web Reactive Framework

时间:2017-03-30 23:32:56

标签: java spring-boot project-reactor reactive

我目前正在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));

但我无法实现这一点并保持正确的线程运行。有没有办法在我的代码中实现这个目标?

非常感谢任何帮助。谢谢!

1 个答案:

答案 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