A high performance caching library for Java providing Google Guava-inspired API with advanced eviction policies and comprehensive features
—
Caffeine provides comprehensive cache performance statistics through the CacheStats class and configurable StatsCounter implementations. Statistics enable monitoring cache effectiveness, identifying performance bottlenecks, and making informed tuning decisions.
The CacheStats class provides an immutable snapshot of cache performance metrics.
public final class CacheStats {
// Request metrics
public long requestCount();
public long hitCount();
public double hitRate();
public long missCount();
public double missRate();
// Load metrics
public long loadCount();
public long loadSuccessCount();
public long loadFailureCount();
public double loadFailureRate();
public double averageLoadPenalty();
public long totalLoadTime();
// Eviction metrics
public long evictionCount();
public long evictionWeight();
// Statistics operations
public CacheStats minus(CacheStats other);
public CacheStats plus(CacheStats other);
// Factory methods
public static CacheStats empty();
public static CacheStats of(long hitCount, long missCount, long loadSuccessCount,
long loadFailureCount, long totalLoadTime,
long evictionCount, long evictionWeight);
}Cache<String, String> statsCache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats()
.build();
// Perform cache operations
statsCache.put("key1", "value1");
statsCache.put("key2", "value2");
statsCache.getIfPresent("key1"); // hit
statsCache.getIfPresent("key3"); // miss
// Get statistics snapshot
CacheStats stats = statsCache.stats();
// Request statistics
System.out.println("Total requests: " + stats.requestCount()); // 2
System.out.println("Hits: " + stats.hitCount()); // 1
System.out.println("Hit rate: " + stats.hitRate()); // 0.5
System.out.println("Misses: " + stats.missCount()); // 1
System.out.println("Miss rate: " + stats.missRate()); // 0.5LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats()
.build(key -> {
if (key.equals("error")) {
throw new RuntimeException("Load error");
}
Thread.sleep(100); // Simulate load time
return "loaded_" + key;
});
// Perform operations that trigger loading
loadingCache.get("key1"); // successful load
loadingCache.get("key2"); // successful load
try {
loadingCache.get("error"); // failed load
} catch (Exception e) {
// Handle load failure
}
CacheStats loadStats = loadingCache.stats();
// Load statistics
System.out.println("Load count: " + loadStats.loadCount()); // 3
System.out.println("Load successes: " + loadStats.loadSuccessCount()); // 2
System.out.println("Load failures: " + loadStats.loadFailureCount()); // 1
System.out.println("Load failure rate: " + loadStats.loadFailureRate()); // 0.33
System.out.println("Average load time (ns): " + loadStats.averageLoadPenalty()); // ~100ms in ns
System.out.println("Total load time (ns): " + loadStats.totalLoadTime());Cache<String, String> evictionCache = Caffeine.newBuilder()
.maximumSize(3)
.recordStats()
.build();
// Fill cache beyond capacity to trigger evictions
evictionCache.put("key1", "value1");
evictionCache.put("key2", "value2");
evictionCache.put("key3", "value3");
evictionCache.put("key4", "value4"); // Triggers eviction
evictionCache.put("key5", "value5"); // Triggers eviction
CacheStats evictionStats = evictionCache.stats();
System.out.println("Evictions: " + evictionStats.evictionCount()); // 2
System.out.println("Eviction weight: " + evictionStats.evictionWeight()); // 2 (default weight 1 per entry)Cache<String, String> weightedCache = Caffeine.newBuilder()
.maximumWeight(10)
.weigher((key, value) -> key.length() + value.length())
.recordStats()
.build();
// Add entries with different weights
weightedCache.put("a", "1"); // weight: 2
weightedCache.put("bb", "22"); // weight: 4
weightedCache.put("ccc", "333"); // weight: 6
weightedCache.put("dddd", "4444"); // weight: 8, may trigger eviction
CacheStats weightStats = weightedCache.stats();
System.out.println("Evictions: " + weightStats.evictionCount());
System.out.println("Total evicted weight: " + weightStats.evictionWeight());Cache<String, String> cache1 = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build();
Cache<String, String> cache2 = Caffeine.newBuilder()
.maximumSize(100)
.recordStats()
.build();
// Perform operations on both caches
cache1.put("key1", "value1");
cache1.getIfPresent("key1"); // hit
cache2.put("key2", "value2");
cache2.getIfPresent("key3"); // miss
// Combine statistics
CacheStats stats1 = cache1.stats();
CacheStats stats2 = cache2.stats();
CacheStats combined = stats1.plus(stats2);
System.out.println("Combined hits: " + combined.hitCount()); // 1
System.out.println("Combined misses: " + combined.missCount()); // 1
System.out.println("Combined requests: " + combined.requestCount()); // 2Cache<String, String> monitoredCache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats()
.build();
// Take baseline snapshot
CacheStats baseline = monitoredCache.stats();
// Perform operations
monitoredCache.put("key1", "value1");
monitoredCache.put("key2", "value2");
monitoredCache.getIfPresent("key1"); // hit
monitoredCache.getIfPresent("key3"); // miss
// Calculate delta
CacheStats current = monitoredCache.stats();
CacheStats delta = current.minus(baseline);
System.out.println("New hits: " + delta.hitCount()); // 1
System.out.println("New misses: " + delta.missCount()); // 1
System.out.println("New requests: " + delta.requestCount()); // 2The StatsCounter interface allows custom statistics collection and reporting.
public interface StatsCounter {
// Recording methods
void recordHits(int count);
void recordMisses(int count);
void recordLoadSuccess(long loadTime);
void recordLoadFailure(long loadTime);
void recordEviction(int weight, RemovalCause cause);
// Snapshot method
CacheStats snapshot();
}// Default thread-safe implementation using LongAdder
Cache<String, String> concurrentStatsCache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats(ConcurrentStatsCounter::new)
.build();
// High-concurrency statistics collection
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
final int threadId = i;
executor.submit(() -> {
concurrentStatsCache.put("key_" + threadId, "value_" + threadId);
concurrentStatsCache.getIfPresent("key_" + threadId);
});
}
// Statistics remain accurate under high concurrency
CacheStats concurrentStats = concurrentStatsCache.stats();
System.out.println("Concurrent hit rate: " + concurrentStats.hitRate());public class CustomStatsCounter implements StatsCounter {
private final AtomicLong hits = new AtomicLong();
private final AtomicLong misses = new AtomicLong();
private final AtomicLong loadSuccesses = new AtomicLong();
private final AtomicLong loadFailures = new AtomicLong();
private final AtomicLong totalLoadTime = new AtomicLong();
private final AtomicLong evictions = new AtomicLong();
private final AtomicLong evictionWeight = new AtomicLong();
@Override
public void recordHits(int count) {
hits.addAndGet(count);
// Custom logging or external metrics reporting
logger.info("Cache hits recorded: {}", count);
}
@Override
public void recordMisses(int count) {
misses.addAndGet(count);
// Alert on high miss rates
long totalRequests = hits.get() + misses.get();
if (totalRequests > 100 && (double) misses.get() / totalRequests > 0.9) {
alertingService.sendAlert("High cache miss rate detected");
}
}
@Override
public void recordLoadSuccess(long loadTime) {
loadSuccesses.incrementAndGet();
totalLoadTime.addAndGet(loadTime);
// Track slow loads
if (loadTime > TimeUnit.SECONDS.toNanos(1)) {
logger.warn("Slow cache load detected: {}ms", TimeUnit.NANOSECONDS.toMillis(loadTime));
}
}
@Override
public void recordLoadFailure(long loadTime) {
loadFailures.incrementAndGet();
totalLoadTime.addAndGet(loadTime);
logger.error("Cache load failure after {}ms", TimeUnit.NANOSECONDS.toMillis(loadTime));
}
@Override
public void recordEviction(int weight, RemovalCause cause) {
evictions.incrementAndGet();
evictionWeight.addAndGet(weight);
metricsRegistry.counter("cache.evictions", "cause", cause.name()).increment();
}
@Override
public CacheStats snapshot() {
return CacheStats.of(
hits.get(),
misses.get(),
loadSuccesses.get(),
loadFailures.get(),
totalLoadTime.get(),
evictions.get(),
evictionWeight.get()
);
}
}
// Use custom stats counter
Cache<String, String> customStatsCache = Caffeine.newBuilder()
.maximumSize(1000)
.recordStats(CustomStatsCounter::new)
.build();Cache<String, String> monitoredCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(Duration.ofMinutes(30))
.recordStats()
.build();
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
CacheStats stats = monitoredCache.stats();
// Log key metrics
System.out.printf("Cache Stats - Requests: %d, Hit Rate: %.2f%%, " +
"Avg Load Time: %.2fms, Evictions: %d%n",
stats.requestCount(),
stats.hitRate() * 100,
stats.averageLoadPenalty() / 1_000_000.0,
stats.evictionCount());
// Send metrics to monitoring system
metricsCollector.gauge("cache.hit_rate", stats.hitRate());
metricsCollector.gauge("cache.miss_rate", stats.missRate());
metricsCollector.gauge("cache.avg_load_time_ms", stats.averageLoadPenalty() / 1_000_000.0);
metricsCollector.counter("cache.evictions").increment(stats.evictionCount());
}, 0, 30, TimeUnit.SECONDS);public class CachePerformanceAnalyzer {
public void analyzePerformance(Cache<?, ?> cache) {
CacheStats stats = cache.stats();
// Hit rate analysis
double hitRate = stats.hitRate();
if (hitRate < 0.8) {
System.out.println("WARNING: Low hit rate (" + (hitRate * 100) + "%)");
System.out.println("Consider: increasing cache size, adjusting expiration, or reviewing access patterns");
}
// Load time analysis
double avgLoadTimeMs = stats.averageLoadPenalty() / 1_000_000.0;
if (avgLoadTimeMs > 100) {
System.out.println("WARNING: High average load time (" + avgLoadTimeMs + "ms)");
System.out.println("Consider: optimizing data loading, adding async loading, or caching strategies");
}
// Load failure analysis
double loadFailureRate = stats.loadFailureRate();
if (loadFailureRate > 0.05) {
System.out.println("WARNING: High load failure rate (" + (loadFailureRate * 100) + "%)");
System.out.println("Consider: improving error handling, adding retry logic, or fallback values");
}
// Eviction analysis
long evictionCount = stats.evictionCount();
long requestCount = stats.requestCount();
if (requestCount > 0 && evictionCount > requestCount * 0.1) {
System.out.println("WARNING: High eviction rate (" + evictionCount + " evictions)");
System.out.println("Consider: increasing cache size, adjusting expiration, or using weight-based eviction");
}
// Overall health summary
System.out.printf("Cache Health Summary:%n" +
" Requests: %d%n" +
" Hit Rate: %.1f%%%n" +
" Avg Load Time: %.1fms%n" +
" Load Failures: %d (%.1f%%)%n" +
" Evictions: %d%n",
requestCount,
hitRate * 100,
avgLoadTimeMs,
stats.loadFailureCount(),
loadFailureRate * 100,
evictionCount);
}
}// Enable for production monitoring
Cache<String, String> productionCache = Caffeine.newBuilder()
.maximumSize(10000)
.recordStats() // Small overhead, valuable insights
.build();
// Disable for high-frequency, low-value caches
Cache<String, String> highFreqCache = Caffeine.newBuilder()
.maximumSize(1000)
// No recordStats() - avoid overhead for simple caches
.build();
// Use custom counter for specialized monitoring
Cache<String, String> specializedCache = Caffeine.newBuilder()
.maximumSize(5000)
.recordStats(() -> new CustomMetricsStatsCounter(metricsRegistry))
.build();ConcurrentStatsCounter provides excellent concurrent performance using LongAdderInstall with Tessl CLI
npx tessl i tessl/maven-com-github-ben-manes-caffeine--caffeine