The most authoritative caffeine course in the whole network

Time:2021-2-23

If you want to see the official website tutorial directly, please move to:https://github.com/ben-manes/…

And if you still want to combine the actual application scenarios, as well as a variety of pit, please see this article.

Recently, an intern, Xiao Zhang, came to see the cache framework caffeine that I used in the company’s projects. He came to me every day to learn from me and said that he wanted to understand caffeine thoroughly. For this reason, he had no choice but to answer carefully one by one.

Later, the director told me that there were still new people in the future. Let me summarize the relevant problems and details into a tutorial, which can be regarded as the first one in the whole network. It combines the application and thinking of business scenarios in our company’s games, as well as the holes we have stepped on.

Intern Xiao Zhang: porridge. In the past, the cache used in our game was actually the concurrent linked HashMap provided by Google. Why do you want to use cafeine instead?

There are several reasons for the above problems

  • There is a vulnerability in using the concurrent linked HashMap provided by Google, that is, the cache expiration will only occur when the cache reaches the upper limit, otherwise it will only be kept in the cache all the time. At first glance, this mechanism is OK. It’s OK, but it’s unreasonable. For example, when a player goes online, he loads a bunch of data into the cache and then doesn’t go online any more. Then the cache will always exist until the cache reaches the upper limit.
  • Concurrent linked HashMap doesn’t provide a mechanism based on time elimination time, while caffeine has, and has a variety of elimination mechanisms, and supports elimination notification.
  • At present, spring is also recommended. Cafe provides an almost optimal hit rate due to the use of window tinylfu recycling strategy.

Intern Zhang: Oh, I understand. Can you introduce caffeine to me?

Yes, coffeine is a high-performance cache Library Based on Java 8, which can provide near optimal hit rate. The bottom layer of caffeine uses concurrent HashMap, which supports to make the cached data expired and then destroyed according to certain rules or user-defined rules.

Another piece of hot news is that many people have heard of Google’s guavacache, but not caffeine. In fact, compared with caffeine, guavacache is just a brother. Spring framework 5.0 (springboot 2.0) has given up Google’s guavacache and opted for caffeine instead.

The most authoritative caffeine course in the whole network

Why dare you praise caffeine like that? We can speak with official data.

The most authoritative caffeine course in the whole network

Caffeine provides a variety of flexible construction methods to create local caches with a variety of features.

  1. Automatically load the data into the local cache, and can be configured asynchronously;
  2. Based on quantity elimination strategy;
  3. Based on the failure time elimination strategy, this time is [access or write] from the last operation;
  4. Asynchronous refresh;
  5. The key will be packaged as a weak reference;
  6. Value will be packaged as a weak or soft reference, so that it can be deleted by GC without memory leakage;
  7. Data elimination reminder;
  8. Write broadcast mechanism;
  9. Cache access can be counted;

Intern Xiao Zhang: I wipe, so powerful, why can it be so powerful? Porridge, aren’t you the person who claims to be most familiar with caffeine? Can you give me a general idea of the internal structure?

My day, I didn’t. I just said that I am most familiar with our project team. Don’t stigmatize meThe most authoritative caffeine course in the whole network

Next, I’d like to introduce the internal structure of caffeine

The most authoritative caffeine course in the whole network

  • There is a concurrent HashMap inside the cache, which is also the place to store all our cache data. As we all know, concurrent HashMap is a concurrent secure container, which is very important. It can be said that caffeine is actually a strengthened concurrent HashMap.
  • Scheduler, a mechanism for clearing data periodically, can not be set. If it is not set, it will not empty expired data actively.
  • Executor, which specifies the thread pool to use when running asynchronous tasks. Can not set, if not set, will use the default thread pool, that is ForkJoinPool.commonPool ()

Intern Zhang: it sounds like an enhanced version of concurrent HashMap. Do you need to import any packages?

Caffeine dependency is actually very simple. You can directly introduce Maven dependency.

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Intern Xiao Zhang: Yes, the import is successful. You always told me that caffeine’s data filling mechanism is very beautiful, isn’t it put data? What’s beautiful? Tell me about it?

