Backend Development 18 min read

Mastering Spring Cache: From Hard‑Coded to Multi‑Level Redis Integration

This tutorial walks through the evolution from manual Redis calls to Spring Cache abstraction, explains AOP‑based proxying, details core annotations, demonstrates Caffeine and Redisson integration, explores list caching, and shows how to build a custom two‑level cache for high‑performance Java back‑ends.

Sanyou's Java Diary
Sanyou's Java Diary
Sanyou's Java Diary
Mastering Spring Cache: From Hard‑Coded to Multi‑Level Redis Integration

1 Hard Coding

Before using Spring Cache, the author manually cached user data with Redis commands, defining cache keys like "userId_" + userId and writing separate logic for read, write, update, and delete operations, which led to duplicated code and tight coupling between business logic and caching.

<code>@Autowire
private UserMapper userMapper;
@Autowire
private StringCommand stringCommand;
public User getUserById(Long userId) {
    String cacheKey = "userId_" + userId;
    User user = stringCommand.get(cacheKey);
    if (user != null) {
        return user;
    }
    user = userMapper.getUserById(userId);
    if (user != null) {
        stringCommand.set(cacheKey, user);
        return user;
    }
    // update and delete methods omitted for brevity
}
</code>

The drawbacks include repetitive key handling and high intrusion into business code.

2 Cache Abstraction

Spring Cache is not a concrete cache implementation but an abstraction ( Cache Abstraction ) that separates cache usage from the underlying provider.

2.1 Spring AOP

Spring AOP creates proxy objects (proxy‑based) so that method calls are intercepted; before and after the actual method execution the proxy can apply caching logic.

<code>Pojo pojo = new SimplePojo();
pojo.foo();
</code>
<code>ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
pojo.foo();
</code>

2.2 Cache Declaration

Spring Cache provides five annotations; the core three are:

@Cacheable – caches the method result based on parameters.

@CachePut – always executes the method and updates the cache.

@CacheEvict – removes entries from the cache.

2.2.1 @Cacheable

<code>@Cacheable(value="user_cache", key="#userId", unless="#result == null")
public User getUserById(Long userId) {
    return userMapper.getUserById(userId);
}
</code>

Key generation can be customized via keyGenerator or by implementing org.springframework.cache.interceptor.KeyGenerator .

<code>Object generate(Object target, Method method, Object... params);
</code>
<code>@Cacheable(value="user_cache", keyGenerator="myKeyGenerator", unless="#result == null")
public User getUserById(Long userId) { ... }
</code>

2.2.2 @CachePut

<code>@CachePut(value="user_cache", key="#user.id", unless="#result != null")
public User updateUser(User user) {
    userMapper.updateUser(user);
    return user;
}
</code>

2.2.3 @CacheEvict

<code>@CacheEvict(value="user_cache", key="#id")
public void deleteUserById(Long id) {
    userMapper.deleteUserById(id);
}
</code>

3 Getting Started Example

Create a Spring Boot project spring-cache-demo and add the cache starter.

3.1 Integrate Caffeine

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-cache&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;com.github.ben-manes.caffeine&lt;/groupId&gt;
  &lt;artifactId&gt;caffeine&lt;/artifactId&gt;
  &lt;version&gt;2.7.0&lt;/version&gt;
&lt;/dependency&gt;
</code>
<code>@Configuration
@EnableCaching
public class MyCacheConfig {
    @Bean
    public Caffeine caffeineConfig() {
        return Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(60, TimeUnit.MINUTES);
    }
    @Bean
    public CacheManager cacheManager(Caffeine caffeine) {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(caffeine);
        return manager;
    }
}
</code>

Business methods become concise:

<code>@Cacheable(value="user_cache", unless="#result == null")
public User getUserById(Long id) { return userMapper.getUserById(id); }

@CachePut(value="user_cache", key="#user.id", unless="#result == null")
public User updateUser(User user) { userMapper.updateUser(user); return user; }

@CacheEvict(value="user_cache", key="#id")
public void deleteUserById(Long id) { userMapper.deleteUserById(id); }
</code>

First call hits the database; subsequent calls retrieve data from the cache.

3.2 Integrate Redisson

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.Redisson&lt;/groupId&gt;
  &lt;artifactId&gt;Redisson&lt;/artifactId&gt;
  &lt;version&gt;3.12.0&lt;/version&gt;
&lt;/dependency&gt;
</code>
<code>@Bean(destroyMethod="shutdown")
public RedissonClient redisson() {
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay");
    return Redisson.create(config);
}

@Bean
CacheManager cacheManager(RedissonClient client) {
    Map<String, CacheConfig> cfg = new HashMap<>();
    cfg.put("user_cache", new CacheConfig(24 * 60 * 1000, 12 * 60 * 1000));
    return new RedissonSpringCacheManager(client, cfg);
}
</code>

Switching from Caffeine to Redisson only requires changing the CacheManager bean; business code stays unchanged.

3.3 List Caching

<code>@Cacheable(value="user_cache")
public List<User> getUserList(List<Long> idList) {
    return userMapper.getUserByIds(idList);
}
</code>

Spring Cache treats the whole list as a single cached value; list‑level granularity is not shared with individual item caches.

4 Custom Two‑Level Cache

Design includes MultiLevelCacheManager , MultiLevelChannel (wrapping Caffeine and Redisson), MultiLevelCache implementing org.springframework.cache.Cache , and MultiLevelCacheConfig for TTL settings.

<code>@Override
public ValueWrapper get(Object key) {
    Object result = getRawResult(key);
    return toValueWrapper(result);
}

public Object getRawResult(Object key) {
    logger.info("Query first‑level cache key:" + key);
    Object result = localCache.getIfPresent(key);
    if (result != null) return result;
    logger.info("Query second‑level cache key:" + key);
    result = redissonCache.getNativeCache().get(key);
    if (result != null) localCache.put(key, result);
    return result;
}
</code>
<code>public void put(Object key, Object value) {
    logger.info("Write first‑level cache key:" + key);
    localCache.put(key, value);
    logger.info("Write second‑level cache key:" + key);
    redissonCache.put(key, value);
}
</code>

Logs demonstrate the fallback from first‑level (Caffeine) to second‑level (Redisson) and eventual DB query.

5 When to Choose Spring Cache

Spring Cache shines in scenarios where cache granularity is moderate, such as portal homepages, ranking lists, or other read‑heavy, low‑real‑time‑requirement pages. For high‑concurrency, fine‑grained control, extensions like multi‑level caching, list caching, or cache listeners are needed, and projects like j2cache or jetcache can be consulted.

Javabackend-developmentRedisCachingcaffeinemulti-level cacheSpring Cache
Sanyou's Java Diary
Written by

Sanyou's Java Diary

Passionate about technology, though not great at solving problems; eager to share, never tire of learning!

0 followers
Reader feedback

How this landed with the community

login 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.