Spring Boot Caching with Redis: Full Integration Guide

Spring Boot, a popular framework for building Java-based applications, provides powerful caching mechanisms that improve performance by storing frequently accessed data. Integrating with Redis, a high-performance in-memory data structure store, creates an optimized caching solution. This article explores integrating Spring Boot caching with Redis, covering setup, configuration, and best practices.

What is Caching and Why Redis?

Caching is a mechanism to store frequently accessed data in memory for quick retrieval. It reduces database access and enhances application speed and scalability. Redis is a preferred caching solution because:

  1. High Performance: Redis operates in-memory, ensuring ultra-low latency.
  2. Flexibility: Supports data structures like strings, lists, sets, and more.
  3. Scalability: Can handle millions of operations per second.
  4. Persistence Options: Can persist cached data to disk for durability.

Setting Up the Redis Server

The developer can create a Redis server on Docker following the tutorial.

Setting Up Spring Boot and Redis

Prerequisites

  • Java Development Kit (JDK) 8 or later.
  • A running Redis server.
  • Maven or Gradle for dependency management.

Step 1: Add Dependencies

Include the necessary dependencies in your pom.xml (for Maven):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
</dependencies>

Step 2: Spring Boot Configuration

Add Redis-specific configuration in application.properties or application.yml:

spring.redis.host=localhost
spring.redis.port=6379
spring.cache.type=redis

Alternatively, use application.yml:

spring:
  redis:
    host: localhost
    port: 6379
  cache:
    type: redis

For Redis cluster

spring:
  redis-cluster-nodes: 192.168.100.11:6379,192.168.100.12:6379,192.168.100.13:6379

Enabling Caching in Spring Boot

Spring Boot makes it easy to enable caching. Use the @EnableCaching annotation in your main application class:

@SpringBootApplication
@EnableCaching
public class RedisCachingApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisCachingApplication.class, args);
    }
}

Using Redis Cache in Spring Boot Application

Caching Service Implementation

1. Create a service with methods that use caching.

Use the @Cacheable@CacheEvict, and @CachePut annotations to manage cache behavior.

@Service
@RequiredArgsConstructor
public class UserProfileCacheService {

    private final UserProfileRepository userProfileRepository;

    @Cacheable(value = "users", key = "#uuid")
    public UserProfile getUserById(UUID uuid) {
        // Expensive database operation
        return userProfileRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @CachePut(value = "users", key = "#result.id")
    public UserProfile updateUser(UserProfile user) {
        return userProfileRepository.save(user);
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(UUID id) {
        userProfileRepository.deleteById(id);
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteCacheByKey(UUID id) {
        //
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearUsersCache() {
        // Clear operation
    }

}

Explain an annotation

  • @Cacheable: Caches the result of the method call.
  • @CacheEvict: Removes data from the cache.
  • @CachePut: Updates the cache without affecting the method execution.

2. Create a user profile bean.

@Data
@Entity
@Table(name = "user_profiles", schema = "public")
@NoArgsConstructor
public class UserProfile {

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

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "birth_date", nullable = false)
    private LocalDateTime birthDate;

    @Column(name = "sex", nullable = false)
    private Integer sex;

    @Column(name = "create_by", nullable = false)
    private UUID createBy;

    @Column(name = "create_date", nullable = false)
    private LocalDateTime createDate;

    @Column(name = "update_by")
    private UUID updateBy;

    @Column(name = "update_date")
    private LocalDateTime updateDate;

}

3. Create a user profile repository

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, UUID> {}

Redis Configuration for Caching

Redis can be further customized by creating a RedisCacheManager bean:

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper();

        // Register JavaTimeModule to handle Java 8 date/time types
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        // Configure ObjectMapper for proper typing
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY
        );

        // Configure JSON serializer with the custom ObjectMapper
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(60))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(serializer));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
    
}

For Redis Cluster Configuration for Caching

@Value("${spring.redis-cluster-nodes}")
private String[] redisClusterNodes;


@Bean
public RedisConnectionFactory redisConnectionFactory() {
    log.info("redis cluster {}", Arrays.asList(redisClusterNodes));
    // Create RedisClusterConfiguration using the nodes from the properties
    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
            Arrays.asList(redisClusterNodes)
    );
    return new LettuceConnectionFactory(clusterConfig);
}

Create a database

The developer creates a database following this tutorial.

data
table structure

application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: none
      naming:
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    url: jdbc:postgresql://localhost:5432/demo
    username: admin
    password: password
    driver-class-name: org.postgresql.Driver

To deploy an application on Docker, change the URL in the application.yml file.

jdbc:postgresql://172.21.0.1:5432/demo

Deploy Spring Boot Application on Docker

1. Directory structure

Directory structure

2. Create Dockerfile

# Stage 1: Build the application with Maven
FROM maven:3.9.9-amazoncorretto-21 AS build