Caffeine provides three mechanisms for put data

  • Manual loading
  • Synchronous loading
  • Asynchronous loading

Let me give you two examples, such as manual loading

/**
 * @author xifanxiaxue
 * @date 2020/11/17 0:16
 *@ desc fill in data manually
 */
public class CaffeineManualTest {

    @Test
    public void test() {
        //Initialize the cache, set the write expiration time of 1 minute, and set the maximum number of 100 caches
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                .build();
        int key1 = 1;
        //Use the getifpresent method to get the value from the cache. If the specified value does not exist in the cache, the method returns null:
        System.out.println(cache.getIfPresent(key1));

        //You can also use the get method to get the value, which passes in a function with key as a parameter. If the key does not exist in the cache
        //This function will be used to provide a default value, which is inserted into the cache after calculation:
        System.out.println(cache.get(key1, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer integer) {
                return 2;
            }
        }));

        //Verify whether the value corresponding to key1 is inserted into the cache
        System.out.println(cache.getIfPresent(key1));

        //Manually put data into the cache
        int value1 = 2;
        cache.put(key1, value1);

        //Use the getifpresent method to get the value from the cache. If the specified value does not exist in the cache, the method returns null:
        System.out.println(cache.getIfPresent(1));

        //Remove data and invalidate it
        cache.invalidate(1);
        System.out.println(cache.getIfPresent(1));
    }
}

As mentioned above, there are two ways to get data. One is getifpercent. If there is no data, null will be returned. To get data, a function object needs to be provided. When there is no query key in the cache, the function will be used to provide the default value and will be inserted into the cache.

Intern Xiao Zhang: if there are multiple threads to get at the same time, will the function object be executed multiple times?

Actually, it won’t. as can be seen from the structure diagram, the most important data structure inside cafe is a concurrent HashMap, and the final execution of the get process is ConcurrentHashMap.compute , which will only be executed once.

Next, let’s talk about loading data synchronously

/**
 * @author xifanxiaxue
 * @date 2020/11/19 9:47
 *@ desc load data synchronously
 */
public class CaffeineLoadingTest {

    /**
     *Simulate reading key from database
     *
     * @param key
     * @return
     */
    private int getInDB(int key) {
        return key + 1;
    }

    @Test
    public void test() {
        //Initialize the cache, set the write expiration time of 1 minute, and set the maximum number of 100 caches
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100)
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        return getInDB(key);
                    }
                });

        int key1 = 1;
        //Get data, if not, read the relevant data from the database, and the value will also be inserted into the cache
        Integer value1 = cache.get(key1);
        System.out.println(value1);

        //It supports getting a set of values directly and batch search
        Map<Integer, Integer> dataMap
                = cache.getAll(Arrays.asList(1, 2, 3));
        System.out.println(dataMap);
    }
}

The so-called synchronous loading data means that when the data cannot be got, the load function in the cacheloader object provided by the build construction will be called eventually. If the return value is returned, it will be inserted into the cache and returned. This is a synchronous operation and batch search is also supported.

Practical application: in our project, we will use this synchronization mechanism, that is, in the load function of the cacheloader object, when we can’t get the data from the cache, we will read the data from the database, and use this mechanism in combination with the database

The last one is asynchronous loading

/**
 * @author xifanxiaxue
 * @date 2020/11/19 22:34
 *@ desc load asynchronously
 */
public class CaffeineAsynchronousTest {

    /**
     *Simulate reading key from database
     *
     * @param key
     * @return
     */
    private int getInDB(int key) {
        return key + 1;
    }

    @Test
    public void test() throws ExecutionException, InterruptedException {
        //Setting thread pool with executor
        AsyncCache<Integer, Integer> asyncCache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();
        Integer key = 1;
        //Get returns completable future
        CompletableFuture<Integer> future = asyncCache.get(key, new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer key) {
                //The thread of execution is no longer main, but the thread provided by the forkjoinpool thread pool
                System.out.println (current thread:+ Thread.currentThread ().getName());
                int value = getInDB(key);
                return value;
            }
        });

        int value = future.get();
        System.out.println (current thread:+ Thread.currentThread ().getName());
        System.out.println(value);
    }
}

