Master Multi‑Level Caching in Spring Boot 3 with Custom Annotations
Learn how to boost Spring Boot 3 application performance by implementing a multi‑level cache that combines local and Redis storage, using custom @MultiLevelCache annotations and AOP aspects, complete with cache clearing mechanisms, code examples, and testing instructions.
1. Introduction
Cache mechanism is essential for improving system performance and reducing database load. This article demonstrates a multi‑level cache that combines local in‑memory cache and Redis, simplified by a custom @MultiLevelCache annotation.
2. Practical Example
2.1 Custom Annotation
<code>@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiLevelCache {
// cache key expression (SpEL)
String key() default "";
// local cache TTL (seconds)
long localTtl() default 30;
// Redis cache TTL (seconds)
long redisTtl() default 300;
// whether to cache null values
boolean cacheNull() default true;
// null value cache TTL (seconds)
long nullTtl() default 60;
}
</code>2.2 Multi‑Level Cache Aspect
<code>@Aspect
@Component
public class MultiLevelCacheAspect {
private static final Logger logger = LoggerFactory.getLogger(MultiLevelCache.class);
private final Cache<String, Object> localCache;
private final StringRedisTemplate redisTemplate;
private final RedissonClient redissonClient;
private final ObjectMapper objectMapper;
private static final String NULL_VALUE = "";
public MultiLevelCacheAspect(Cache<String, Object> localCache,
StringRedisTemplate redisTemplate,
RedissonClient redissonClient,
ObjectMapper objectMapper) {
this.localCache = localCache;
this.redisTemplate = redisTemplate;
this.redissonClient = redissonClient;
this.objectMapper = objectMapper;
}
@Around("@annotation(multiLevelCache)")
public Object cacheAround(ProceedingJoinPoint pjp, MultiLevelCache multiLevelCache) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
String cacheKey = generateCacheKey(signature, pjp.getArgs(), multiLevelCache.key());
// 1. Local cache
Object result = getFromLocalCache(cacheKey);
if (result != null) {
logger.info("Local cache hit: {}", cacheKey);
return convertNullValue(result);
}
// 2. Redis cache
result = getFromRedis(cacheKey);
if (result != null) {
logger.info("Redis cache hit: {}", cacheKey);
if (NULL_VALUE.equals(result)) {
return null;
}
result = objectMapper.readValue(result.toString(), signature.getReturnType());
localCache.put(cacheKey, result);
return convertNullValue(result);
}
// 3. Distributed lock
RLock lock = redissonClient.getLock(cacheKey + ":lock");
try {
if (lock.tryLock()) {
// double‑check cache
Object localFallback = localCache.getIfPresent(cacheKey);
if (localFallback != null) {
return convertNullValue(localFallback);
}
result = getFromRedis(cacheKey);
if (result != null) {
return convertNullValue(result);
}
// 4. Execute target method (DB query)
logger.info("Querying database");
result = pjp.proceed();
// 5. Update caches
cacheResult(cacheKey, result, multiLevelCache);
return result;
} else {
return getFallbackValue(signature.getReturnType());
}
} finally {
lock.unlock();
}
}
// Helper methods (generateCacheKey, getFromLocalCache, getFromRedis,
// cacheResult, convertNullValue, getFallbackValue) omitted for brevity
}
</code>2.3 Business Code and Test Interface
<code>@MultiLevelCache(key = "#id", localTtl = 10, redisTtl = 60, cacheNull = true, nullTtl = 30)
public Product query(Long id) {
int t = new Random().nextInt(10000);
return new Product(id, "Spring Boot3 Practical Cache Example - " + t,
BigDecimal.valueOf(t));
}
</code> <code>@GetMapping("/{id}")
public ResponseEntity<Product> query(@PathVariable Long id) {
return ResponseEntity.ok(productService.query(id));
}
</code>Running the above endpoints shows that data is first retrieved from the local cache, then from Redis after the local entry expires.
2.4 Cache Clearing
Custom Annotation
<code>@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheClear {
// cache key expression (SpEL)
String key() default "";
}
</code>Cache‑Clear Aspect
<code>@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class CacheClearAspect {
private static final Logger logger = LoggerFactory.getLogger(CacheClearAspect.class);
private final Cache<String, Object> localCache;
private final StringRedisTemplate redisTemplate;
public CacheClearAspect(Cache<String, Object> localCache,
StringRedisTemplate redisTemplate) {
this.localCache = localCache;
this.redisTemplate = redisTemplate;
}
@Around("@annotation(cacheClear)")
public Object cacheClearAround(ProceedingJoinPoint pjp, CacheClear cacheClear) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
String cacheKey = generateCacheKey(signature, pjp.getArgs(), cacheClear.key());
Object ret = pjp.proceed();
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
clearCache(cacheKey);
}
});
} else {
clearCache(cacheKey);
}
return ret;
}
private void clearCache(String cacheKey) {
try {
localCache.invalidate(cacheKey);
redisTemplate.delete(cacheKey);
} catch (Exception e) {
logger.error("Cache clear failed: {}", cacheKey, e);
}
}
// generateCacheKey method omitted for brevity
}
</code>Example usage:
<code>@Transactional
@CacheClear(key = "#product.id")
public void update(Product product) {
// update logic
}
</code> <code>@GetMapping("/update/{id}")
public ResponseEntity<Object> update(@PathVariable Long id) {
productService.update(new Product(id));
return ResponseEntity.ok("success");
}
</code>Console logs confirm that the cache is cleared after the update operation.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.