Quantcast
Channel: Design Pattern – Vinsguru
Viewing all articles
Browse latest Browse all 44

Spring Boot Redis Integration

$
0
0

Overview:

In this article, I would like to show you Spring Boot Redis Integration and we could improve the performance of a Spring Boot Microservice using Cache-Aside design pattern.

Redis:

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 in other articles.

Need For Spring Boot Redis Integration:

In the Microservices architecture, we have a few 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.

The Microservice might cache this information in its local memory. In today’s architecture we run multiple run instances of a same service. Saving locally might not help other instances. Using a centralized cache store might help other instances as well.

This is where Spring Boot 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.

  • GET – API to provide the product info for the given product id
  • PATCH – API to update some product information.

Database Setup:

  • I use below docker-compose file to setup Postgres.
version: "3"
services:
  postgres:
    image: postgres
    container_name: postgres
    environment:
      - POSTGRES_USER=vinsguru
      - POSTGRES_PASSWORD=admin
      - POSTGRES_DB=productdb
    volumes:
      - ./db:/var/lib/postgresql/data
  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@vinsguru.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - 80:80
  • Create a product table as shown here
CREATE TABLE product(
   id serial PRIMARY KEY,
   description VARCHAR (500),
   price numeric (10,2) NOT NULL,
   qty_available integer NOT NULL
);
  • I inserted 1000 records into product table as shown here

Screen Shot 2019-11-10 at 9.22.53 PM

Project Setup:

  • Lets first create our application with the following dependencies.

  • No Redis for now. We will add it later. We would develop our application to directly talk to DB.

Screenshot from 2019-11-11 19-06-39

  • Product Entity
@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String description;
    private double price;
    private long qtyAvailable;

    //getters & setters

}
  • Product Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
  • Product DTO
public class ProductDto {

    private long id;
    private String description;
    private double price;
    private long quantityAvailable;

    // getters & setters

}
  • Product Service
public interface ProductService {
    Optional<ProductDto> getProduct(long id);
    void updateProduct(ProductDto productDto);
}
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Optional<ProductDto> getProduct(long id) {
        return this.productRepository
                .findById(id)
                .map(this::entityToDto);
    }

    @Override
    public void updateProduct(ProductDto productDto) {
        this.productRepository
                .findById(productDto.getId())
                .map(p -> this.setQuantityAvailable(p, productDto))
                .ifPresent(this.productRepository::save);
    }

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

    private Product setQuantityAvailable(Product product, ProductDto dto){
        product.setQtyAvailable(dto.getQuantityAvailable());
        return product;
    }
}
  • Product Controller
@RestController
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/product/{id}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable long id){
        return this.productService.getProduct(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.noContent().build());
    }

    @PatchMapping("/product")
    public void updateProduct(@RequestBody ProductDto dto){
        this.productService.updateProduct(dto);
    }

}
  • application.yaml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/productdb
    username: vinsguru
    password: admin
  • Run the application. Send a Get request and verify the response.
// http://localhost:8080/product/2

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

Performance Run 1:

I created a simple performance test script using JMeter. I will be testing the application with below workload.

  • 200 concurrent users would be accessing the application – call the GET product request for a random product id.
  • 20 concurrent requests would be made to the application to update the product available quantity (PATCH)

Screenshot from 2019-11-11 18-57-05

Results:

  • As we have only 1000 records, both my application and postgres are running on the same machine, We were able to make ~ 2500 GET requests/second which is good.

Screenshot from 2019-11-11 18-38-58

Now lets see how we can improve performance using Redis!

Spring Boot Redis:

  • Lets bring Redis into our application by adding the dependency.

  • The idea here is – The app will check with Redis first before contacting the Postgres!

spring boot redis

  • Add @EnableCaching
@EnableCaching
@SpringBootApplication
public class CacheAsideApplication {

    public static void main(String[] args) {
        SpringApplication.run(CacheAsideApplication.class, args);
    }

}
  • Run the Redis server along with PostgreSQL using docker-compose
version: "3"
services:
  postgres:
    image: postgres
    container_name: postgres
    environment:
      - POSTGRES_USER=vinsguru
      - POSTGRES_PASSWORD=admin
      - POSTGRES_DB=productdb
    volumes:
      - ./db:/var/lib/postgresql/data
    ports:
      - 5432:5432
  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@vinsguru.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    ports:
      - 80:80
  redis:
    container_name: redis
    image: redis
    ports:
      - 6379:6379
  redis-commander:
    container_name: redis-commander
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:redis:6379
    ports:
      - 8081:8081
  • Include Redis configuration in the application.yaml file
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/productdb
    username: vinsguru
    password: admin
  cache:
    type: redis
  redis:
    host: localhost
    port: 6379
  • Now at this point, Spring Boot is ready to contact Redis. However we will have to tell Spring Boot when to do and what type of information we would like to cache.

Cacheable & CacheEvict:

As usual Spring simplifies most of the things with its annotations!

  • Use @Cacheable on any method to cache the response the method returned!
  • If you want to remove the cache in some cases, use @CacheEvict.

For example, update Product Service class with @Cacheable and @CacheEvict as shown here.

  • @Cacheable – caches the response for the given product id
  • @CacheEvict – we need to remove the cache whenever there is an update to the product. Otherwise data would NOT be in sync. When the cache is removed, the next GET request for the product id would be updating the cache with latest information.
    @Override
    @Cacheable("product")
    public Optional<ProductDto> getProduct(long id) {
        return this.productRepository
                .findByDescription("Product"+id)
                .map(this::entityToDto);
    }

    @Override
    @CacheEvict(value = "product", key = "#productDto.id")
    public void updateProduct(ProductDto productDto) {
        this.productRepository
                .findById(productDto.getId())
                .map(p -> this.setQuantityAvailable(p, productDto))
                .ifPresent(this.productRepository::save);
    }

Performance Run 2:

I reran the same performance test. If you see the result, We were able to improve the performance of our application & make more than 6500 GET requests / second just by adding Redis with couple of annotations!

Screenshot from 2019-11-11 18-52-35

 

Summary:

We were able to successfully demo Spring Boot 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. Otherwise we will end up serving stale data.

Learn more about Spring Boot Redis.

The source code is available here.

Happy learning 🙂

Share This:


Viewing all articles
Browse latest Browse all 44

Latest Images

Trending Articles





Latest Images