google-site-verification: google61fe8ba583a51912.html
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