Skip to main content

Boosting Spring Boot Performance, Implementing Second Level Cache with Redis

· 6 min read
Huseyin BABAL
Software Developer

Introduction

Caching is a fundamental technique to improve the performance of applications by storing frequently accessed data in memory, thereby reducing the need for repeated database queries. In Spring Boot, caching is often implemented at two levels: the first level cache and the second level cache.

First Level and Second Level Cache

First Level Cache: This is the cache associated with the Hibernate session. It is enabled by default and works only within the scope of a session, meaning that the data cached is only available within a single transaction.

Second Level Cache: This is a more sophisticated cache that works at the session factory level. It is shared among all sessions, making it possible to cache data across transactions. This type of cache can significantly reduce database load and improve application performance.

In this blog post, we will focus on integrating the second level cache in a Spring Boot application using Redis.

Spring Starter Projects and Database Integration

For our Spring Boot application, we will use the following starter projects:

  • Spring Data JPA: For ORM and database interactions.
  • PostgreSQL: As our relational database.
  • Redis: For caching purposes.

We will also leverage two SaaS solutions:

  • Rapidapp for PostgreSQL: To quickly set up and manage our PostgreSQL database.
tip

Create a free database in Rapidapp Starter in seconds here

  • Upstash Redis: A managed Redis service optimized for low-latency data caching.
tip

Create a free Redis database in Upstash here

Spring Boot Application Setup

Application

This section contains application level configurations such as the application name, datasource, and jpa as shown below:

application.yaml
spring:
application:
name: sb-l2-redis
datasource:
driver-class-name: org.postgresql.Driver
url: <connection-string-from-rapidapp|or your own managed postgres url>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
cache:
use_second_level_cache: true

Line 5: JDBC driver class for PostgreSQL.

Line 6: Connection URL for the PostgreSQL database. You can obtain this from Rapidapp or your own managed PostgreSQL service. It should have a format like jdbc:postgresql://<host>:<port>/<database>?user=<user>&password=<password>&sslmode=require.

warning

We use connection string here for demo purposes, but it is not secure to use connection string in application.yaml. You should use environment variables (SPRING_DATASOURCE_URL) or secrets management tools to store sensitive information.

Line 10: This will create tables in PostgreSQL database automatically by using entities.

Line 15: Enable second level cache in Hibernate. We will visit this configuration later.

Entities

Let's create a simple Product entity that will be stored in the PostgreSQL database.

Product.java
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Cache(region = "products", usage = CacheConcurrencyStrategy.READ_WRITE)
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String description;
private BigDecimal price;
}
  1. @Entity annotation is used to mark the class as a JPA entity.
  2. @Cache annotation is used to enable caching for the entity.
  • region attribute is used to specify the cache name.
  • usage attribute is used to specify the cache concurrency strategy. The remaining attributes are coming from Lombok library to automatically generate getters, setters, and constructors.

Repository

Next, we will create a ProductRepository interface that extends JpaRepository to interact with the Product entity.

ProductRepository.java
interface ProductRepository extends JpaRepository<Product, Long> {}

Controller

Finally, let's set up a simple Spring Boot application endpoint. We will create a ProductController with endpoints to create, get, and list products.

ProductController.java
@RestController
@RequestMapping("/products")
class ProductController {

private final ProductRepository productRepository;

public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}

@GetMapping
public Iterable<Product> getProducts() {
return productRepository.findAll();
}

@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productRepository.findById(id).orElseThrow();
}

@PostMapping
public Product createProduct(@RequestBody CreateProductRequest request) {
Product product = Product.builder()
.name(request.getName())
.description(request.getDescription())
.price(request.getPrice())
.build();
return productRepository.save(product);
}
}

Generate Data

Right after running application, we can now generate some sample data with the following curl command.

curl \
--parallel \
-XPOST \
-H "Content-Type: application/json" \
http://localhost:8080/products\?\[1-200\] \
-d '{"name": "book", "description": "desc", "price": "100.3"}'

Line 2: --parallel flag is used to send multiple requests in parallel. Line 5: [1-200] is used to sent same payload 200 times.

Since we have spring.jpa.show_sql=true in our application.yaml, we can see 200 insert SQL statements in the console. In same way, we can see select SQL statements when we call GET /products/{id} endpoint. This is where second level cache comes into play.

Implementing Second Level Cache with Redis

Redis Client Configuration

First, we need to configure the Redis client in our Spring Boot application. We will use redisson as the Redis client library.

redisson.yaml
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
password: <redis_password>
subscriptionsPerConnection: 5
clientName: sb-l2-redis
address: <redis_address>
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 10
connectionPoolSize: 64
database: 0
dnsMonitoringInterval: 5000

Line 7: Redis password. You can obtain this from Upstash or your own managed Redis service.

Line 10: Redis server address. You can obtain this from Upstash or your own managed Redis service.

You can see the other configuration internals here. Now we have redis client configuration, and with the help of spring.jpa.hibernate.properties.cache.use_second_level_cache: true in application.yaml file, we can enable second level cache in Hibernate. The cache provider class is automatically injected in to the application with following dependency.

pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-hibernate-53</artifactId>
<version>3.30.0</version>
</dependency>

Cache Verification

After running the application, we can see that the select SQL statements are not executed when we call the GET /products/{id} endpoint second time. This is because the data is fetched from the Redis cache instead of the database.

Conclusion

Implementing a second level cache in your Spring Boot application using Redis can significantly improve performance by reducing database load. With the combination of Spring Data JPA, PostgreSQL, and Redis, you can achieve scalable and efficient caching solution. Using managed services like Rapidapp and Upstash for Redis further simplifies the setup and management of your infrastructure.

By following the steps outlined in this blog post, you can easily integrate a second level cache in your Spring Boot application and enjoy the benefits of faster data access and reduced database queries.

tip

You can find the complete source code for this project on GitHub.