Caffeine Caching Library

repository·master·Indexed 12 days ago

https://github.com/ben-manes/caffeine

A high-performance, near-optimal in-memory caching library for Java designed as a modern successor to Google Guava's cache. It features advanced eviction policies like TinyLFU and supports integration with Project Reactor for coalescing cache refreshes, Hibernate as a JCache provider, and GraalVM for native image compilation.

Tokens
6.2K
Snippets
20
Records
24
Agent score
97%

What's inside Caffeine

  1. Overview of Caffeine Cache Features

    master

    Caffeine is a high-performance in-memory caching library that provides several optional features for cache management:

    • Automatic Loading: Entries can be loaded automatically, optionally asynchronously.
    • Size-based Eviction: Evicts entries based on frequency and recency when a maximum size is exceeded.
    • Time-based Expiration: Entries can expire based on time measured since their last access or last write.
    • Asynchronous Refresh: Stale entries can be refreshed asynchronously upon the first request.
    • Reference-based Eviction: Keys and values can be automatically wrapped in weak or soft references.
    • Removal Listeners: Provides notifications when entries are evicted or otherwise removed.
    • Write Propagation: Allows writes to be propagated to an external resource.
    • Statistics: Accumulates cache access statistics for monitoring.
  2. Use an Indexable Cache for multiple key lookups

    master

    An IndexedCache allows you to associate a single cache value with multiple unique keys (similar to a relational database with primary and secondary indexes). This enables fast retrieval of the same object using different identifiers (e.g., looking up a user by ID, email, or username).

    When a value is updated, deleted, or evicted, the IndexedCache ensures that all associated key mappings are kept consistent.

    To implement this, you use an IndexedCache.Builder to define:

    1. A primary key function to identify the canonical key for the value.
    2. Secondary key functions to define the alternative lookup paths.
    3. A data loader function to fetch the value on a cache miss.
    4. Standard Caffeine bounding constraints like expireAfterWrite or maximumSize.
    var cache = new IndexedCache.Builder<UserKey, User>()
        .primaryKey(user -> new UserById(user.id()))
        .addSecondaryKey(user -> new UserByEmail(user.email()))
        .addSecondaryKey(user -> new UserByLogin(user.username()))
        .expireAfterWrite(Duration.ofMinutes(5))
        .maximumSize(10_000)
        .build(this::findUser);
    
    // All these calls return the same instance if the user is already cached
    var userByEmail = cache.get(new UserByEmail("john.doe@example.com"));
    var userByLogin = cache.get(new UserByLogin("john.doe"));
  3. Coalesce cache refreshes using Reactor data streams

    master

    You can use Project Reactor to combine independent asynchronous loads into batches. This reduces the load on source systems by consolidating multiple requests into a single bulk operation, at the cost of a small buffering delay.

    This pattern is particularly useful for refreshAfterWrite scenarios. By batching optimistic reloads (which occur in the background), you can minimize source system impact without affecting the responsiveness of explicit, blocking requests.

  4. Copy metadata to native-image configuration

    master

    After running with the agent, copy the captured metadata into the project's resources so the Graal compiler can use it as source configuration. Use the metadataCopy task, specifying the target task and the destination directory.

    ./gradlew metadataCopy --task run --dir src/main/resources/META-INF/native-image
  5. Install Caffeine via Gradle

    master

    To use Caffeine in your project, add the dependency to your build.gradle file. Use version 3.x for Java 11 or above, and version 2.x for older Java versions.

    Optional extensions like Guava adapters or JCache support can also be added as dependencies.

    // Core library
    implementation("com.github.ben-manes.caffeine:caffeine:3.2.4")
    
    // Optional extensions
    implementation("com.github.ben-manes.caffeine:guava:3.2.4")
    implementation("com.github.ben-manes.caffeine:jcache:3.2.4")
  6. Implement Async Coalescing with AsyncCacheLoader

    master

    If all loads (not just refreshes) should be collected into batches, implement AsyncCacheLoader. This is most suitable for an AsyncLoadingCache because it does not block map operations while an entry is being loaded. All requests are submitted to a Reactor Sink via asyncLoad and processed in batches by a subscriber.

    public final class CoalescingBulkLoader<K, V> implements AsyncCacheLoader<K, V> {
      private final Function<Set<K>, Map<K, V>> mappingFunction;
      private final Sinks.Many<Request<K, V>> sink;
    
      public CoalescingBulkLoader(int maxSize, Duration maxTime, int parallelism, 
          Function<Set<K>, Map<K, V>> mappingFunction) {
        this.sink = Sinks.many().unicast().onBackpressureBuffer();
        this.mappingFunction = requireNonNull(mappingFunction);
        sink.asFlux()
            .bufferTimeout(maxSize, maxTime)
            .map(requests -> requests.stream().collect(
                toMap(Entry::getKey, Entry::getValue)))
            .parallel(parallelism)
            .runOn(Schedulers.boundedElastic())
            .subscribe(this::handle);
      }
    
      @Override public synchronized CompletableFuture<V> asyncLoad(K key, Executor e) {
        var entry = Map.entry(key, new CompletableFuture<V>());
        sink.tryEmitNext(entry).orThrow();
        return entry.getValue();
      }
    
      private void handle(Map<K, CompletableFuture<V>> requests) {
        try {
          var results = mappingFunction.apply(requests.keySet());
          requests.forEach((key, result) -> result.complete(results.get(key)));
        } catch (Throwable t) {
          requests.forEach((key, result) -> result.completeExceptionally(t));
        }
      }
    }
  7. Implement Refresh Coalescing with CacheLoader

    master

    To aggregate only background refreshes while keeping explicit loads immediate, implement CacheLoader. In this pattern, load and loadAll invoke the mapping function directly (synchronously), while asyncReload submits the request to a Reactor Sink for batching.

    Note: The asyncReload method must be synchronized because the Reactor Sink does not support concurrent submissions.

    public final class CoalescingBulkLoader<K, V> implements CacheLoader<K, V> {
      private final Function<Set<K>, Map<K, V>> mappingFunction;
      private final Sinks.Many<Request<K, V>> sink;
    
      /**
       * @param maxSize the maximum entries to collect before performing a bulk request
       * @param maxTime the maximum duration to wait before performing a bulk request
       * @param parallelism the number of parallel bulk loads that can be performed
       * @param mappingFunction the function to compute the values
       */
      public CoalescingBulkLoader(int maxSize, Duration maxTime, int parallelism, 
          Function<Set<K>, Map<K, V>> mappingFunction) {
        this.sink = Sinks.many().unicast().onBackpressureBuffer();
        this.mappingFunction = requireNonNull(mappingFunction);
        sink.asFlux()
            .bufferTimeout(maxSize, maxTime)
            .map(requests -> requests.stream().collect(
                toMap(Entry::getKey, Entry::getValue)))
            .parallel(parallelism)
            .runOn(Schedulers.boundedElastic())
            .subscribe(this::handle);
      }
    
      @Override public V load(K key) {
        return loadAll(Set.of(key)).get(key);
      }
    
      @Override public Map<K, V> loadAll(Set<? extends K> keys) {
        return mappingFunction.apply(keys);
      }
    
      @Override public synchronized CompletableFuture<V> asyncReload(K key, V oldValue, Executor e) {
        var entry = Map.entry(key, new CompletableFuture<V>());
        sink.tryEmitNext(entry).orThrow();
        return entry.getValue();
      }
    
      private void handle(Map<K, CompletableFuture<V>> requests) {
        try {
          var results = mappingFunction.apply(requests.keySet());
          requests.forEach((key, result) -> result.complete(results.get(key)));
        } catch (Throwable t) {
          requests.forEach((key, result) -> result.completeExceptionally(t));
        }
      }
    }
  8. Configure Caffeine as a JCache provider for Hibernate

    master

    To use Caffeine as the second-level cache for Hibernate, you must enable the second-level cache, specify jcache as the region factory class, and set the Caffeine JCache provider in your hibernate.properties file.

    You can also override the default Caffeine configuration file path by setting the hibernate.javax.cache.uri property.

    hibernate.cache.use_second_level_cache=true
    hibernate.cache.region.factory_class=jcache
    hibernate.javax.cache.provider=com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider
  9. Implement retry strategies for Caffeine using Failsafe

    master

    You can use Failsafe's RetryPolicy to make Caffeine cache operations resilient to intermittent failures. There are two primary ways to apply retries:

    1. Retry outside the cache loader (Synchronous): Wrap the cache.get call itself. This retries the entire cache lookup/load process.
    2. Retry inside the cache load (Asynchronous): For AsyncCache, wrap the loading function inside failsafe.getAsync. This retries only the specific loading operation if it fails, without re-triggering the cache lookup logic.

    Note: This requires the Failsafe library.

    var retryPolicy = RetryPolicy.builder()
        .withDelay(Duration.ofSeconds(1))
        .withMaxAttempts(3)
        .build();
    var failsafe = Failsafe.with(retryPolicy);
    
    // Retry outside of the cache loader for synchronous calls
    Cache<K, V> cache = Caffeine.newBuilder().build();
    failsafe.get(() -> cache.get(key, key -> /* intermittent failures */ ));
    
    // Optionally, retry inside the cache load for asynchronous calls
    AsyncCache<K, V> asyncCache = Caffeine.newBuilder().buildAsync();
    asyncCache.get(key, (key, executor) -> failsafe.getAsync(() -> /* intermittent failure */ ));