乐闻世界logo
搜索文章和话题

How to implement caching in Spring Boot?

3月6日 21:58

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+)

yaml
spring: 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

yaml
spring: 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 TypeUse CaseProsCons
ConcurrentMapCacheSingle-node, dev/testSimple, no dependenciesNo distribution support
CaffeineSingle-node high performanceExtremely fast, feature-richNo distribution support
RedisDistributed systemsDistributed, persistentNetwork overhead
EhCacheEnterprise applicationsComprehensive, clusteringComplex configuration

Recommendations:

  • Single-node applications: Caffeine
  • Distributed applications: Redis
  • Simple scenarios: ConcurrentMapCache
标签:Spring Boot