Redis Cache with Spring Data in Java

Redis is an in-memory data structure store that can be used as a database, cache, and message broker. When integrated with Spring Data, it provides a powerful caching solution that significantly improves application performance.


Why Use Redis with Spring?

  • Blazing Fast: In-memory data storage
  • Rich Data Structures: Strings, Hashes, Lists, Sets, Sorted Sets
  • Persistence: Optional disk persistence
  • Replication: Master-slave replication
  • High Availability: Redis Sentinel and Cluster support
  • Spring Integration: Seamless Spring Cache abstraction

Project Setup

Maven Dependencies

<dependencies>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Jedis Client (alternative to Lettuce) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Jackson for JSON serialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>

Application Configuration

# application.yml
spring:
cache:
type: redis
redis:
time-to-live: 300000  # 5 minutes in milliseconds
cache-null-values: false
key-prefix: "app:"
use-key-prefix: true
data:
redis:
host: localhost
port: 6379
password: 
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
max-wait: -1ms
timeout: 2000ms

Basic Configuration Class

@Configuration
@EnableCaching
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;
@Value("${spring.data.redis.port}")
private int redisPort;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
// Use Jackson for JSON serialization
Jackson2JsonRedisSerializer<Object> serializer = 
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))  // Default TTL
.disableCachingNullValues()
.prefixCacheNameWith("app:");
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withInitialCacheConfigurations(getCacheConfigurations())
.build();
}
private Map<String, RedisCacheConfiguration> getCacheConfigurations() {
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
// Different TTL for different caches
cacheConfigs.put("users", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)));
cacheConfigs.put("products", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)));
cacheConfigs.put("config", RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1)));
return cacheConfigs;
}
}

Entity Classes

// User Entity
public class User implements Serializable {
private Long id;
private String username;
private String email;
private boolean active;
private LocalDateTime createdAt;
// Constructors
public User() {}
public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
this.createdAt = LocalDateTime.now();
this.active = true;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
@Override
public String toString() {
return String.format("User{id=%d, username='%s', email='%s'}", id, username, email);
}
}
// Product Entity
public class Product implements Serializable {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stock;
private String category;
// Constructors, getters, setters
public Product() {}
public Product(Long id, String name, String description, BigDecimal price, Integer stock) {
this.id = id;
this.name = name;
this.description = description;
this.price = price;
this.stock = stock;
}
// Getters and setters...
}

Repository with Redis Cache

Example 1: User Service with Cache Annotations

