Advanced SpringBoot Caching: How to Build a Custom CacheManager
The article explains why the default SpringBoot cache manager is insufficient for production, then walks through creating custom Caffeine and Redis CacheManager beans, configuring expiration, key prefixes, serialization, and multi‑level caching to solve issues like cache penetration, key collisions, and performance bottlenecks.
1. Spring Cache Core Mechanism and Default Pain Points
Spring Cache is not a cache component but a unified abstraction that hides the underlying implementation (JDK memory, Caffeine, Redis, Ehcache, Guava, etc.) behind annotations, allowing seamless switching.
The default cache manager has several critical drawbacks for production:
Global uniform TTL prevents fine‑grained control of different business caches.
No unified key prefix leads to easy key collisions across modules.
Redis uses JDK serialization by default, producing unreadable, non‑cross‑language data.
Cannot coexist multiple caches; local and distributed caches cannot be used together.
No eviction policy or maximum capacity, causing memory overflow.
Lacks dynamic TTL, cache monitoring, and custom eviction logic.
2. Custom Caffeine Local CacheManager
2.1 Why Not Use ConcurrentHashMap?
JDK native maps have no expiration, eviction, or LRU cleanup, and under high concurrency they easily cause memory overflow.
2.2 Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.benmanes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>2.3 Configuration
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.cache.annotation.EnableCaching;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean("caffeineCacheManager")
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// Core strategy: 10‑minute expiration, max 10,000 entries
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
// Removal listener for troubleshooting
.removalListener((key, value, cause) -> {
System.out.println("[Local cache eviction] key=" + key + ", cause=" + cause);
});
cacheManager.setCaffeine(caffeine);
// Allow null values to prevent cache‑penetration
cacheManager.setAllowNullValues(true);
return cacheManager;
}
}2.4 Usage
@Cacheable(value = "userLocalCache", key = "#id", cacheManager = "caffeineCacheManager")
@GetMapping("/user/local/{id}")
public String getUserLocal(@PathVariable Long id) {
System.out.println("Query database");
return "User static data:" + id;
}2.5 Suitable Scenarios
High‑frequency, rarely changing static data (dictionary, region, configuration).
Hot data that can tolerate brief inconsistency.
Off‑loading Redis to increase interface QPS.
3. Custom Redis Distributed CacheManager
3.1 Dependencies
<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>3.2 Full Configuration
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
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 org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching
public class RedisCacheConfig {
private GenericJackson2JsonRedisSerializer jsonSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
return new GenericJackson2JsonRedisSerializer(mapper);
}
@Bean("redisCacheManager")
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
// Global default configuration
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.prefixCacheNameWith("project:cache:")
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer()))
.disableCachingNullValues();
// Per‑cache TTL settings
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("tempCache", defaultConfig.entryTtl(Duration.ofMinutes(5)));
configMap.put("goodsCache", defaultConfig.entryTtl(Duration.ofHours(1)));
configMap.put("dictCache", defaultConfig.entryTtl(Duration.ofHours(24)));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configMap)
.allowInFlightCacheCreation(true)
.build();
}
}3.3 Six Production Benefits
Unified key prefix avoids cross‑module collisions.
JSON serialization makes Redis data readable and cross‑language compatible.
Per‑cache TTL isolates temporary, hot, and static data lifetimes.
Null‑value caching disabled to precisely control cache‑penetration strategy.
Consistent cache style across the team reduces maintenance cost.
Dynamic cache creation allows new cache names without configuration changes.
4. Coexistence of Multiple CacheManagers
4.1 Why Multi‑Level Cache?
Pure Redis incurs network I/O, becoming a performance bottleneck for high‑QPS interfaces. A local Caffeine cache provides zero‑latency, high‑concurrency reads, while Redis ensures distributed consistency.
4.2 How It Works
SpringBoot can hold several CacheManager beans. The cacheManager attribute of @Cacheable selects the desired manager; if omitted, the bean marked @Primary is used.
4.3 Example
// Local cache: ultra‑high‑frequency static data
@Cacheable(value = "dictCache", key = "#key", cacheManager = "caffeineCacheManager")
@GetMapping("/dict/{key}")
public String getDict(@PathVariable String key) {
return "Dictionary config";
}
// Redis cache: distributed shared data
@Cacheable(value = "goodsCache", key = "#id", cacheManager = "redisCacheManager")
@GetMapping("/goods/{id}")
public String getGoods(@PathVariable Long id) {
return "Goods detail";
}Conclusion
Custom CacheManagers transform a simple @Cacheable usage into a sophisticated caching architecture with multi‑level storage, differentiated TTLs, standardized keys, and high performance, effectively solving common production problems such as cache penetration, key conflicts, performance bottlenecks, and memory overflow.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
