Mastering Two-Level Caching in Spring Boot with CacheFrontend & Caffeine
This guide walks through implementing a two‑level cache in Spring Boot 2.7 using Lettuce’s CacheFrontend together with Caffeine, covering dependency setup, configuration, custom CacheAccessor, bean definitions, and sample endpoints that demonstrate automatic synchronization and eviction between local JVM cache and Redis.
Environment: SpringBoot 2.7.16
This article explains how to use CacheFrontend and Caffeine to implement a secondary cache.
1. Introduction
CacheFrontend is a front‑end component that can automatically synchronize a local JVM cache with Redis. If data exists locally it is returned directly; otherwise it is fetched from Redis.
CacheFrontend works by storing data retrieved from Redis in a local cache to reduce backend calls, and by updating or invalidating the local cache when the backend data changes.
2. Practical Example
2.1 Dependency Management
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2.2 Configuration File
spring:
redis:
host: localhost
password: xxxooo
database: 10
port: 63792.3 Local Cache Bean
@Bean
public Cache<String, String> localCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(1000)
.build();
}2.4 RedisClient Bean
@Bean
public RedisClient redisClient(RedisProperties props) {
RedisURI clientResources = RedisURI.Builder
.redis(props.getHost(), props.getPort())
.withPassword(props.getPassword().toCharArray())
.withDatabase(props.getDatabase())
.build();
RedisClient client = RedisClient.create(clientResources);
return client;
}2.5 CacheFrontend Bean
@Bean
public CacheFrontend<String, String> cacheFrontend(RedisClient redisClient, Cache<String, String> localCache) {
StatefulRedisConnection<String, String> connection = redisClient.connect();
// add listener for invalidation events
connection.addListener(new PushListener() {
@Override
public void onPushMessage(PushMessage message) {
String type = message.getType();
if ("invalidate".equals(type)) {
System.out.println("...");
}
List<Object> contents = message.getContent();
Object content = contents.get(0);
if (content instanceof ByteBuffer) {
ByteBuffer buf = (ByteBuffer) content;
System.out.printf("response content: %s%n", StringCodec.UTF8.decodeValue(buf));
}
}
});
// custom CacheAccessor
CacheAccessor<String, String> cacheAccessor = new CacheAccessor<String, String>() {
@Override
public String get(String key) {
@Nullable
String present = localCache.getIfPresent(key);
System.out.printf("get operator: %s%n", present);
return present;
}
@Override
public void put(String key, String value) {
localCache.put(key, value);
System.out.printf("put operator: key = %s, value = %s%n", key, value);
}
@Override
public void evict(String key) {
localCache.invalidate(key);
System.out.printf("evict operator: %s%n", key);
}
};
CacheFrontend<String, String> frontend = ClientSideCaching.enable(
cacheAccessor,
connection,
TrackingArgs.Builder.enabled());
return frontend;
}2.6 Test Controllers
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedisClient redisClient;
@Resource
private CacheFrontend<String, String> cf;
@GetMapping("/c1")
public Object cache1() {
stringRedisTemplate.opsForValue().set("name", "张三", Duration.ofSeconds(60));
return "c1";
}
@GetMapping("/c2")
public Object cache2() {
StatefulRedisConnection<String, String> conn = this.redisClient.connect(StringCodec.UTF8);
conn.sync().set("name", String.valueOf(System.currentTimeMillis()));
conn.close();
return "c2";
}
@GetMapping("/get")
public Object get() {
return this.cf.get("name");
}Initial Redis data:
Accessing /get returns the value from the local cache after synchronization.
Console output shows the cache miss on the first request, followed by data being stored locally.
After invoking /c1 or /c2, the local cache is automatically invalidated, as shown below.
Subsequent calls to /get retrieve the refreshed value from Redis, confirming that the local and Redis caches stay in sync.
In summary, combining a local Caffeine cache with Lettuce’s CacheFrontend provides performance gains, reduces database load, and supports high concurrency. Proper configuration and continuous monitoring are essential for maintaining system stability and efficiency.
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.
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.
