Analysis of Guava Cache Implementation Principle

I. Overview

In the last article "Guava Cache Doing Local Cache Things" I introduced the general usage of Guava Cache as a local cache, described the method of constructing LoadingCache and its basic use, as well as the difference between the two get methods, the difference between LoadingCache and Cache And other content. In order to know why and why, this article briefly introduces the implementation principle of Guava Cache. If there is something wrong, please criticize and correct it.

First of all, put out the source link of Guava Cache on github. There are notes written by the author, which is convenient for everyone to learn.
Guava Cache annotated source code

2. Issues of concern

From the above source code and the previous article on the usage of Guava Cache, you can see that Guava Cache involves a lot of classes and codes. If you analyze the files one by one, there are too many details involved, and it is not easy to grasp the key issues. So the author decided to analyze the principle of Guava Cache from the execution process of the get method. The analysis mainly focuses on the following three issues:

  1. How Guava Cache maps to value through key;
  2. How to call the CacheLoader.load method to load the cache when the cache is missed;
  3. How are the two cache invalidation strategies implemented?

3. Source code analysis

3.1 Key-value mapping process

Let's take a look at the key-value mapping process through the following example:

    public static void demo1() throws Exception {
    	//定义Cache,并定义未命中时的载入方法
        LoadingCache<Integer, Integer> cache = CacheBuilder.newBuilder().build(new CacheLoader<Integer, Integer>() {
            @Override
            public Integer load(Integer key) throws Exception {
                return key + 1;
            }
        });
        cache.get(1); //第一个get:第一次访问key,使用载入方法载入
        cache.get(1); //第二个get:第二次访问,在缓存中得到value
    }

The above demo defines a LoadingCache and passes in the CacheLoader to provide a processing method when the cache is missed. Later, you can see that the get method is called twice. The first time is to load the corresponding value into the cache through the load method. This process is introduced in section 3.2. This section mainly looks at the second get process: how to get the value through the key when the corresponding value exists in the cache.

Through the debug breakpoint, we enter the get method and observe its execution process:

step1:

The first entry is the following method, you can infer that the LoaclLoadingCache it is in is the default implementation of the LoadingCache interface. This class is an internal class of LoaclCache. The get method directly calls the getOrLoad method.

static class LocalLoadingCache<K, V> extends LocalCache.LocalManualCache<K, V> implements LoadingCache<K, V> {
        public V get(K key) throws ExecutionException {
            return this.localCache.getOrLoad(key);
        }
}

step2:
We continue to look down at the getOrLoad method and find that it is only a layer of encapsulation, go on!

    V getOrLoad(K key) throws ExecutionException {
        return this.get(key, this.defaultLoader);
    }

step3:
Continue to track down to the LocalCache.get method. Some operations can finally be seen here. There are two sentences in total, analyze them separately.

The first sentence is to find the hash value, where the Preconditions.checkNotNull method is used to check the null pointer. If the key is null, a null pointer exception is thrown, and then the hash operation is performed. In the hash operation, first obtain the return value of key.hashCode(), and then perform the rehash operation. The purpose of rehash is to prevent serious hash conflicts caused by poor quality of key.hashCode(). The specific method I understand is to perform a series of bit operations to confuse the high and low hash values.

The second sentence is the focus of our analysis, please see step 4.

    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = this.hash(Preconditions.checkNotNull(key));
        return this.segmentFor(hash).get(key, hash, loader);
    }

step4:
LoadingCache uses a method similar to ConcurrentHashMap to divide the mapping table into multiple segments. The segments can be accessed concurrently, which can greatly improve the efficiency of concurrency and reduce the possibility of concurrent conflicts (there is still possible conflict when accessing keys on a segment at the same time).

The last line of code in step 3 can be divided into two parts, one is this.segmentFor(hash), which is to determine which segment the key is located on through the hash value, and obtain the segment. Instead, execute the get method through the segment to obtain the specific value.

First look at the code of this.segmentFor(hash):

LocalCache.Segment<K, V> segmentFor(int hash) {
    return this.segments[hash >>> this.segmentShift & this.segmentMask];
}