# Set the working directory inside the container
WORKDIR /app

# Copy the pom.xml and src directory for building the app
COPY pom.xml .
COPY src ./src

# Run Maven to clean and build the application (skip tests if needed)
RUN mvn clean package -DskipTests

# Stage 2: Create the final image to run the app
FROM openjdk:21-jdk-slim

# Set the working directory inside the container
WORKDIR /app

# Copy the built JAR file from the build stage
COPY --from=build /app/target/*.jar app.jar

# Expose the port the app will run on
EXPOSE 8080

# Run the Spring Boot application
ENTRYPOINT ["java", "-jar", "app.jar"]

3. Create docker-compose.yml

services:
  spring-boot-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: spring-boot-container
    ports:
      - "8080:8080" # Map the container port to the host port
    environment:
      SPRING_PROFILES_ACTIVE: docker # Use the "docker" profile for Spring Boot (optional)
    volumes:
      - ./logs:/app/logs # Bind a volume for application logs (optional)
    networks:
      - redis_redis-network
      - postgres_my-network

networks:
  redis_redis-network:
    external: true
  postgres_my-network:
    external: true

Explain Network

Connects the spring-boot-app service to two external networks:

  • redis_redis-network: To communicate with a Redis service running in another Docker Compose environment.
  • postgres_my-network: To communicate with a PostgreSQL service running in another Docker Compose environment.

4. The developer can inspect Docker networks using the Docker command.

 docker network ls
NETWORK ID     NAME                            DRIVER    SCOPE
b09af9a41b15   bridge                          bridge    local
90e23212ded7   host                            host      local
61218b2c7675   none                            null      local
5483b4087e6b   postgres_my-network             bridge    local
51e470e2cc34   redis_redis-network             bridge    local

5. Deploy the Spring Boot application.

docker compose up

6. Check container status.

docker ps
CONTAINER ID   IMAGE                         COMMAND                  CREATED              STATUS              PORTS                                              NAMES
0e25469ba14d   programming-spring-boot-app   "java -jar app.jar"      About a minute ago   Up About a minute   0.0.0.0:8080->8080/tcp                             spring-boot-container
277135a9898e   redis/redisinsight:latest     "./docker-entry.sh n…"   23 hours ago         Up 43 minutes       5000/tcp, 0.0.0.0:5540->5540/tcp                   redisinsight
2c7dd97affb4   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6381->6379/tcp, 0.0.0.0:16381->16379/tcp   redis-node-3-1
7e8a53fa9502   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6383->6379/tcp                             redis-node-5-1
8dba4ab3eeae   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6380->6379/tcp, 0.0.0.0:16380->16379/tcp   redis-node-2-1
49c3147a86a6   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6382->6379/tcp                             redis-node-4-1
c1951110af9d   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6384->6379/tcp                             redis-node-6-1
ba8e5777905f   redis:latest                  "docker-entrypoint.s…"   23 hours ago         Up 4 minutes        0.0.0.0:6379->6379/tcp, 0.0.0.0:16379->16379/tcp   redis-node-1-1
5eec2b6b186f   postgres                      "docker-entrypoint.s…"   6 months ago         Up 43 minutes       0.0.0.0:5432->5432/tcp                             postgres-db-1

Monitoring and Debugging

Enable Redis CLI

Use Redis CLI to monitor cache operations:

redis-cli
127.0.0.1:6379> KEYS *

Enable Spring Boot Logs

Add logging configuration to monitor cache behavior:

logging.level.org.springframework.cache=DEBUG

Redis insight

http://localhost:5540

Testing the Cache

1. Run the Application: Start your Spring Boot application.

2. Access Endpoints: Use an API testing tool (e.g., Postman) or a simple REST client to test the caching.

@RestController
@RequestMapping("/api/v1/users-cache")
@Slf4j
@RequiredArgsConstructor
public class UserCacheController {

    private final UserProfileCacheService userProfileCacheService;

    private final CacheManager cacheManager;

    Random random = new Random();

    @GetMapping("/{id}")
    public ResponseEntity<UserProfile> getUserById(@PathVariable String id) {
        UUID uuid = UUID.fromString(id);
        UserProfile user = userProfileCacheService.getUserById(uuid);
        return ResponseEntity.ok(user);
    }

    @PostMapping("/{name}")
    public ResponseEntity<UserProfile> createUserProfile(@PathVariable String name) {
        // Create a demo UserProfile instance using @Builder
        UserProfile demoUserProfile = new UserProfile();
        demoUserProfile.setId(UUID.randomUUID());
        demoUserProfile.setFirstName(name);
        demoUserProfile.setLastName("Doe");
        demoUserProfile.setEmail(name.toLowerCase() + ".doe@example.com");

        long start = LocalDateTime.of(1990, 1, 1, 0, 0, 0, 0).toEpochSecond(ZoneOffset.UTC);
        long end = LocalDateTime.of(2022, 12, 31, 23, 59, 59, 999999999).toEpochSecond(ZoneOffset.UTC);

        // Generate a random number of seconds within the range
        long randomSeconds = ThreadLocalRandom.current().nextLong(start, end);

        // Convert the random seconds back to LocalDateTime
        LocalDateTime randomDateTime = LocalDateTime.ofEpochSecond(randomSeconds, 0, ZoneOffset.UTC);

        demoUserProfile.setBirthDate(randomDateTime);
        int randomSex = random.nextInt(2) + 1;
        demoUserProfile.setSex(randomSex);
        demoUserProfile.setCreateBy(UUID.randomUUID());
        demoUserProfile.setCreateDate(LocalDateTime.now());
        demoUserProfile.setUpdateBy(null);
        demoUserProfile.setUpdateDate(null);
        UserProfile user = userProfileCacheService.updateUser(demoUserProfile);
        return ResponseEntity.ok(user);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteUserProfile(@PathVariable String id) {
        UUID uuid = UUID.fromString(id);
        userProfileCacheService.deleteUser(uuid);
        return ResponseEntity.ok("delete success.");
    }

    @DeleteMapping
    public ResponseEntity<String> clearCache() {
        userProfileCacheService.clearUsersCache();
        return ResponseEntity.ok("clear cache success.");
    }

Test case: HTTP GET Method

In this test case, developers get users by UUID

1. GET User by ID:
/api/v1/users-cache/81f867c4–86e9–47e6–9f50-bbf22dfe13eb

request

2. In the console log application, connect to the database.

Hibernate: select up1_0.id,up1_0.birth_date,up1_0.create_by,up1_0.create_date,up1_0.email,up1_0.first_name,up1_0.last_name,up1_0.sex,up1_0.update_by,up1_0.update_date from public.user_profiles up1_0 where up1_0.id=?
response

3. Check the cache in Redis Insight.

http://localhost:5540/
cache

When developers request again, the application does not connect to the database, indicating that it uses a cache from Redis.

4. GET User by ID with a different UUID:
/api/v1/users-cache/

1e7a3085-8d0c-453f-b3d2-d1283557a34d
request
cache

Test case: HTTP POST Method

In the test scenario, the user sends a request to insert the database, and the Redis server saves the user profile in a cache using UUID as a key. The user sends a request to get data from the user profile. The Redis server sends cache data as response data.

1. POST create a user profile : /api/v1/users-cache/Charlene

request
response

2. Check the cache in Redis Insight.

cache

The cache key is the UUID of the data in the database.

3. Check Database

SELECT id, first_name, last_name, email, birth_date, sex, create_by, create_date, update_by, update_date
 FROM public.user_profiles where first_name = 'Charlene';
data

4. Check the log on Docker

spring-boot-container  | Hibernate: select up1_0.id,up1_0.birth_date,up1_0.create_by,up1_0.create_date,up1_0.email,up1_0.first_name,up1_0.last_name,up1_0.sex,up1_0.update_by,up1_0.update_date from public.user_profiles up1_0 where up1_0.id=?
spring-boot-container  | Hibernate: insert into public.user_profiles (birth_date,create_by,create_date,email,first_name,last_name,sex,update_by,update_date,id) values (?,?,?,?,?,?,?,?,?,?)

5. GET User by ID with UUID from POST request:
/api/v1/users-cache/

c9f35a2a-ab0d-4b43-82a2-454441dc578a
request
response

Response data from Spring Boot application using a Cache from the Redis server.

DELETE Test case

1. Delete a user profile using UUID : /api/v1/users-cache/

c9f35a2a-ab0d-4b43-82a2-454441dc578a
request
cache data before deletion
data in the database
response

2. Check the Redis server and Database

cache data after delete
data in the database

The cache and data in the database have been deleted.

DELETE All Test cases

1. Delete a user profile : /api/v1/users-cache/

Request to delete all cache
cache before clear
response

2. Check the Redis server.

cache after clear

All caches in the Redis Server have been deleted.

Best Practices

  1. Set TTL (Time-to-Live): Prevent stale data by configuring cache expiry.
  2. Use Efficient Keys: Use meaningful and unique keys to avoid collisions.
  3. Avoid Caching Sensitive Data: Ensure sensitive information is encrypted or not cached.
  4. Monitor Cache Performance: Use tools like RedisInsight to analyze cache metrics.

Conclusion

When integrated with Redis, Spring Boot’s caching capabilities create a robust, high-performance solution for modern applications. With minimal configuration, developers can achieve significant performance improvements. Following this article’s steps and best practices, you can seamlessly implement caching in your Spring Boot applications and leverage Redis for optimal performance.

Leave a Comment

Your email address will not be published. Required fields are marked *