Spring No Reactivo vs Reactivo desde un ejemplo

Con la salida de Spring 5, se dotó al framework de capacidades de programación asincrónica reactiva real (Ver Manifiesto Reactivo). Esto se materializó cambiando el mecanismo de funcionamiento basado en los tradicionales Servlets por un stack reactivo basado en Netty llamado Spring Reactor.

Spring mantiene ambos stacks el tradicional basado en servlets y el reactivo basado en Netty y que ya tiene soporte en la mayoría de los servidores de aplicaciones Jetty, Tomcat, etc.

Spring stacks

Los principios descritos en el manifiesto reactivo fueron implementados de forma total por NodeJS y luego otros frameworks y librerías comenzaron a adoptarlos.

En Spring el stack reactivo viene de la mano del proyecto WebFlux, y a nivel de tipos se materializa en Mono y Flux, ambos son Publishers.

Ventajas de la programación reactiva

  • Manejo de flujos asíncronos.
  • Uso eficiente de los recursos, evitando la creciente creación de threads ante aumento de la carga de peticiones del sistema.
  • Mayor posibilidad de escalabilidad del sistema.
  • Ahorro de recursos (con menos recursos el sistema ofrece mejor throughput).
  • Mejores tiempos de respuesta.

No reactivo vs Reactivo: Un ejemplo práctico.

Veamos un ejemplo ahora de como es el comportamiento en un sistema no reactivo y su misma implementación reactiva. El ejemplo es muy sencillo, pero ayudará a ilustrar la mejora en tiempos de respuesta y el comportamiento en flujo.

Spring no reactivo

Para implementar nuestro ejemplo no-reactivo vamos a usar la siguiente dependencia Maven:

   <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

En el ejemplo vamos a implementar dos funciones que devuelven dos mensajes en un servicio, vamos «dormir» cada función para que demore dos segundos:

@Service
public class SyncService {

    public String operationA(){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Operation A";
    }

    public String operationB(){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Operation B";
    }

}

Luego en el controlador hacemos las llamados a ambas funciones, y concatenamos los resultados.

@RestController
public class SyncResource {

    @Autowired
    private SyncService syncService;

    @GetMapping("/api/sync")
    private String callSync(){

        long t1= System.currentTimeMillis();
        String a = syncService.operationA();
        String b = syncService.operationB();
        long t2= System.currentTimeMillis();
        long total = t2 - t1;
        String result = a + b;
        System.out.println("Total " + total);
        return result;
    }

}

Cuando llamamos al API http://localhost:8080/api/sync y medimos el tiempo, el total de duración de la operación es 4 segundos, y la operación se ejecuta secuencialmente, primero espera 2s a que termine la primera llamada a syncService,operationA() y luego 2s más a que se ejecute syncService,operationB(). Cuando todo está concluido se retorna el resultado, los pasos son:

  1. Se toma el tiempo t1.
  2. Se manda a ejecutar la operación A.
  3. Se manda a ejecutar la operación B
  4. Se toma el tiempo t2.
  5. Se ejecuta la resta de los tiempos.
  6. Se concatena el resultado de a y b.
  7. Se imprime y se retorna un el resultado.

Este tipo de operaciones en ocasiones es necesaria, pero en muchos casos no deberíamos esperar a que una operación termine para iniciar la otra y pudiéramos a la misma vez mandar a hacer las dos operaciones.

Spring reactivo

Implementemos ahora el mismo ejemplo, pero con el stack reactivo. Con ello ganaremos dos cosas fundamentales para el consumidor:

  1. Disminuiremos el tiempo de respuesta de la operación.
  2. El resultado de las operaciones será devuelto como flujo al consumidor, lo que permite que quien invoca la API pueda disponer de un streams de resultados (en este caso 2) en un flujo e ir visualizando a medida que se van teniendo respuestas.
NOTA: La imagen es solo para ilustrar la sensación de flujo en un Publisher Flux.
La imagen mostrada representa un modelo físico de inducción del campo magnético.

La dependencia para el ejemplo reactivo es:

  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>

Hemos cambiado el starter (web x webflux).

La implementación del servicio ahora sería:

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.Duration;

@Service
public class AsyncService {

    public Mono<String> operationA(){
        return Mono.just("Operation A").delayElement(Duration.ofSeconds(2));
    }

    public Mono<String> operationB(){
        return Mono.just("Operation B").delayElement(Duration.ofSeconds(2)) ;
    }

}

Como detalle, estamos usando Mono<String> en el tipo de retorno. Mono y Flux son implementaciones de Reactive Stream Publisher Interface, son tipos para Publicar 1 (Mono) o muchos (Flux) datos.

El controlador es:

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
public class AsyncResource {

    @Autowired
    private AsyncService asyncService;
    
    @GetMapping("/api/async")
    private Flux<String> callSync(){

        long t1= System.currentTimeMillis();
        Mono<String> a = asyncService.operationA();
        Mono<String> b = asyncService.operationB();
        long t2= System.currentTimeMillis();
        long total = t2 -t1;
        Flux<String> data = Flux.concat(a,b);

        System.out.println("Total: " + total);
        return data;

    }
}

El resultado de esta implementación es el mismo que de la otra implementación, sin embargo la forma y tiempo en que llegan los datos a los clientes es diferente.

Si nos fijamos en lo que imprime la variable total en esta operación vamos a ver que es cero segundos o muy cercano a cero ¿Por qué pasa esto?: Lo cierto es que internamente al estar trabajando con un marco reactivo al llamar a http://localhost:8080/api/sync pasa la siguiente:

  1. Se toma el tiempo t1.
  2. Se manda a ejecutar la operación A.
  3. Se manda a ejecutar la operación B
  4. Se toma el tiempo t2.
  5. Se ejecuta la resta de los tiempos.
  6. Se concatena el resultado de a y b en un flujo
  7. Se imprime y se retorna un el flujo.

La diferencia radica en que las llamadas de los pasos 2 y 3 se ejecutan asíncrona y al mismo tiempo por lo que la diferencia entre t2 y t1 es muy cercana a 0, puesto que no se espera de las respuestas, en cambio se crea un flujo o tubería en el que se unen en data los resultados y se devuelve ese Flux, internamente esta tubería queda creada y a medida que vayan saliendo los resultados se van entregando a quien realizó la llamada al API. En este caso a los 2 segundos salen los dos resultados; si uno demorara 2s y otro 3s se publicaría (Publisher) en el Flux el de 2s y luego el de 3s.

Si te ha sido útil comparte este artículo y regalame una estrella en GitHub, puedes ver los ejemplos en mi repositorio de GitHub. Les animo a que clonen y prueben los resultados del experimento.

Un comentario en «Spring No Reactivo vs Reactivo desde un ejemplo»

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

SACAViX Tech