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.
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><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
</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><dependency>
<groupId>org.Redisson</groupId>
<artifactId>Redisson</artifactId>
<version>3.12.0</version>
</dependency>
</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.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
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.