The results are as follows

The most authoritative caffeine course in the whole network

You can see that getindb is executed in the thread provided by the thread pool forkjoinpool, and asyncCache.get () returns a complete future, which is familiar to those who are familiar with streaming programming. You can use complete future to realize asynchronous serial.

Intern Xiao Zhang: I see that the default is the thread provided by the thread pool forkjoinpool. In fact, it is unlikely to use the default, so can we specify it ourselves?

The answer is yes, for example

//Setting thread pool with executor
AsyncCache<Integer, Integer> asyncCache = Caffeine.newBuilder()
        .expireAfterWrite(1, TimeUnit.MINUTES)
        .maximumSize(100).executor(Executors.newSingleThreadExecutor()).buildAsync();

Intern Xiao Zhang: it’s said that caffeien’s best part is its sound elimination mechanism. Can you talk about it?

Yes, in fact, compared with concurrent HashMap, caffeine provides a complete elimination mechanism.

Based on the basic requirements, caffeine provides three elimination mechanisms:

  • Size based
  • Based on weight
  • Time based
  • Reference based

Basically, these three are enough for us. Next, I will give relevant examples for these.

The first is based on size elimination. The setting method is maximum size, which means that when the cache size exceeds the configured size limit, recycling will occur.

/**
 * @author xifanxiaxue
 * @date 2020/11/19 22:34
 *@ desc based on size
 */
public class CaffeineSizeEvictionTest {

    @Test
    public  void test() throws InterruptedException {
        //Initialize the cache. The maximum number of caches is 1
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(1)
                .build();

        cache.put(1, 1);
        //The number of print buffers is 1
        System.out.println(cache.estimatedSize());

        cache.put(2, 2);
        //Sleep for a second
        Thread.sleep(1000);
        //The number of print buffers is 1
        System.out.println(cache.estimatedSize());
    }
}

I set the maximum number of caches to 1. When I put in two data, the first one will be eliminated. At this time, there is only one cache left.

The reason why you need to sleep for one second in the demo is that data elimination is an asynchronous process, and asynchronous recovery ends when you sleep for one second.

Next, let’s talk about the elimination method based on weight, setting method: maximum weight (number), which means that when the cache size exceeds the configured weight limit, recycling will occur.

/**
 * @author xifanxiaxue
 * @date 2020/11/21 15:26
 *@ desc based on cache weight
 */
public class CaffeineWeightEvictionTest {

    @Test
    public void test() throws InterruptedException {
        //Initialize the cache and set the maximum weight to 2
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumWeight(2)
                .weigher(new Weigher<Integer, Integer>() {
                    @Override
                    public @NonNegative int weigh(@NonNull Integer key, @NonNull Integer value) {
                        return key;
                    }
                })
                .build();

        cache.put(1, 1);
        //The number of print buffers is 1
        System.out.println(cache.estimatedSize());

        cache.put(2, 2);
        //Sleep for a second
        Thread.sleep(1000);
        //The number of print buffers is 1
        System.out.println(cache.estimatedSize());
    }
}

I set the maximum weight to 2. The weight is calculated by using key directly. When put 1 comes in, the total weight is 1. When put 2 goes into the cache, the total weight is 3, which exceeds the maximum weight of 2. Therefore, the elimination mechanism will be triggered, and the number after recovery is only 1.

Then there is the time-based method and the time-based recycling mechanism. Caffeine provides three types, which can be divided into:

  • When the access expires, the time node starts from the last read or write, that is, get or put.
  • When the write expires, the time node is counted from the beginning of the write, that is, put.
  • Custom policy, custom specific expiration time.

I’ll give you an example of these three. I’ll look at the relevant differences.

/**
 * @author xifanxiaxue
 * @date 2020/11/24 23:41
 *@ desc elimination based on time
 */
public class CaffeineTimeEvictionTest {

