Site icon Vinsguru

Spring WebFlux Redis Integration

spring-webflux-redis

Overview:

In this article, I would like to show how we could improve the performance of our application with Spring WebFlux Redis Integration.

Spring WebFlux Redis Integration:

Redis stands for Remote Dictionary Server.  It is an in-memory, fastest NoSQL DB primarily used for caching the frequently used data. It also has so many other features which we talk in this blog as part of other articles.

In the Microservices architecture, we might some services and they talk to each other to get the tasks done! In some cases, some Microservices might receive a lot of GET requests to get specific information about a resource. For example, a product-service might get frequent requests to get some product information. Instead of fetching the product information every time from DB, the Microservice might want to cache this information – so that we could avoid unnecessary DB calls which involves multiple table joins.

A microservice might cache this information in its local memory. But when we run multiple run instances of a same service, each and every instance has to store the information locally. But if the product information is updated, then we need to find a strategy to clear every instances local cache for the product. It is more challenging.

For microservices architecture, a distributed cache would be a better choice. This is where Spring Webflux Redis integration helps.

Sample Application:

Let’s consider a simple application, product-service which is responsible for providing product information based on the given id. Our underlying data source for the product service is a PostgreSQL DB. Our application will expose 2 APIs.

Database Setup:

version: "3"
services:
  postgres:
    image: postgres
    container_name: postgres
    environment:
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=admin
      - POSTGRES_DB=productdb
    volumes:
      - ./data/db:/var/lib/postgresql/data
      - ./data/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - 5432:5432
  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - 80:80
CREATE TABLE product(
   id serial PRIMARY KEY,
   description VARCHAR (500),
   price numeric (10) NOT NULL,
   qty_available integer NOT NULL
);

Spring WebFlux Redis – Project Setup:

@Data
@ToString
@Table
public class Product {

    @Id
    private Integer id;
    private String description;
    private Integer price;
    private Integer qtyAvailable;

}
@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}
@Data
@ToString
public class ProductDto {

    private Integer id;
    private String description;
    private Integer price;
    private Integer quantityAvailable;

}
public class EntityDtoUtil {

    public static ProductDto toDto(Product product){
        ProductDto dto = new ProductDto();
        dto.setId(product.getId());
        dto.setPrice(product.getPrice());
        dto.setDescription(product.getDescription());
        dto.setQuantityAvailable(product.getQtyAvailable());
        return dto;
    }

    public static Product toEntity(ProductDto dto){
        Product product = new Product();
        product.setId(dto.getId());
        product.setPrice(dto.getPrice());
        product.setDescription(dto.getDescription());
        product.setQtyAvailable(dto.getQuantityAvailable());
        return product;
    }

}
public interface ProductService {

    Mono<ProductDto> getProduct(Integer id);
    Mono<Void> updateProduct(Integer id, Mono<ProductDto> productDto);

}

We are going to provide 2 different implementations for the above product service.

  1. ProductServiceWithNoCache (An implementation which does not use Redis. Always talks to the DB)
  2. ProductServiceWithRedisCache (An implementation which uses Redis Cache)
@Service
@ConditionalOnProperty(name = "cache.enabled", havingValue = "false")
public class ProductServiceWithNoCache implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Mono<ProductDto> getProduct(Integer id) {
        return this.productRepository.findById(id)
                .map(EntityDtoUtil::toDto);
    }

    @Override
    public Mono<Void> updateProduct(Integer id, Mono<ProductDto> mono) {
        return this.productRepository.findById(id)
                .zipWith(mono)
                .doOnNext(t -> t.getT1().setQtyAvailable(t.getT2().getQuantityAvailable()))
                .map(Tuple2::getT1)
                .flatMap(this.productRepository::save)
                .then();
    }

}
@Service
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
public class ProductServiceWithRedisCache extends ProductServiceWithNoCache {

    private static final String KEY = "product";

    @Autowired
    private ReactiveHashOperations<String, Integer, ProductDto> hashOperations;

    @Override
    public Mono<ProductDto> getProduct(Integer id) {
        return hashOperations.get(KEY, id)
                             .switchIfEmpty(this.getFromDatabaseAndCache(id));
    }

    @Override
    public Mono<Void> updateProduct(Integer id, Mono<ProductDto> mono) {
        return super.updateProduct(id, mono)
                                  .then(this.hashOperations.remove(KEY, id))
                                  .then();
    }

    private Mono<ProductDto> getFromDatabaseAndCache(Integer id) {
        return super.getProduct(id)
                    .flatMap(dto -> this.hashOperations.put(KEY, id, dto)
                                                       .thenReturn(dto));
    }

}
@RestController
@RequestMapping("product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("{id}")
    public Mono<ProductDto> getProduct(@PathVariable Integer id){
        return this.productService.getProduct(id);
    }

    @PatchMapping("{id}")
    public Mono<Void> updateProduct(@PathVariable Integer id, @RequestBody Mono<ProductDto> mono){
        return this.productService.updateProduct(id, mono);
    }

}
spring.r2dbc.url=r2dbc:postgresql://localhost:5432/productdb
spring.r2dbc.username=admin
spring.r2dbc.password=admin
cache.enabled=false
// http://localhost:8080/product/2

{
   "id":2,
   "description":"Product2",
   "price":1297,
   "quantityAvailable":69
}

Performance Run 1:

Results:

Now let’s see how we can improve performance using Spring WebFlux Redis Integration!

Spring WebFlux Redis:

redis:
    container_name: redis
    image: redis
    ports:
      - 6379:6379

cache.enabled=true

Performance Run 2:

I reran the same performance test. If you see the result, The performance of our application has increased tremendously. Our application was able to process more than ~16000 requests / second just by adding Redis!

Udemy Course:

I have a 12 hour course which talks about Redis, its various data structures and how it can be used to develop various spring applications. Click on the image to go to Udemy course.

Summary:

We were able to successfully demo Spring WebFlux Redis Integration & improve the performance of our application. Most of the CRUD applications do a lot of READ than WRITE. So caching the frequently accessed information could improve the performance of the application. When you use this pattern, do remember to have the cache evicted policy – that is when to clear the cache (In our cache PATCH requests evict the cache). Otherwise we will end up serving stale data.

Learn more about Spring WebFlux Redis.

The source code is available here.

Happy learning 🙂

Share This:

Exit mobile version