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.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Implementing Multilevel Cache in Spring Boot with Redis and Caffeine

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CacheCaffeinespring-cachemultilevel-cachespring-boot
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.