Spring Boot Caching Explained
Why Caching
- Reduce Database Load: Hot data served directly from cache
- Improve Response Speed: Memory access orders of magnitude faster than disk
- Lower System Load: Reduce redundant computations
- Better User Experience: Faster page loads
Spring Boot Cache Abstraction
Spring provides a cache abstraction supporting multiple implementations:
- ConcurrentMapCache: In-memory, single-node use
- Caffeine: High-performance local cache
- EhCache: Established caching framework
- Redis: Distributed cache
- Hazelcast: Distributed in-memory data grid
Approach 1: ConcurrentMapCache (In-Memory)
1. Enable Cache Support
java@Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); List<Cache> caches = new ArrayList<>(); caches.add(new ConcurrentMapCache("users")); caches.add(new ConcurrentMapCache("products")); caches.add(new ConcurrentMapCache("orders")); cacheManager.setCaches(caches); return cacheManager; } }
2. Using Cache Annotations
java@Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; /** * @Cacheable: Check cache first, execute method if miss, cache result */ @Cacheable(value = "users", key = "#id", unless = "#result == null") public User getUserById(Long id) { log.info("Querying user from database: {}", id); return userRepository.findById(id).orElse(null); } /** * @CachePut: Execute method first, then update cache */ @CachePut(value = "users", key = "#user.id") public User updateUser(User user) { log.info("Updating user: {}", user.getId()); return userRepository.save(user); } /** * @CacheEvict: Remove from cache */ @CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { log.info("Deleting user: {}", id); userRepository.deleteById(id); } /** * Clear entire cache region */ @CacheEvict(value = "users", allEntries = true) public void clearUserCache() { log.info("Clearing user cache"); } }
Approach 2: Caffeine High-Performance Cache
1. Add Dependencies
xml<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2. Configure Caffeine
java@Configuration @EnableCaching public class CaffeineCacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .expireAfterAccess(5, TimeUnit.MINUTES) .recordStats() ); cacheManager.setCacheNames(Arrays.asList("users", "products", "orders")); return cacheManager; } }
3. YAML Configuration (Spring Boot 2.7+)
yamlspring: cache: type: caffeine caffeine: spec: maximumSize=1000,expireAfterWrite=10m
Approach 3: Redis Distributed Cache
1. Add Dependencies
xml<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>
2. Configure Redis
yamlspring: redis: host: localhost port: 6379 cache: type: redis redis: time-to-live: 600000 cache-null-values: true
3. Configure RedisCacheManager
java@Configuration @EnableCaching public class RedisCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); Map<String, RedisCacheConfiguration> configMap = new HashMap<>(); configMap.put("users", defaultConfig.entryTtl(Duration.ofMinutes(30))); configMap.put("products", defaultConfig.entryTtl(Duration.ofMinutes(10))); configMap.put("orders", defaultConfig.entryTtl(Duration.ofMinutes(5))); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .withInitialCacheConfigurations(configMap) .transactionAware() .build(); } }
Cache Annotations Explained
@Cacheable
java@Cacheable( value = "users", key = "#id", condition = "#id > 0", unless = "#result == null" ) public User getUser(Long id) { return userRepository.findById(id).orElse(null); }
@CachePut
java@CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }
@CacheEvict
java@CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); } @CacheEvict(value = "users", allEntries = true) public void clearAllUsers() { // Clear all user cache }
Cache Penetration, Breakdown, and Avalanche Solutions
java@Service public class CacheSolutionService { @Autowired private StringRedisTemplate redisTemplate; /** * Solve cache penetration: Cache null values */ public User getUserWithNullCache(Long id) { String cacheKey = "users::" + id; String cached = redisTemplate.opsForValue().get(cacheKey); if ("null".equals(cached)) { return null; } User user = userRepository.findById(id).orElse(null); if (user == null) { redisTemplate.opsForValue().set(cacheKey, "null", 5, TimeUnit.MINUTES); } else { redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES); } return user; } /** * Solve cache breakdown: Distributed lock */ public User getUserWithLock(Long id) { String cacheKey = "users::" + id; String lockKey = "lock:users:" + id; String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } // Acquire distributed lock Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // Double-check cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, User.class); } User user = userRepository.findById(id).orElse(null); if (user != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES ); } return user; } finally { redisTemplate.delete(lockKey); } } return null; } /** * Solve cache avalanche: Random expiration time */ public List<Product> getProductsByCategory(Long categoryId) { List<Product> products = productRepository.findByCategoryId(categoryId); int randomExpire = 600 + (int)(Math.random() * 300); redisTemplate.opsForValue().set( "products::" + categoryId, JSON.toJSONString(products), randomExpire, TimeUnit.SECONDS ); return products; } }
Summary
| Cache Type | Use Case | Pros | Cons |
|---|---|---|---|
| ConcurrentMapCache | Single-node, dev/test | Simple, no dependencies | No distribution support |
| Caffeine | Single-node high performance | Extremely fast, feature-rich | No distribution support |
| Redis | Distributed systems | Distributed, persistent | Network overhead |
| EhCache | Enterprise applications | Comprehensive, clustering | Complex configuration |
Recommendations:
- Single-node applications: Caffeine
- Distributed applications: Redis
- Simple scenarios: ConcurrentMapCache