Skip to main content

TP14.1: Caching with Redis

Objectives of the Lab​

  • Configure Redis with Spring Boot
  • Implement caching on a REST API
  • Measure the impact on performance
  • Manage cache invalidation

Estimated duration: 1h


Architecture​

Client --> API --> Cache (Redis) --> Database
|
+--> Cache Hit: immediate return
|
+--> Cache Miss: query DB, cache result

Part 1: Configuration (15 min)​

1.1 Start Redis with Docker​

docker run -d --name redis -p 6379:6379 redis:7-alpine

1.2 Maven Dependencies​

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

1.3 Configuration​

# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true

data:
redis:
host: localhost
port: 6379

cache:
type: redis
redis:
time-to-live: 300000 # 5 minutes

1.4 Java Configuration​

package com.example.cache.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 CacheConfig {

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
.disableCachingNullValues();

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

Part 2: Entity and Repository (10 min)​

2.1 Entity Product​

package com.example.cache.model;

import jakarta.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "products")
public class Product implements Serializable {

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

private String name;
private String description;
private Double price;
private Integer stock;

public Product() {}

public Product(String name, String description, Double price, Integer stock) {
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}

// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }

public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }

public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
}

2.2 Repository​

package com.example.cache.repository;

import com.example.cache.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContaining(String name);
}

Part 3: Service with Cache (20 min)​

3.1 Service​

package com.example.cache.service;

import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

private static final Logger log = LoggerFactory.getLogger(ProductService.class);

private final ProductRepository productRepository;

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

@Cacheable(value = "products", key = "#id")
public Product getProductById(Long id) {
log.info("Fetching product {} from DATABASE", id);
simulateSlowQuery();
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));
}

@Cacheable(value = "productList")
public List<Product> getAllProducts() {
log.info("Fetching ALL products from DATABASE");
simulateSlowQuery();
return productRepository.findAll();
}

@CachePut(value = "products", key = "#result.id")
@CacheEvict(value = "productList", allEntries = true)
public Product createProduct(Product product) {
log.info("Creating product: {}", product.getName());
return productRepository.save(product);
}

@CachePut(value = "products", key = "#id")
@CacheEvict(value = "productList", allEntries = true)
public Product updateProduct(Long id, Product productDetails) {
log.info("Updating product: {}", id);
Product product = productRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Product not found: " + id));

product.setName(productDetails.getName());
product.setDescription(productDetails.getDescription());
product.setPrice(productDetails.getPrice());
product.setStock(productDetails.getStock());

return productRepository.save(product);
}

@CacheEvict(value = {"products", "productList"}, allEntries = true)
public void deleteProduct(Long id) {
log.info("Deleting product: {}", id);
productRepository.deleteById(id);
}

@CacheEvict(value = {"products", "productList"}, allEntries = true)
public void clearCache() {
log.info("Cache cleared");
}

private void simulateSlowQuery() {
try {
Thread.sleep(1000); // Simulate a slow query
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

Part 4: Controller (10 min)​

package com.example.cache.controller;

import com.example.cache.model.Product;
import com.example.cache.service.ProductService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
long start = System.currentTimeMillis();
List<Product> products = productService.getAllProducts();
long duration = System.currentTimeMillis() - start;

return ResponseEntity.ok()
.header("X-Response-Time", duration + "ms")
.body(products);
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
long start = System.currentTimeMillis();
Product product = productService.getProductById(id);
long duration = System.currentTimeMillis() - start;

return ResponseEntity.ok()
.header("X-Response-Time", duration + "ms")
.body(product);
}

@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(productService.createProduct(product));
}

@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@RequestBody Product product) {
return ResponseEntity.ok(productService.updateProduct(id, product));
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}

@PostMapping("/cache/clear")
public ResponseEntity<Map<String, String>> clearCache() {
productService.clearCache();
return ResponseEntity.ok(Map.of("message", "Cache cleared"));
}
}

Part 5: Test Data (5 min)​

package com.example.cache;

import com.example.cache.model.Product;
import com.example.cache.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class DataInitializer implements CommandLineRunner {

private final ProductRepository productRepository;

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

@Override
public void run(String... args) {
productRepository.save(new Product("Laptop", "High-performance laptop", 999.99, 50));
productRepository.save(new Product("Phone", "Smartphone 5G", 699.99, 100));
productRepository.save(new Product("Tablet", "10-inch tablet", 499.99, 75));
productRepository.save(new Product("Headphones", "Wireless headphones", 199.99, 200));
productRepository.save(new Product("Watch", "Smart watch", 299.99, 150));
}
}

Part 6: Tests (15 min)​

6.1 Test the Cache​

# First call (Cache Miss - ~1 second)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 1002ms

# Second call (Cache Hit - ~5ms)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 5ms

# Third call (Cache Hit)
curl -i http://localhost:8080/api/products/1
# X-Response-Time: 3ms

6.2 Test Script​

#!/bin/bash
echo "=== Test Cache Performance ==="

echo "First call (Cache Miss):"
time curl -s http://localhost:8080/api/products/1 > /dev/null

echo "Second call (Cache Hit):"
time curl -s http://localhost:8080/api/products/1 > /dev/null

echo "Third call (Cache Hit):"
time curl -s http://localhost:8080/api/products/1 > /dev/null

echo ""
echo "=== Clear Cache ==="
curl -X POST http://localhost:8080/api/products/cache/clear

echo ""
echo "After clear (Cache Miss):"
time curl -s http://localhost:8080/api/products/1 > /dev/null

6.3 Check Redis​

# Connect to Redis CLI
docker exec -it redis redis-cli

# List keys
KEYS *

# View a value
GET "products::1"

# TTL of a key
TTL "products::1"

# Delete all
FLUSHALL

Validation Checklist​

  • Redis starts on port 6379
  • First call: ~1 second (Cache Miss)
  • Second call: ~5ms (Cache Hit)
  • Update invalidates the cache
  • Keys are visible in Redis

TP14.2: Messaging RabbitMQ

Back to Module 14