@Service
@Slf4j
public class UserService {
private final UserRepository userRepository;
// Simulating database calls
private final Map<Long, User> userDatabase = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
initializeSampleData();
}
private void initializeSampleData() {
saveUserWithoutCache(new User(null, "john_doe", "[email protected]"));
saveUserWithoutCache(new User(null, "jane_smith", "[email protected]"));
saveUserWithoutCache(new User(null, "bob_wilson", "[email protected]"));
}
private void saveUserWithoutCache(User user) {
user.setId(idGenerator.getAndIncrement());
userDatabase.put(user.getId(), user);
}
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
log.info("Fetching user from database: {}", id);
simulateSlowService(); // Simulate database latency
return userDatabase.get(id);
}
@Cacheable(value = "users", key = "#username")
public User getUserByUsername(String username) {
log.info("Fetching user by username from database: {}", username);
simulateSlowService();
return userDatabase.values().stream()
.filter(user -> user.getUsername().equals(username))
.findFirst()
.orElse(null);
}
@CachePut(value = "users", key = "#user.id")
public User saveUser(User user) {
log.info("Saving user: {}", user.getUsername());
if (user.getId() == null) {
user.setId(idGenerator.getAndIncrement());
}
userDatabase.put(user.getId(), user);
return user;
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
log.info("Deleting user: {}", id);
userDatabase.remove(id);
}
@CacheEvict(value = "users", allEntries = true)
public void evictAllUsersCache() {
log.info("Evicting all users cache");
}
@Cacheable(value = "users")
public List<User> getAllUsers() {
log.info("Fetching all users from database");
simulateSlowService();
return new ArrayList<>(userDatabase.values());
}
private void simulateSlowService() {
try {
Thread.sleep(1000); // Simulate 1 second database call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

Example 2: Product Service with Conditional Caching

@Service
@Slf4j
public class ProductService {
private final Map<Long, Product> productDatabase = new ConcurrentHashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);
public ProductService() {
initializeSampleData();
}
private void initializeSampleData() {
saveProductWithoutCache(new Product(null, "Laptop", "High-performance laptop", 
new BigDecimal("999.99"), 10));
saveProductWithoutCache(new Product(null, "Mouse", "Wireless mouse", 
new BigDecimal("29.99"), 50));
saveProductWithoutCache(new Product(null, "Keyboard", "Mechanical keyboard", 
new BigDecimal("79.99"), 30));
}
private void saveProductWithoutCache(Product product) {
product.setId(idGenerator.getAndIncrement());
productDatabase.put(product.getId(), product);
}
@Cacheable(value = "products", key = "#id", 
unless = "#result == null or #result.stock < 5")
public Product getProductById(Long id) {
log.info("Fetching product from database: {}", id);
simulateSlowService();
return productDatabase.get(id);
}
@Cacheable(value = "products", key = "'all'")
public List<Product> getAllProducts() {
log.info("Fetching all products from database");
simulateSlowService();
return new ArrayList<>(productDatabase.values());
}
@CachePut(value = "products", key = "#product.id")
public Product saveProduct(Product product) {
log.info("Saving product: {}", product.getName());
if (product.getId() == null) {
product.setId(idGenerator.getAndIncrement());
}
productDatabase.put(product.getId(), product);
// Also evict the "all products" cache
evictAllProductsCache();
return product;
}
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
log.info("Deleting product: {}", id);
productDatabase.remove(id);
evictAllProductsCache();
}
@CacheEvict(value = "products", key = "'all'")
public void evictAllProductsCache() {
log.info("Evicting all products cache");
}
@Cacheable(value = "products", key = "'category:' + #category")
public List<Product> getProductsByCategory(String category) {
log.info("Fetching products by category from database: {}", category);
simulateSlowService();
return productDatabase.values().stream()
.filter(product -> category.equals(product.getCategory()))
.collect(Collectors.toList());
}
@Caching(evict = {
@CacheEvict(value = "products", key = "#id"),
@CacheEvict(value = "products", key = "'all'")
})
public void updateProductStock(Long id, Integer newStock) {
log.info("Updating product stock: {} to {}", id, newStock);
Product product = productDatabase.get(id);
if (product != null) {
product.setStock(newStock);
}
}
private void simulateSlowService() {
try {
Thread.sleep(800); // Simulate 800ms database call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

Custom Redis Repository

Example 3: Custom Redis Operations

@Repository
public class UserSessionRepository {
private static final String SESSION_KEY_PREFIX = "session:";
private static final String USER_SESSIONS_KEY = "user:sessions:";
private final RedisTemplate<String, Object> redisTemplate;
private final ValueOperations<String, Object> valueOperations;
private final HashOperations<String, String, Object> hashOperations;
public UserSessionRepository(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOperations = redisTemplate.opsForValue();
this.hashOperations = redisTemplate.opsForHash();
}
public void saveSession(UserSession session) {
String sessionKey = SESSION_KEY_PREFIX + session.getSessionId();
String userSessionsKey = USER_SESSIONS_KEY + session.getUserId();
// Store session data
hashOperations.put(sessionKey, "userId", session.getUserId());
hashOperations.put(sessionKey, "username", session.getUsername());
hashOperations.put(sessionKey, "createdAt", session.getCreatedAt().toString());
hashOperations.put(sessionKey, "lastAccessed", session.getLastAccessed().toString());
// Set TTL for session
redisTemplate.expire(sessionKey, Duration.ofHours(2));
// Add to user's sessions set
redisTemplate.opsForSet().add(userSessionsKey, session.getSessionId());
redisTemplate.expire(userSessionsKey, Duration.ofHours(2));
}
public UserSession getSession(String sessionId) {
String sessionKey = SESSION_KEY_PREFIX + sessionId;
Map<String, Object> sessionData = hashOperations.entries(sessionKey);
if (sessionData.isEmpty()) {
return null;
}
return UserSession.builder()
.sessionId(sessionId)
.userId((String) sessionData.get("userId"))
.username((String) sessionData.get("username"))
.createdAt(LocalDateTime.parse((String) sessionData.get("createdAt")))
.lastAccessed(LocalDateTime.parse((String) sessionData.get("lastAccessed")))
.build();
}
public void updateLastAccessed(String sessionId) {
String sessionKey = SESSION_KEY_PREFIX + sessionId;
hashOperations.put(sessionKey, "lastAccessed", LocalDateTime.now().toString());
redisTemplate.expire(sessionKey, Duration.ofHours(2)); // Refresh TTL
}
public void deleteSession(String sessionId) {
String sessionKey = SESSION_KEY_PREFIX + sessionId;
// Get user ID before deleting
String userId = (String) hashOperations.get(sessionKey, "userId");
// Delete session
redisTemplate.delete(sessionKey);
// Remove from user's sessions
if (userId != null) {
String userSessionsKey = USER_SESSIONS_KEY + userId;
redisTemplate.opsForSet().remove(userSessionsKey, sessionId);
}
}
public Set<String> getUserSessions(String userId) {
String userSessionsKey = USER_SESSIONS_KEY + userId;
return redisTemplate.opsForSet().members(userSessionsKey);
}
@Data
@Builder
public static class UserSession {
private String sessionId;
private String userId;
private String username;
private LocalDateTime createdAt;
private LocalDateTime lastAccessed;
}
}

REST Controller with Caching

Example 4: REST API with Cache Management

@RestController
@RequestMapping("/api")
@Slf4j
public class UserController {
private final UserService userService;
private final ProductService productService;
public UserController(UserService userService, ProductService productService) {
this.userService = userService;
this.productService = productService;
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
log.info("GET /users/{}", id);
User user = userService.getUserById(id);
return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
}
@GetMapping("/users/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
log.info("GET /users/username/{}", username);
User user = userService.getUserByUsername(username);
return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
}
@GetMapping("/users")
public List<User> getAllUsers() {
log.info("GET /users");
return userService.getAllUsers();
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
log.info("POST /users - {}", user.getUsername());
return userService.saveUser(user);
}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
log.info("PUT /users/{}", id);
User existing = userService.getUserById(id);
if (existing == null) {
return ResponseEntity.notFound().build();
}
user.setId(id);
return ResponseEntity.ok(userService.saveUser(user));
}
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
log.info("DELETE /users/{}", id);
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/cache/users/evict")
public ResponseEntity<String> evictUserCache() {
log.info("POST /cache/users/evict");
userService.evictAllUsersCache();
return ResponseEntity.ok("User cache evicted");
}
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
log.info("GET /products/{}", id);
Product product = productService.getProductById(id);
return product != null ? ResponseEntity.ok(product) : ResponseEntity.notFound().build();
}
@GetMapping("/products")
public List<Product> getAllProducts() {
log.info("GET /products");
return productService.getAllProducts();
}
}

Testing the Cache

Example 5: Integration Tests

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class RedisCacheIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
@Autowired
private CacheManager cacheManager;
@Test
@Order(1)
void testUserCache() {
// First call - should hit database
long startTime = System.currentTimeMillis();
User user1 = userService.getUserById(1L);
long firstCallTime = System.currentTimeMillis() - startTime;
// Second call - should hit cache (much faster)
startTime = System.currentTimeMillis();
User user2 = userService.getUserById(1L);
long secondCallTime = System.currentTimeMillis() - startTime;
assertThat(user1).isEqualTo(user2);
assertThat(secondCallTime).isLessThan(firstCallTime / 2); // Should be much faster
// Verify cache was used
Cache usersCache = cacheManager.getCache("users");
assertThat(usersCache).isNotNull();
}
@Test
@Order(2)
void testCacheEviction() {
// Populate cache
userService.getUserById(2L);
// Verify cache contains the user
Cache usersCache = cacheManager.getCache("users");
assertThat(usersCache.get(2L)).isNotNull();
// Delete user - should evict from cache
userService.deleteUser(2L);
// Verify cache eviction
assertThat(usersCache.get(2L)).isNull();
}
@Test
@Order(3)
void testConditionalCaching() {
Product product = productService.getProductById(1L);
assertThat(product).isNotNull();
// Update stock to low value (should not be cached due to 'unless' condition)
productService.updateProductStock(1L, 3);
// Clear cache and verify low-stock product is not cached
productService.evictAllProductsCache();
// This should not be cached due to stock < 5
productService.getProductById(1L);
productService.getProductById(1L); // Second call should still hit DB
}
}