    /**
     *Due after visit
     *
     * @throws InterruptedException
     */
    @Test
    public void testEvictionAfterProcess() throws InterruptedException {
        //Set data expiration after 5 seconds of access
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(5, TimeUnit.SECONDS).scheduler(Scheduler.systemScheduler())
                .build();
        cache.put(1, 2);
        System.out.println(cache.getIfPresent(1));

        Thread.sleep(6000);

        System.out.println(cache.getIfPresent(1));
    }

    /**
     *Due after write
     *
     * @throws InterruptedException
     */
    @Test
    public void testEvictionAfterWrite() throws InterruptedException {
        //Set the data to expire after 5 seconds of writing
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.SECONDS).scheduler(Scheduler.systemScheduler())
                .build();
        cache.put(1, 2);
        System.out.println(cache.getIfPresent(1));

        Thread.sleep(6000);

        System.out.println(cache.getIfPresent(1));
    }

    /**
     *Custom expiration time
     *
     * @throws InterruptedException
     */
    @Test
    public void testEvictionAfter() throws InterruptedException {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfter(new Expiry<Integer, Integer>() {
                    //It will expire one second after creation. You can see that nanoseconds must be used here
                    @Override
                    public long expireAfterCreate(@NonNull Integer key, @NonNull Integer value, long currentTime) {
                        return TimeUnit.SECONDS.toNanos(1);
                    }

                    //Update 2 seconds later expired, you can see here must use nanoseconds
                    @Override
                    public long expireAfterUpdate(@NonNull Integer key, @NonNull Integer value, long currentTime, @NonNegative long currentDuration) {
                        return TimeUnit.SECONDS.toNanos(2);
                    }

                    //It will expire after 3 seconds of reading. You can see that nanoseconds must be used here
                    @Override
                    public long expireAfterRead(@NonNull Integer key, @NonNull Integer value, long currentTime, @NonNegative long currentDuration) {
                        return TimeUnit.SECONDS.toNanos(3);
                    }
                }).scheduler(Scheduler.systemScheduler())
                .build();

        cache.put(1, 2);

        System.out.println(cache.getIfPresent(1));

        Thread.sleep(6000);

        System.out.println(cache.getIfPresent(1));
    }
}

There are three demos mentioned above, which are very detailed. One additional point to be mentioned here is that when I build a cache object, I always call the scheduler( Scheduler.systemScheduler ()), the scheduler mentioned when describing the caffeine structure above. The scheduler is a mechanism for clearing data periodically. It can not be set. If it is not set, the expired data will not be cleared actively.

Intern Xiao Zhang: Diao big porridge, the problem is, if not set when the data expired, when was cleared?

In order to find out the answer to this question, I specially read the source code of caffeine, and finally found the answer, that is, when we operate the data, we will empty the expired data asynchronously, that is, when we put or get, about the source code, and I will talk about it later when we finish explaining the specific usage.

Intern Xiao Zhang: another question, why did I use scheduler in my project( Scheduler.systemScheduler ()) didn’t work?

This is really a good question. If I don’t read the documents and run the demo as carefully as I do, I don’t know how to answer this question. In fact, it’s the limitation of JDK version. Only java9 or above will take effect.

Practical application: at present, in our company’s project, we use two kinds of elimination based on cache size and expiration after access. At present, from the online performance, the effect is extremely obvious, but we should pay attention to one point, that is, remember to save the cache that needs to be put into storage, otherwise it is easy to cause data loss

The last elimination mechanism is based on reference. Many people may not have any idea about reference, so let Ruyi gate gohttps://mp.weixin.qq.com/s/-N…If you don’t understand, learn first, and then look at this cafe.

/**
 * @author xifanxiaxue
 * @date 2020/11/25 0:43
 *@ desc based on reference
 */
public class CaffeineReferenceEvictionTest {

    @Test
    public void testWeak() {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                //Set the key to weak reference, and the life cycle is the next GC
                .weakKeys()
                //Set value to weak reference, and the life cycle is the next GC
                .weakValues()
                .build();
        cache.put(1, 2);
        System.out.println(cache.getIfPresent(1));

        //Force a GC call
        System.gc();

        System.out.println(cache.getIfPresent(1));
    }

