Implementing Multilevel Cache in Spring Boot with Redis and Caffeine
This article explains how to build a three‑layer cache architecture for Spring Boot by integrating Redis as a distributed cache with Caffeine as a local cache, covering configuration, custom CacheManager implementation, message‑driven synchronization, and practical usage examples.
1. Background
Cache brings data closer to the consumer, speeding access and improving system performance. The mechanism loads data from the cache first; if the data is missing, it loads from a slower source such as a database and then synchronizes the result back to the cache.
Multilevel cache refers to caching at different layers of the system architecture to further improve access speed. The three layers discussed are gateway Nginx cache, distributed cache, and local cache. This article integrates a Redis distributed cache with a Caffeine local cache to form a two‑level cache.
First‑level cache (Caffeine): a high‑performance Java cache library using the Window TinyLfu eviction policy, offering near‑optimal hit rates. Advantages: data resides in application memory, so access is fast. Disadvantages: limited by application memory, no persistence (data lost on restart), and cannot synchronize across distributed nodes.
Second‑level cache (Redis): a high‑performance, highly available key‑value store supporting multiple data types and clustering. Advantages: supports many data types, easy horizontal scaling, persistence prevents data loss on restart, and provides a centralized cache that avoids synchronization issues between application servers. Disadvantage: each access incurs I/O to Redis.
By combining Redis and Caffeine, the shortcomings of each single cache are mitigated, achieving complementary benefits.
2. Integration Implementation
2.1 Idea
Spring already provides cache support through the Cache and CacheManager interfaces, but Spring Cache has two main limitations:
It only supports a single cache source, i.e., you can choose either Redis or Caffeine, but not both simultaneously.
Data consistency between cache layers (e.g., between application‑level cache and distributed cache) is not guaranteed.
To overcome these issues, we re‑implement the Cache and CacheManager interfaces to integrate Redis and Caffeine, thereby achieving a multilevel cache. The following diagram (originally in the article) illustrates the call flow of the multilevel cache.
2.2 Implementation
First, a configuration class MultilevelCacheProperties is defined to hold dynamic cache attributes and switches, making the cache plug‑in‑able.
<code>@ConfigurationProperties(prefix = "multilevel.cache")
@Data
public class MultilevelCacheProperties {
/** 一级本地缓存最大比例 */
private Double maxCapacityRate = 0.2;
/** 一级本地缓存与最大缓存初始化大小比例 */
private Double initRate = 0.5;
/** 消息主题 */
private String topic = "multilevel-cache-topic";
/** 缓存名称 */
private String name = "multilevel-cache";
/** 一级本地缓存名称 */
private String caffeineName = "multilevel-caffeine-cache";
/** 二级缓存名称 */
private String redisName = "multilevel-redis-cache";
/** 一级本地缓存过期时间(秒) */
private Integer caffeineExpireTime = 300;
/** 二级缓存过期时间(秒) */
private Integer redisExpireTime = 600;
/** 一级缓存开关 */
private Boolean caffeineSwitch = true;
}</code>The configuration class is enabled with
@EnableConfigurationProperties(MultilevelCacheProperties.class)and injected where needed.
Next, the custom cache implementation MultilevelCache extends AbstractValueAdaptingCache and delegates operations to both Redis and Caffeine caches.
<code>public class MultilevelCache extends AbstractValueAdaptingCache {
@Resource
private MultilevelCacheProperties multilevelCacheProperties;
@Resource
private RedisTemplate redisTemplate;
private RedisCache redisCache;
private CaffeineCache caffeineCache;
private ExecutorService cacheExecutor = new plasticeneThreadExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 20,
Runtime.getRuntime().availableProcessors() * 200,
"cache-pool");
public MultilevelCache(boolean allowNullValues, RedisCache redisCache, CaffeineCache caffeineCache) {
super(allowNullValues);
this.redisCache = redisCache;
this.caffeineCache = caffeineCache;
}
@Override
public String getName() {
return multilevelCacheProperties.getName();
}
@Override
public Object getNativeCache() {
return null;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
return (T) value;
}
@Override
public void put(@NonNull Object key, Object value) {
redisCache.put(key, value);
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, value);
}
}
@Override
public ValueWrapper putIfAbsent(@NonNull Object key, Object value) {
ValueWrapper vw = redisCache.putIfAbsent(key, value);
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, value);
}
return vw;
}
@Override
public void evict(Object key) {
redisCache.evict(key);
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(key, null);
}
}
@Override
public void clear() {
redisCache.clear();
if (multilevelCacheProperties.getCaffeineSwitch()) {
asyncPublish(null, null);
}
}
@Override
protected Object lookup(Object key) {
Assert.notNull(key, "key不可为空");
ValueWrapper value;
if (multilevelCacheProperties.getCaffeineSwitch()) {
value = caffeineCache.get(key);
if (Objects.nonNull(value)) {
log.info("查询caffeine 一级缓存 key:{}, 返回值是:{}", key, value.get());
return value.get();
}
}
value = redisCache.get(key);
if (Objects.nonNull(value)) {
log.info("查询redis 二级缓存 key:{}, 返回值是:{}", key, value.get());
if (multilevelCacheProperties.getCaffeineSwitch()) {
ValueWrapper finalValue = value;
cacheExecutor.execute(() -> caffeineCache.put(key, finalValue.get()));
}
return value.get();
}
return null;
}
/** Notify other nodes to clear local cache when cache changes */
void asyncPublish(Object key, Object value) {
cacheExecutor.execute(() -> {
CacheMessage cacheMessage = new CacheMessage();
cacheMessage.setCacheName(multilevelCacheProperties.getName());
cacheMessage.setKey(key);
cacheMessage.setValue(value);
redisTemplate.convertAndSend(multilevelCacheProperties.getTopic(), cacheMessage);
});
}
}</code>Supporting message classes and listeners are also provided:
<code>@Data
public class CacheMessage implements Serializable {
private String cacheName;
private Object key;
private Object value;
private Integer type;
}</code> <code>public class CaffeineCacheRemovalListener implements RemovalListener<Object, Object> {
@Override
public void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause cause) {
log.info("[移除缓存] key:{} reason:{}", k, cause.name());
// handle different removal causes if needed
}
}</code> <code>public class RedisCacheMessageListener implements MessageListener {
private CaffeineCache caffeineCache;
@Override
public void onMessage(Message message, byte[] pattern) {
log.info("监听的redis message: {}" + message.toString());
CacheMessage cacheMessage = JsonUtils.parseObject(message.toString(), CacheMessage.class);
if (Objects.isNull(cacheMessage.getKey())) {
caffeineCache.invalidate();
} else {
caffeineCache.evict(cacheMessage.getKey());
}
}
}</code>The auto‑configuration class registers beans for RedisTemplate, RedisCache, CaffeineCache, the custom MultilevelCache, and the message listeners, wiring them together based on the properties defined earlier.
3. Usage
Using the multilevel cache is straightforward: inject MultilevelCache and call put or get as needed.
<code>@RestController
@RequestMapping("/api/data")
@Api(tags = "api数据")
@Slf4j
public class ApiDataController {
@Resource
private MultilevelCache multilevelCache;
@GetMapping("/put/cache")
public void put() {
DataSource ds = new DataSource();
ds.setName("多级缓存");
ds.setType(1);
ds.setCreateTime(new Date());
ds.setHost("127.0.0.1");
multilevelCache.put("test-key", ds);
}
@GetMapping("/get/cache")
public DataSource get() {
return multilevelCache.get("test-key", DataSource.class);
}
}</code>4. Conclusion
The multilevel cache solution addresses the limitations of using a single cache by combining the speed of local Caffeine with the durability and scalability of Redis. Typical scenarios include permission checks—where role‑to‑permission mappings rarely change—and hierarchical organization data displayed in management system lists, both of which benefit greatly from reduced database load.
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