It can be seen that the segments are stored in an array. The length of the array is a power of 2 and equal to the concurrency level of LoadingCache. So how do segmentShift and segmentMask take values? SegmentMash can be understood as the length of the array minus one, so that the range after the AND operation is always within the length of the array. If you are interested in the value of segmentShift, you can refer to the construction method of the source code LocalCache, so I won't go into details here.

After obtaining the segment, we can execute our final get method. The core main code is as follows:

	   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

The code ReferenceEntry<K, V> e = this.getEntry(key, hash); obtains the key-value pair related to the key, and the subsequent code performs operations such as expiration strategy (introduced in section 3.3) and loading operation (introduced in 3.2). Here we mainly look at the getEntry method.

Similar to HashMap and so on, segment also uses the data structure of array plus linked list. First, the hash value and the length of the array are minus one to get the head node of the linked list, and then the real result is obtained by traversing the linked list.

Summary The
above is the analysis of the get method of LocalCache. It can be seen that its implementation method is basically similar to the implementation in Java standard class libraries such as ConcurrentHashMap except for some details. From the analysis of step 4, we also know that CacheLoader.load will be called and the cache invalidation judgment will be performed when the get operation is performed. Let's take a look at the principles of these two pieces of content in detail below.

3.2 The calling mechanism of CacheLoader.load

First review the knowledge of the previous article , CacheLoader.load is passed to the CacheBuilder when the LoadingCache is built, and is called in the case of a cache miss. In step 4 of Section 3.1, we saw the entry point where Load is called. For the convenience of reading, we will post the code here:

	   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               if (value != null) {
                   this.recordRead(e, now);
                   this.statsCounter.recordHits(1);
                   Object var17 = this.scheduleRefresh(e, key, hash, value, now, loader);
                   return var17;
               }

               LocalCache.ValueReference<K, V> valueReference = e.getValueReference();
               if (valueReference.isLoading()) {
                   Object var9 = this.waitForLoadingValue(e, key, valueReference);
                   return var9;
               }
           }
       }
       //未载入的情况,3.2节进行分析
       Object var15 = this.lockedGetOrLoad(key, hash, loader);
       return var15;

You can see that when this.count==0, the this.lockedGetOrLoad method of the penultimate line will be executed, and CacheLoader.load is called in this method. Since this is a write operation, the segment needs to be locked. Segment is a subclass of ReentrantLock and can be locked by directly calling its lock() method. Then call CacheLoader.load() to get the value and put it into the linked list in the table. The specific code is more cumbersome and will not be analyzed anymore.

3.3 Implementation of two cache expiration strategies

Guava Cache can set two expiration strategies, one is that the read cache does not expire within a unit time, and the other is that the write cache does not expire within a unit time.

The entry for judging whether it has expired is in the segment.get mentioned in step 4 of section 3.1:

   //count记录当前segment中存活的缓存个数
       if (this.count != 0) {
       	   //其实这一句就已经获取到了value值,下面的代码进行了过期判断等操作
           ReferenceEntry<K, V> e = this.getEntry(key, hash);
           if (e != null) {
               long now = this.map.ticker.read();
               //判断是否过期
               V value = this.getLiveValue(e, now);
               ……
           }
           ……
       }

Tracing the getLiveValue method all the way, I found the position of the following judgment:

    boolean isExpired(ReferenceEntry<K, V> entry, long now) {
        Preconditions.checkNotNull(entry);
        if (this.expiresAfterAccess() && now - entry.getAccessTime() >= this.expireAfterAccessNanos) {
            return true;
        } else {
            return this.expiresAfterWrite() && now - entry.getWriteTime() >= this.expireAfterWriteNanos;
        }
    }

You can see whether the expiration judgment is made according to the expiration policy, the current time, and the set expiration time. At the same time, we also know that Guava Cache's judgment of expiration is not to open a separate thread, but to judge the expiration when reading and writing operations.

Pay attention to the public account and receive more articles from me in the first time
Insert picture description here

Guess you like

Origin blog.csdn.net/vxzhg/article/details/102648232