    @Test
    public void testSoft() {
        Cache<Integer, Integer> cache = Caffeine.newBuilder()
                //Set value to soft reference, and the life cycle is GC and the heap memory is not enough to trigger clearing
                .softValues()
                .build();
        cache.put(1, 2);
        System.out.println(cache.getIfPresent(1));

        //Force a GC call
        System.gc();

        System.out.println(cache.getIfPresent(1));
    }
}

There are three things to pay attention to here

  • System.gc () doesn’t necessarily trigger GC, it’s just a notification mechanism, but GC doesn’t always happen. It’s uncertain whether the garbage collector will start GC, so it’s possible to see that the setting of weakkeys is calling System.gc () without losing the cache data.
  • Asynchronous loading is not allowed to use the reference elimination mechanism. When starting the program, an error will be reported java.lang.IllegalStateException : weak or soft values can not be combined with asynccache. It is speculated that the reason is caused by the conflict between the lifecycle of asynchronous loading data and the lifecycle of reference elimination mechanism, so caffeine does not support it.
  • When using the reference elimination mechanism, it is necessary to judge whether two keys or two values are the same, using = =, rather than equals (), that is to say, two keys need to point to the same object to be considered consistent, which is likely to cause unexpected problems in cache hits.

Therefore, in conclusion, we should be cautious in using the elimination mechanism based on Citation. In fact, other elimination mechanisms are enough.

Intern Xiao Zhang: I have received a demand that it needs to be timed out for a period of time after writing. However, if there is access to data within a certain period of time, it will be timed again. What should I do?

In fact, this kind of demand is not common. Reasonably speaking, it is enough to use the expired after reading and writing, but we can’t rule out the above special situation.

Therefore, we need to talk about the refresh mechanism provided by caffeine. It’s very easy to use, just use the interface refreshafterwrite. It can be said that refreshafterwrite is actually used in combination with expireafterwrite, but we need to pay attention to some pitfalls when using refreshafterwrite, for example.

/**
 * @author xifanxiaxue
 * @date 2020/12/1 23:12
 * @desc
 */
public class CaffeineRefreshTest {

    private int index = 1;

    /**
     *Simulate reading data from database
     *
     * @return
     */
    private int getInDB() {
        //Here, index is used to show that the data is got again++
        index++;
        return index;
    }

    @Test
    public void test() throws InterruptedException {
        //It is set that the data will be expired 3 seconds after writing, and the data will be refreshed if there is data access 2 seconds later
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .refreshAfterWrite(2, TimeUnit.SECONDS)
                .expireAfterWrite(3, TimeUnit.SECONDS)
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        return getInDB();
                    }
                });
        cache.put(1, getInDB());

        //Sleep for 2.5 seconds, then take the value
        Thread.sleep(2500);
        System.out.println(cache.getIfPresent(1));

        //Sleep for 1.5 seconds, then take the value
        Thread.sleep(1500);
        System.out.println(cache.getIfPresent(1));
    }
}

You can see that I set the data to expire after 3 seconds of writing, refresh the data if there is data access after 2 seconds, and after putting the data, I first sleep for 2.5 seconds, then print the value, then sleep for 1.5 seconds, and then print the value.

Porridge: Xiao Zhang, guess what the final print is?

Intern Xiao Zhang: it should be 3.4, because the refresh time after writing is set to 2 seconds, and the first sleep has passed 2.5 seconds, so it should have taken the initiative to print.

Porridge: actually not. The final printed result is: 2 3

Pit point:I’ve studied the source code. Refreshing after writing is not automatically refreshing after a certain time as described by the method name, but automatically refreshing after accessing after a certain time. That’s the first timecache.get(1)In fact, the old value is still taken at the same timedoAfterReadIt does an automatic refresh operation, so that in the second timecache.get(1)What you get is the value after brushing.

Porridge: Xiao Zhang, you can tell me, the first sleep has passed 2.5 seconds, the second sleep has passed 1.5 seconds, the total time is 4 seconds, and the expiration time after writing is actually set to 3 seconds, why the second value is still not expired?

