Spring Boot 3 + Redis Multi-Level Cache: From Single-Node to Production Hotspot Management
This article presents a production-ready multi-level caching architecture for Spring Boot 3 using Caffeine as L1, Redis as L2, and a database as L3, detailing design goals, hotspot mitigation, consistency handling, failure protection, implementation code, monitoring, and performance evaluation for high-concurrency e-commerce scenarios.
Why redesign the cache architecture
Many teams start with a simple @Cacheable annotation, add Redis to offload the database, and see good benchmark results. Under high‑traffic events such as flash sales, live streams, or trending products the system begins to jitter. The core issue is not whether a cache exists, but whether the cache system is architected.
Four reliability concerns in production
Can hotspot traffic be absorbed in layers?
Will cache expiration cause a database stampede?
Can data updates achieve fast consistency?
Will failures trigger graceful degradation instead of cascading outages?
Design goals (e‑commerce product detail example)
Low‑latency responses for high‑concurrency reads
Automatic hotspot key detection and local handling
Three‑tier access chain: L1 local cache → L2 Redis → DB
Protection mechanisms: null‑value caching, mutex loading, logical expiration, random TTL
Redis Pub/Sub for cache‑eviction broadcasting
Micrometer metrics for hit‑rate and hotspot observation
Modular design to easily add MQ, Canal, CDC, or config‑center integrations
Core architecture: L1 + L2 + L3 with two governance paths
┌──────────────────────────┐
│ Client/App │
└────────────┬─────────────┘
│
HTTP / Gateway / LB
│
┌───────────────────────┴───────────────────────┐
│ Spring Boot 3 Application Cluster │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ ProductQueryService │ │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ ┌──────────▼──────────┐
│ │ MultiLevelCache │ │ HotKeyStats │
│ └──────────┬──────────┘ └─────────────────────┘
│ │
│ ┌──────────┴──────────┐
│ │ Redis L2 │
│ └───────┬────────────┘
│ │
│ DB (MySQL / PG)Why L1 + L2 + L3?
L1 (Caffeine) absorbs hotspot traffic, eliminates network overhead, keeps per‑key high concurrency within the JVM, and offers microsecond latency.
L2 (Redis) is a shared distributed cache, handles warm data, and reduces DB load.
L3 (Database) is the ultimate source of truth and should be accessed as little as possible.
Core principles
Read path – gradual miss and fill
1. Check L1 local cache
2. If miss, check L2 Redis
3. If Redis hit, back‑fill L1 and return
4. If Redis miss, query DB
5. If DB hit, back‑fill Redis and L1
6. If DB miss, cache a null value to prevent penetrationWrite path – cache‑aside with eviction broadcast
1. Update the database
2. Delete the corresponding Redis key
3. Publish an eviction event via Redis Pub/Sub
4. All nodes receive the event and clear their local L1 entryHotspot mitigation – double‑check, local mutex, distributed lock
When many threads or instances request the same hot key, the service performs:
Local mutex (a ConcurrentHashMap ‑based lock) to merge concurrent reads inside a JVM.
Redisson distributed lock to avoid multiple instances hitting the DB simultaneously.
Double‑check before and after acquiring the lock to ensure another thread hasn't already populated the cache.
Failure protection
Cache penetration : cache null values with a short TTL (e.g., 30 s).
Cache breakdown : use mutex/SingleFlight and keep hot keys alive longer in L1.
Cache avalanche : add random seconds to TTL, stagger expirations, and employ multi‑level cache.
Inconsistency : delete rather than update cache entries, broadcast eviction, and avoid local caching for strongly consistent data such as inventory.
Key implementation classes (simplified)
package com.example.cache.cache;
public class CacheValue<T> {
private boolean nullValue;
private T data;
private long version;
private long cachedAt;
public static <T> CacheValue<T> of(T data, long version) { ... }
public static <T> CacheValue<T> nullValue() { ... }
} package com.example.cache.cache;
@Service
public class MultiLevelCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
private final Cache<String, CacheValue<?>> localCache;
// metrics counters omitted for brevity
public <T> T get(String cacheName, String key, Class<T> type, Callable<T> loader) { ... }
public void evict(String cacheName, String key) { ... }
public <T> void put(String cacheName, String key, T value) { ... }
// helper methods: retryRead, putRedis, nextTtlSeconds, buildKey
} package com.example.cache.service;
@Service
public class ProductQueryService {
private static final String CACHE_NAME = "product:detail";
private final MultiLevelCacheService cacheService;
private final ProductRepository productRepository;
public ProductDetail getProductDetail(Long productId) {
return cacheService.get(CACHE_NAME, String.valueOf(productId), ProductDetail.class,
() -> productRepository.findById(productId)
.filter(p -> "ONLINE".equals(p.getStatus()))
.map(this::toDetail)
.orElse(null));
}
private ProductDetail toDetail(Product p) { ... }
}Observability
Micrometer counters track L1 hits, L2 hits, DB loads, and null‑value caching. A custom gauge reports the current number of hot keys. Grafana dashboards should display hit‑rate, hotspot rankings, Redis health, and DB back‑source metrics. Alerts fire on low L1 hit‑rate, DB QPS spikes, Redis CPU > 80 %, and eviction‑broadcast failures.
Advanced enhancements
Logical expiration + async refresh : keep hot keys in Redis indefinitely, embed an expireAt field, serve stale data while a background thread refreshes.
Hot‑key pre‑warm : load known hot product details before a promotion starts.
MQ‑based eviction : replace Redis Pub/Sub with Kafka/RocketMQ for durable broadcast.
Canal/CDC driven invalidation : listen to binlog changes and invalidate caches without touching business code.
Key‑level routing & isolation : assign ultra‑hot keys to a dedicated Redis shard or a separate service cluster.
Performance test
Using wrk -t12 -c400 -d30s http://127.0.0.1:8080/api/products/1001, the three compared setups yielded:
No Cache – Avg Latency 40‑120 ms, P99 > 200 ms, Redis QPS 0, DB load very high.
Single‑Layer Redis – Avg Latency 3‑10 ms, P99 20‑50 ms, Redis QPS high, DB load low, Redis becomes hotspot.
L1 + Redis – Avg Latency <2 ms, P99 5‑15 ms, Redis QPS significantly lower, DB load very low, hot traffic stays in local cache.
Best‑practice checklist
Separate TTLs for normal data (120‑600 s) and null values (15‑60 s).
Use a consistent key naming scheme, e.g., product:detail:1001.
Avoid caching strongly consistent data (inventory, balance) with L1.
Cap local cache size (e.g., 50 000 entries) and monitor GC impact.
Provide runtime switches for L1, hotspot detection, logical expiration, and eviction broadcast.
Roll out changes gradually: enable Redis first, then L1, then broadcast, then advanced features.
Conclusion
A production‑grade multi‑level cache for Spring Boot 3 must combine fast L1 access, shared L2 storage, robust consistency mechanisms, hotspot detection, and full observability. Applied to read‑heavy e‑commerce scenarios such as product detail pages, it can cut Redis pressure by over 60 %, keep average latency under 2 ms, and sustain tens of thousands of QPS without sacrificing eventual consistency.
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.
Cloud Architecture
Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.
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.
