Caffeineのチューニングによるインメモリキャッシュ
高スループットのローカルキャッシュ向けに、Caffeineの追い出し、有効期限、サイズのポリシーを設定します。
「Caffeineのチューニングによるインメモリキャッシュ」はCoddyKit上の無料Spring Boot 4 Complete Guideレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはSpring Boot 4 Complete Guide学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Spring Boot 4 Complete Guideコースには全4レッスンが含まれています。
このレッスンの一部はまだ翻訳されておらず、英語で表示されています。
Why Caffeine for Local Caches
Caffeine is a high-performance, near-optimal Java caching library and the default in-memory cache for Spring Boot when it is on the classpath.
- It uses the Window TinyLFU eviction policy, which beats plain LRU on real-world hit rates.
- It supports size-based, time-based, and reference-based eviction.
- It is fully concurrent and lock-free on the read path, ideal for high-throughput services.
In this lesson you will tune Caffeine's eviction, expiry, and size policies so a local cache stays fast without exhausting heap.
Adding Caffeine to a Spring Boot 4 App
Spring Boot auto-configures Caffeine when both spring-boot-starter-cache and the caffeine dependency are present.
Then enable caching with @EnableCaching on a configuration class. Methods annotated with @Cacheable will use the Caffeine-backed CacheManager.
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
// CaffeineCacheManager is auto-configured
// when com.github.ben-manes.caffeine:caffeine is on the classpath
}Maximum Size Eviction
The most common policy for a local cache is bounded size. Use maximumSize to cap the number of entries; Caffeine evicts the least valuable entries (Window TinyLFU) once the bound is exceeded.
- Pick a size that fits comfortably in heap given your value object footprint.
- Eviction is not immediate at the boundary; it happens promptly but asynchronously.
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("products");
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats());
return manager;
}Weight-Based Eviction
When entries vary wildly in size, bound the cache by weight instead of count. Provide a maximumWeight and a Weigher that returns each entry's cost.
- You cannot combine
maximumSizeandmaximumWeighton the same cache. - Weights are computed once at insertion and are not updated afterward.
import com.github.benmanes.caffeine.cache.Caffeine;
Caffeine.newBuilder()
.maximumWeight(50_000_000) // ~50MB budget
.weigher((String key, byte[] value) -> value.length)
.build();expireAfterWrite vs expireAfterAccess
Time-based expiry comes in two flavors:
- expireAfterWrite: entry expires a fixed duration after it was created or last replaced. Best for data with a known freshness window (e.g. a price valid for 5 minutes).
- expireAfterAccess: entry expires a duration after its last read or write. Best for keeping hot data alive and dropping idle entries.
You may combine both; the entry expires when either condition fires first.
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.expireAfterAccess(Duration.ofMinutes(2))
.build();Configuring via application.properties
For a single shared spec, Spring Boot lets you skip Java config and use the spring.cache.caffeine.spec property. It accepts the comma-separated Caffeine spec string.
This is convenient but applies the same spec to every cache name; per-cache tuning still requires a programmatic CaffeineCacheManager or a custom CacheLoader.
spring.cache.type=caffeine
spring.cache.cache-names=products,prices
spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=5m,recordStatsrefreshAfterWrite for Stale-While-Revalidate
refreshAfterWrite differs from expiry: instead of removing the entry, it asynchronously reloads it after the duration while still serving the old value. This avoids a latency spike on the first request after staleness.
- It requires a
LoadingCache(a cache built with aCacheLoader). - Only one thread triggers the refresh; others keep reading the existing value.
- Combine with a longer
expireAfterWriteas a hard ceiling.
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import java.time.Duration;
LoadingCache<String, String> cache = Caffeine.newBuilder()
.refreshAfterWrite(Duration.ofMinutes(1))
.expireAfterWrite(Duration.ofMinutes(10))
.build(key -> loadFromDatabase(key));A Standalone Caffeine Demo
Here is a complete, framework-free program demonstrating maximumSize eviction. Insert more entries than the bound and observe that the cache never exceeds its size after cleanup.
This is the kind of micro-benchmark you can run to validate a tuning choice before wiring it into Spring.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
public class CaffeineDemo {
public static void main(String[] args) {
Cache<Integer, String> cache = Caffeine.newBuilder()
.maximumSize(3)
.build();
for (int i = 0; i < 10; i++) {
cache.put(i, "value-" + i);
}
cache.cleanUp(); // force pending eviction work
System.out.println("Estimated size: " + cache.estimatedSize());
System.out.println("Get key 9: " + cache.getIfPresent(9));
}
}Per-Cache Tuning with Custom Specs
Real services need different policies per cache: a tiny hot lookup table vs a large warm dataset. Subclass or configure CaffeineCacheManager so each name gets its own builder.
One clean approach is registering individual native caches by name on the manager.
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import java.time.Duration;
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.registerCustomCache("prices", Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofSeconds(30))
.build());
manager.registerCustomCache("catalog", Caffeine.newBuilder()
.maximumSize(100_000)
.expireAfterAccess(Duration.ofHours(1))
.build());
return manager;
}Measuring Hit Rate with recordStats
You cannot tune what you do not measure. Enable recordStats() to expose CacheStats: hit count, miss count, eviction count, and average load penalty.
- A low hit rate suggests the cache is too small or the keys too cardinal.
- High eviction with high miss rate means
maximumSizeis starving the working set. - Spring Boot's Micrometer integration publishes these as metrics when stats are on.
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
public class StatsDemo {
public static void main(String[] args) {
Cache<String, Integer> cache = Caffeine.newBuilder()
.maximumSize(2)
.recordStats()
.build();
cache.put("a", 1);
cache.getIfPresent("a"); // hit
cache.getIfPresent("b"); // miss
CacheStats stats = cache.stats();
System.out.printf("hits=%d misses=%d rate=%.2f%n",
stats.hitCount(), stats.missCount(), stats.hitRate());
}
}Avoiding Common Tuning Pitfalls
A few traps to watch for under high throughput:
- Unbounded caches: never build a cache without a size or expiry bound, or you risk an
OutOfMemoryError. - initialCapacity: set it near the expected steady-state size to avoid resize churn on the read-heavy path.
- Soft/weak references:
softValues()ties eviction to GC pressure, which is unpredictable; prefer explicit size/time bounds for latency-sensitive caches. - refreshAfterWrite without a loader: it silently has no effect on a manual cache.
Quick Check: Choosing an Expiry Policy
You cache product prices that the upstream system guarantees are valid for exactly 5 minutes after publication. Reads are frequent but you must never serve a price older than 5 minutes. Which Caffeine policy fits best?
Recap
You tuned Caffeine for high-throughput local caching in Spring Boot 4:
- Size:
maximumSizefor uniform entries,maximumWeight+Weigherfor variable cost. - Time:
expireAfterWritefor freshness contracts,expireAfterAccessto keep hot data, combine for both ceilings. - Refresh:
refreshAfterWriteon aLoadingCachefor stale-while-revalidate without latency spikes. - Config:
spring.cache.caffeine.specfor one shared spec, orregisterCustomCachefor per-cache tuning. - Measure: always enable
recordStats()and watch hit rate and eviction count to guide further tuning.
よくある質問
「Caffeineのチューニングによるインメモリキャッシュ」レッスンは無料ですか?
はい。「Caffeineのチューニングによるインメモリキャッシュ」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Spring Boot 4 Complete Guideコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Spring Boot 4 Complete Guideコースには全4レッスンが含まれています。
「Caffeineのチューニングによるインメモリキャッシュ」で何を学びますか?
高スループットのローカルキャッシュ向けに、Caffeineの追い出し、有効期限、サイズのポリシーを設定します。 ブラウザで直接実行するハンズオンコードでSpring Boot 4 Complete Guideを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Spring Boot 4 Complete Guideを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのSpring Boot 4 Complete Guideは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「Caffeineのチューニングによるインメモリキャッシュ」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このSpring Boot 4 Complete Guideレッスンでコードを書いて実行できますか?
はい。すべてのSpring Boot 4 Complete Guideレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- Spring Cache Abstractionの基礎
- Caffeineのチューニングによるインメモリキャッシュ
- RedisとTTLによる分散キャッシュ
- キャッシュスタンピード、無効化、整合性