Intern Xiao Zhang: it should be like this: after refreshing after writing, the value is filled into the cache again, thus triggering the recalculation of the post write expiration time mechanism. So although it seems that it has passed 4 seconds when getting data for the second time, in fact, for the post write expiration mechanism, it is only 1.5 seconds.

Porridge: correct solution.

Pit point:When post write refresh is triggered, the data will be refilled, thus triggering the recalculation of post write expiration time mechanism.

Intern Xiao Zhang: the supervisor said that if the maximum number of caches is set for cafeine directly, there will be a hidden danger, that is, when the number of online players exceeds the maximum number of caches at the same time, the cache will be cleared, and then the database will be read frequently to load data. Let me solve this problem on the basis of cafeine combined with secondary cache.

Yes, at present, coffeine provides a complete set of mechanisms to facilitate our combination with L2 cache.

Before giving specific examples, we should first introduce the concept of a cachewriter. We can regard it as a callback object and call it back when we put data or remove data into the cache of caffeine.

/**
 * @author xifanxiaxue
 * @date 2020/12/5 10:18
 * @desc
 */
public class CaffeineWriterTest {

    /**
     *It serves as the second level cache and only lives to the next GC
     */
    private Map<Integer, WeakReference<Integer>> secondCacheMap =
            new ConcurrentHashMap<>();

    @Test
    public void test() throws InterruptedException {
        //Set the maximum number of caches to 1
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .maximumSize(1)
                //Set the callback of put and remove
                .writer(new CacheWriter<Integer, Integer>() {
                    @Override
                    public void write(@NonNull Integer key, @NonNull Integer value) {
                        secondCacheMap.put(key, new WeakReference<>(value));
                        System.out.println (trigger) CacheWriter.write , put key = "+ key +" into the L2 cache ");
                    }

                    @Override
                    public void delete(@NonNull Integer key, @Nullable Integer value, @NonNull RemovalCause cause) {
                        switch (cause) {
                            case EXPLICIT:
                                secondCacheMap.remove(key);
                                System.out.println (trigger cachewriter)+
                                        ". delete, clear reason: active clear, key =" + key+
                                        "Clear from L2 cache");
                                break;
                            case SIZE:
                                System.out.println (trigger cachewriter)+
                                        ". delete, clear reason: the number of caches exceeds the upper limit, key =" + key ");
                                break;
                            default:
                                break;
                        }
                    }
                })
                .build(new CacheLoader<Integer, Integer>() {
                    @Nullable
                    @Override
                    public Integer load(@NonNull Integer key) {
                        WeakReference<Integer> value = secondCacheMap.get(key);
                        if (value == null) {
                            return null;
                        }

                        System.out.println (trigger) CacheLoader.load , read key = "+ key" from L2 cache;
                        return value.get();
                    }
                });

        cache.put(1, 1);
        cache.put(2, 2);
        //Since the cache is cleared asynchronously, sleep for 1 second and wait for the clear to complete
        Thread.sleep(1000);
        
        //After the cache exceeds the upper limit to trigger clearing
        System.out.println ("get data from cafe, key is 1, value is"+ cache.get (1));
    }
}

This example is a little complicated. After all, it is used in combination with L2 cache. If it is not complicated, there is no way to show the beauty of caffeine. Let’s take a look at the secondcachemap object, which I use as L2 cache. Because of the value value, I set it as WeakReference weak reference, so the life cycle only lives to the next GC.

Porridge: Xiao Zhang, this example can solve the problem of how to combine your L2 cache. Can you tell me whether the final print result value is null or non null?

Xiao Zhang: it must be null, because the cache with key 1 has been cleared because the number of caches exceeds the upper limit.

People who are not familiar with caffeine’s operation mechanism are easy to make such mistakes as Xiao Zhang, resulting in misjudgment of the results.

In order to clarify the logic of program running, I printed out the result of program running

trigger CacheWriter.write And put the key = 1 into the L2 cache
trigger CacheWriter.write , put key = 2 into the L2 cache
trigger CacheWriter.delete , clear reason: the number of caches exceeds the upper limit, key = 1
trigger CacheLoader.load , read key = 1 from L2 cache
Get data from cafe, key is 1, value is 1
trigger CacheWriter.delete , clear reason: the number of caches exceeds the upper limit, key = 2

