Boosting Spring Boot Performance, Implementing Second Level Cache with Redis
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.
Create a free database in Rapidapp Starter in seconds here
- Upstash Redis: A managed Redis service optimized for low-latency data caching.
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:
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
.
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.
@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;
}
@Entity
annotation is used to mark the class as a JPA entity.@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.
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.
@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.
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.
<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.
You can find the complete source code for this project on GitHub.