Monitoring and Metrics

Example 6: Cache Monitoring

@Component
@Slf4j
public class CacheMetrics {
@Autowired
private CacheManager cacheManager;
@Scheduled(fixedRate = 60000) // Every minute
public void logCacheStatistics() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache instanceof RedisCache) {
RedisCache redisCache = (RedisCache) cache;
// Log cache information
log.info("Cache: {}, Native Cache: {}", cacheName, 
redisCache.getNativeCache().getClass().getSimpleName());
}
});
}
public Map<String, Object> getCacheInfo() {
Map<String, Object> cacheInfo = new HashMap<>();
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
Map<String, Object> info = new HashMap<>();
info.put("type", cache.getClass().getSimpleName());
info.put("nativeCache", cache.getNativeCache().getClass().getSimpleName());
cacheInfo.put(cacheName, info);
});
return cacheInfo;
}
}

Best Practices

  1. Key Design: Use meaningful, consistent cache keys
  2. TTL Strategy: Set appropriate time-to-live values
  3. Serialization: Use efficient serialization (JSON, Protobuf)
  4. Memory Management: Monitor Redis memory usage
  5. Cache Warming: Pre-load frequently accessed data
  6. Eviction Policies: Implement proper cache eviction strategies
  7. Monitoring: Track cache hit/miss ratios
  8. Fallback: Implement fallback mechanisms for cache failures

Conclusion

Redis with Spring Data provides a robust caching solution that:

Key Benefits:

  • Performance: Sub-millisecond response times
  • Scalability: Horizontal scaling with Redis Cluster
  • Flexibility: Rich data structures and operations
  • Integration: Seamless Spring Cache abstraction
  • Persistence: Optional data durability

Common Use Cases:

  • Session storage
  • API response caching
  • Database query caching
  • Rate limiting
  • Real-time analytics
  • Message brokering

Spring Cache Annotations:

  • @Cacheable: Method result caching
  • @CachePut: Update cache without interfering method execution
  • @CacheEvict: Remove entries from cache
  • @Caching: Group multiple cache operations
  • @CacheConfig: Shared cache configuration

By implementing Redis caching with Spring Data, you can significantly improve your application's performance, reduce database load, and provide a better user experience.


Production Considerations: Use Redis Sentinel for high availability, Redis Cluster for horizontal scaling, and implement proper monitoring and alerting for your cache infrastructure.

Leave a Reply

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


Macro Nepal Helper