Combined with the code, we can see that CacheWriter.delete If the cache exceeds the upper limit, then the data in the L2 cache will not be cleared, and CacheLoader.load The data will be read from the L2 cache, so when the data with key 1 is finally loaded from the cafe, it is not null, but it is taken from the L2 cache Here’s the data.

Intern Zhang: the last trigger of “printing” CacheWriter.delete , clear reason: the number of caches exceeds the upper limit, key = 2 “what is the situation?

That’s because coffeine is callingCacheLoader.loadAfter getting the non null data, it will be put into the cache again. As a result, the number of caches exceeds the maximum limit, so the cache with key 2 is cleared.

Intern Zhang: porridge, I’d like to see how the cache hit rate is. Is there any method?

Yes, yes. If you look at the source code, you can see that caffeine has a lot of internal records, but we need to turn on the records when we build the cache.

/**
 * @author xifanxiaxue
 * @date 2020/12/1 23:12
 * @desc
 */
public class CaffeineRecordTest {

    /**
     *Simulate reading data from database
     *
     * @param key
     * @return
     */
    private int getInDB(int key) {
        return key;
    }

    @Test
    public void test() {
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                //Open record
                .recordStats()
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(@NonNull Integer key) {
                        return getInDB(key);
                    }
                });
        cache.get(1);

        //Hit rate
        System.out.println(cache.stats().hitRate());
        //Number of rejected
        System.out.println(cache.stats().evictionCount());
        //Average time spent loading new values [nanoseconds]
        System.out.println(cache.stats().averageLoadPenalty() );
    }
}

Practical application:Last time caffeine was introduced into the game, it was used torecordThis mechanism is only used for testing. Generally, it is not recommended to use this mechanism in production environment. The specific usage is to open a thread timing print hit rate, the number of rejected and the average time spent loading new values, and then judge whether the introduction of caffeine has a certain value.

Intern Xiao Zhang: I’ve already used caffeine, but there will be a problem. If the data is forgotten to be saved and put into storage, and then eliminated, the player’s data will be lost. Does caffeine provide any method for developers to do something about elimination?

Porridge: it’s true. Caffeine provides elimination monitoring, so we just need to save it in the monitor.

/**
 * @author xifanxiaxue
 * @date 2020/11/19 22:34
 *@ desc elimination notice
 */
public class CaffeineRemovalListenerTest {

    @Test
    public void test() throws InterruptedException {
        LoadingCache<Integer, Integer> cache = Caffeine.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .scheduler(Scheduler.systemScheduler())
                //Added elimination monitoring
                .removalListener(((key, value, cause) -> {
                    System.out.println (elimination notice, key: + key +, reason: + cause);
                }))
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(@NonNull Integer key) throws Exception {
                        return key;
                    }
                });

        cache.put(1, 2);

        Thread.currentThread().sleep(2000);
    }

You can see that I use removallistener to provide elimination listening, so you can see the following print results:

Elimination notice, key: 1, reason: expired

Intern Xiao Zhang: when I saw the data elimination, I provided several causes, that is, the reasons. What do they correspond to?

At present, data is eliminated for the following reasons:

  • Explicit: if this is the reason, it means that the data has been removed manually.
  • Replaced: replace, that is, remove the old data when it is put.
  • Collected: this ambiguity is actually caused by collection, that is, garbage collection. Generally, weak reference or soft reference will lead to this situation.
  • Expired: data out of date, no need to explain the reason.
  • Size: removal caused by exceeding the limit.

These are the reasons why data will be carried when it is eliminated. If necessary, we can handle different businesses according to different reasons.

Practical application:At present, some of our projects have done cache storage when the data is eliminated. After all, some developers forget to save the data manually after the logical processing, so they can only do a mechanism to avoid data loss.

The Caffeine series is basically the same now. We are ready to share a Caffeine persistent DB component with interest. We are interested in paying attention to the official account and showing you all kinds of useful technologies.

The most authoritative caffeine course in the whole network