Lab: Redis Caching with Spring Boot
Lab Overview: Redis Caching with Spring Boot
This lab will walk you through creating a simple Spring Boot application that uses Redis for caching. You will implement a book catalog service where Redis caching will significantly improve data retrieval performance.
Learning Objectives
By the end of this lab, you will:
- Understand the fundamentals of caching and why it’s important
- Set up a Redis server and connect to it from a Spring Boot application
- Implement Spring’s caching abstraction with Redis as the cache provider
- Measure and observe the performance benefits of caching
Lab Instructions
Part 1: Setting up the Environment
- Prerequisites:
- Java 17 or higher installed
- Maven or Gradle
- IDE (IntelliJ IDEA, Eclipse, or VS Code)
- Docker (for running Redis)
- Start Redis using Docker: Run the following command in your terminal:
docker run --name redis -p 6379:6379 -d redis
This starts a Redis server on the default port 6379.
- Create a new Spring Boot project:
- Visit Spring Initializr
- Select:
- Maven or Gradle project
- Java 17
- Spring Boot 3.x
- Dependencies: Spring Web, Spring Data Redis, Lombok (optional)
- Generate and unzip the project
Part 2: Creating the Application Structure
- Create a Book entity:
package com.example.redislab.model;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String title;
private String author;
private String isbn;
private double price;
// This toString helps us see when a Book is retrieved from cache vs database
@Override
public String toString() {
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", price=" + price +
'}';
}
}
- Create a Repository Interface (simulating a database):
package com.example.redislab.repository;
import com.example.redislab.model.Book;
import org.springframework.stereotype.Repository;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Repository
public class BookRepository {
private final Map<Long, Book> bookStore = new HashMap<>();
@PostConstruct
public void init() {
// Populate with sample data
bookStore.put(1L, new Book(1L, "To Kill a Mockingbird", "Harper Lee", "978-0061120084", 14.99));
bookStore.put(2L, new Book(2L, "1984", "George Orwell", "978-0451524935", 12.99));
bookStore.put(3L, new Book(3L, "The Great Gatsby", "F. Scott Fitzgerald", "978-0743273565", 11.99));
bookStore.put(4L, new Book(4L, "Pride and Prejudice", "Jane Austen", "978-0141439518", 9.99));
bookStore.put(5L, new Book(5L, "The Catcher in the Rye", "J.D. Salinger", "978-0316769488", 10.99));
}
public Book findById(Long id) {
// Simulate database latency
try {
Thread.sleep(1000); // 1 second delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Fetching book with ID: " + id + " from database");
return bookStore.get(id);
}
public Book save(Book book) {
// Simulate database latency
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
bookStore.put(book.getId(), book);
return book;
}
public void delete(Long id) {
bookStore.remove(id);
}
}
- Create a Service Layer:
package com.example.redislab.service;
import com.example.redislab.model.Book;
import com.example.redislab.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private final BookRepository bookRepository;
@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
// Add caching annotation - books will be cached by id
@Cacheable(value = "books", key = "#id")
public Book findById(Long id) {
return bookRepository.findById(id);
}
// Update cache when a book is updated
@CachePut(value = "books", key = "#book.id")
public Book updateBook(Book book) {
return bookRepository.save(book);
}
// Remove from cache when a book is deleted
@CacheEvict(value = "books", key = "#id")
public void deleteBook(Long id) {
bookRepository.delete(id);
}
// Clear entire cache
@CacheEvict(value = "books", allEntries = true)
public void clearAllCaches() {
System.out.println("Clearing all books from cache");
}
}
- Create a Controller:
package com.example.redislab.controller;
import com.example.redislab.model.Book;
import com.example.redislab.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookService bookService;
@Autowired
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/{id}")
public Book getBook(@PathVariable Long id) {
long startTime = System.currentTimeMillis();
Book book = bookService.findById(id);
long endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + " ms");
return book;
}
@PutMapping("/{id}")
public Book updateBook(@PathVariable Long id, @RequestBody Book book) {
book.setId(id);
return bookService.updateBook(book);
}
@DeleteMapping("/{id}")
public void deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
}
@DeleteMapping("/cache")
public void clearCache() {
bookService.clearAllCaches();
}
}
Part 3: Configure Redis and Spring Caching
- Update application.properties:
# Redis Configuration
spring.data.redis.host=localhost
spring.data.redis.port=6379
# Logging
logging.level.org.springframework.cache=TRACE
- Create Redis Configuration Class:
package com.example.redislab.config;
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 java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// Create a default cache configuration that expires after 10 minutes
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // Set Time-To-Live to 10 minutes
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer())); // Use JSON serialization
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(cacheConfig)
.build();
}
}
- Create application main class:
package com.example.redislab;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RedisLabApplication {
public static void main(String[] args) {
SpringApplication.run(RedisLabApplication.class, args);
}
}
Part 4: Running and Testing the Application
- Start the Spring Boot application:
./mvnw spring-boot:run
Or using Gradle:
./gradlew bootRun
- Test the endpoints:
Using a tool like curl
, Postman, or your web browser, make the following requests:
-
First request to get a book:
GET http://localhost:8080/api/books/1
-
Second request for the same book (should be much faster):
GET http://localhost:8080/api/books/1
-
Update a book:
PUT http://localhost:8080/api/books/1 Body: {"title":"To Kill a Mockingbird - Updated Edition","author":"Harper Lee","isbn":"978-0061120084","price":19.99}
-
Delete a book from cache:
DELETE http://localhost:8080/api/books/1
-
Clear the entire cache:
DELETE http://localhost:8080/api/books/cache
Part 5: Understanding the Results
- Observe the console output when making requests:
- The first time a book is requested, you should see a message about fetching from the database and a request time of around 1000ms.
- The second time, you should not see the database message, and the request should be much faster (a few milliseconds).
- Understand what’s happening:
- First request: Data is fetched from the “database” (our HashMap with simulated delay), then stored in Redis
- Subsequent requests: Data is retrieved from Redis cache, bypassing the slow database access
- After updating a book: Cache is updated with new data
- After deleting a book: Its entry is removed from the cache
- After clearing the cache: All books are removed from Redis
Lab Understanding
After completing the lab, explain:
- Why is caching important in web applications? What are the tradeoffs?
- What problems might arise if we don’t properly manage our cache (e.g., when data is updated)?
- How do the different cache annotations work?
@Cacheable
- Caches the result of the method@CachePut
- Updates the cache without affecting method execution@CacheEvict
- Removes entries from the cache
- What would happen if we didn’t use a serializable class with Redis?
- How might we implement more complex caching strategies (different TTLs for different types of data, etc.)?
Extensions
Additional challenges:
- Implement a cache hit counter to measure cache effectiveness
- Add a method to batch-load books and observe caching behavior with high volumes
- Configure different expiration times for different types of data
- Implement conditional caching (only cache certain books based on criteria)
- Create a simple front-end that demonstrates the speed difference in real-time
This lab provides a hands-on introduction to Redis caching with Spring Boot. The simulated database delay makes the benefits of caching immediately apparent, which helps reinforce why caching is valuable in real-world applications.
By Wahid Hamdi