Caffeine: the king of local cache performance

Keywords: Java github Redis Ehcache

preface

With the rapid development of the Internet, more and more websites and apps appear on the market. We judge whether a software is easy to use, and user experience is an important measure. For example, we often use wechat, it takes more than ten seconds to open a page, and it takes a few minutes for the other party to receive a voice. I believe that such software is certainly not willing to use. In order to achieve good user experience and fast response, caching is an essential artifact. There are two types of cache: in-process cache and distributed cache: distributed cache, such as redis and memcached, and local (in-process) cache, such as ehcache, GuavaCache and Caffeine. Speaking of Guava Cache, many people are familiar with it. It is a very convenient and easy-to-use localized cache implementation in Google Guava toolkit. Based on LRU algorithm, it supports a variety of cache expiration strategies. Due to the extensive use of Guava, Guava Cache has also been widely used. However, is the performance of Guava Cache necessarily the best? Maybe its performance was very good. The so-called Yangtze River back wave pushes the front wave, and the front wave is photographed on the beach. Let's introduce a caching framework with higher performance than Guava Cache: Caffeine.

Tips: Spring5 (SpringBoot2) began to replace guava with Caffeine. See official information SPR-13797 for details
https://jira.spring.io/browse/SPR-13797

Official performance comparison

The following tests are based on jmh tests, Official website address
Why is the test based on jmh, You can refer to Zhihu's answer

When running microbenchmark on HotSpot VM, remember not to run cycle timing in main(). This is a typical mistake. Repeat the important things three times: JMH, JMH and JMH. Unless you know the implementation details of HotSpot very well, the result of running cycle timing in main doesn't mean anything to ordinary programmers, because it can't be explained.

  • 8 threads read, 100% read operation
  • Six threads read, two threads write, that is 75% of read operations, 25% of write operations.
  • 8 threads write, 100% write operation

    Comparative conclusion

    It can be seen from the data that the performance of Caffeine is better than that of Guava. Then the operation function of Caffeine's API is basically consistent with that of Guava. In order to be compatible with the users of Guava before, Caffeine has made a Guava Adapter for you to use, which is also very considerate.

    How to use

    • At pom.xml Add caffeine dependency to
      <!-- https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
      <dependency>
      <groupId>com.github.ben-manes.caffeine</groupId>
      <artifactId>caffeine</artifactId>
      <version>2.8.2</version>
      </dependency>

      create object

 Cache<String, Object> cache = Caffeine.newBuilder()
                .initialCapacity(100)//initial size
                .maximumSize(200)//Maximum quantity
                .expireAfterWrite(3, TimeUnit.SECONDS)//Expiration time
                .build();

Introduction to creating parameters

  • initialCapacity: initial cache space size
  • maximumSize: maximum number of caches
  • maximumWeight: the maximum weight of the cache
  • expireAfterAccess: expires after the specified time after the last read or write operation
  • expireAfterWrite: expires after the specified time after the last write operation
  • refreshAfterWrite: refresh the cache at a specified time interval after the cache is created or last updated
  • weakKeys: open weak reference of key
  • weakValues: open a weak reference to value
  • softValues: open the soft reference of value
  • recordStats: developing statistical functions

    be careful:
    When both expireAfterWrite and expireAfterAccess exist, the expireAfterWrite shall prevail. maximumSize and maximumWeight cannot be used at the same time.

Add data

Caffeine provides us with manual, synchronous and asynchronous population strategies.
Now let's demonstrate the manual filling strategy. If you are interested, you can go to Official website

  Cache<String, String> cache = Caffeine.newBuilder()
                .build();
        cache.put("java finance", "java finance");
        System.out.println(cache.getIfPresent("java finance"));

Auto add (custom add function)

  public static void main(String[] args) {
        Cache<String, String> cache = Caffeine.newBuilder()
                .build();
        // 1. If it can be found in the cache, it will return directly
        // 2. If it cannot be found, get the data from our custom getValue method and add it to the cache
        String val = cache.get("java finance", k -> getValue(k));
        System.out.println(val);
    }
    /**
     * If it is not found in the cache, it will enter this method. Generally, the content is obtained from the database
     * @param k
     * @return
     */
    private static String getValue(String k) {
        return k + ":value";
    }

Expiration Policies

Caffeine provided us with Three expiration strategies
, which are size based, time-based and reference based

Size based
      LoadingCache<String, String> cache = Caffeine.newBuilder()
                // Maximum capacity is 1
                .maximumSize(1)
                .build(k->getValue(k));
        cache.put("java Finance 1","java Finance 1");
        cache.put("java Finance 2","java Finance 2");
        cache.put("java Finance 3","java Finance 3");
        cache.cleanUp();
        System.out.println(cache.getIfPresent("java Finance 1"));
        System.out.println(cache.getIfPresent("java Finance 2"));
        System.out.println(cache.getIfPresent("java Finance 3"));

The operation result is as follows: only one is left after two are eliminated.

null
null
java Finance 3
Time based

Caffeine provides three timing expulsion strategies:

expireAfterWrite(long, TimeUnit)
  • Timing starts after the last write to the cache and expires after the specified time.
    LoadingCache<String, String> cache =  Caffeine.newBuilder()
                // Maximum capacity is 1
                .maximumSize(1)
                .expireAfterWrite(3, TimeUnit.SECONDS)
                .build(k->getValue(k));
        cache.put("java finance","java finance");
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));

    The value is empty in the third second of the running result.

java finance
java finance
null
expireAfterAccess
  • Starts timing after the last read or write and expires after the specified time. If there is always a request to access the key, the cache will never expire.
    LoadingCache<String, String> cache =  Caffeine.newBuilder()
                // Maximum capacity is 1
                .maximumSize(1)
                .expireAfterAccess(3, TimeUnit.SECONDS)
                .build(k->getValue(k));
        cache.put("java finance","java finance");
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));
        Thread.sleep(1*1000);
        System.out.println(cache.getIfPresent("java finance"));
        Thread.sleep(3001);
        System.out.println(cache.getIfPresent("java finance"));

    Run result: when there is no read or write, it will expire in 3 seconds, and then null will be output.

    java finance
    java finance
    java finance
    null
    expireAfter(Expiry)
  • You need to implement the Expiry interface in the expireAfter. This interface supports expireAfterCreate,expireAfterUpdate, and how long the expireAfterRead expires. Note that this is mutually exclusive with expireAfterAccess and expireAfterAccess. Different from expireafteraccess and expireafteraccess, you need to tell the cache framework that it should expire at a specific time to get the specific expiration time.
 LoadingCache<String, String> cache = Caffeine.newBuilder()
                // Maximum capacity is 1
                .maximumSize(1)
                .removalListener((key, value, cause) ->
                        System.out.println("key:" + key + ",value:" + value + ",Delete reason:" + cause))
                .expireAfter(new Expiry<String, String>() {
                    @Override
                    public long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {
                        return currentTime;
                    }
                    @Override
                    public long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }

                    @Override
                    public long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {
                        return currentTime;
                    }
                })
                .build(k -> getValue(k));

delete

Posted by MrSheen on Sat, 13 Jun 2020 21:29:50 -0700