Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

preface

When it comes to cache, the first thing that may come to mind is Redis, Memcache, etc., all of which belong to distributed cache. However, in some scenarios, we may not need distributed cache, after all, we need to introduce and maintain an additional middleware, so when the data volume is small and the access is frequent, Or some static configuration data that will not change can be considered in the local cache, so how do we usually do? If you’re writing or reading local cache code, you’ll see this:

private static final Map<K,V> LOCAL_CACHE = new ConcurrentHashMap<>();
Copy the code

It is true that this method is simple and effective, but the disadvantage is that it is too simple and lacks functions. Moreover, if it is not used much, it will bring terrible memory overflow. For example, when talking about cache, we have to mention cache elimination strategy and cache expiration strategy, but don’t worry. The powerful Guava tool library has provided us with a simple and effective Guava Cache.

It’s worth noting that don’t be fooled by the powerful Guava Cache. If your Cache scenarios don’t use these Cache features, ConcurrentHashMap may be your best bet

Guava Cache

Official address: github.com/google/guav…

Guava Cache capabilities

Introduction to use

Calculation method of cache value corresponding to key

Guava Cache can be used to Cache values that take a long time to compute (not only CPU tasks, but also I/O tasks), and only perform the actual computation when the specified key is accessed from the Cache for the first time. They are CacheLoader, Callable, and direct insert.

CacheLoader

If you create a Cache using the Cache reader method, it will be computed in the same way no matter which key you access.

@Test
public void guavaCacheTest001(a){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(2)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"Really!");
                return "cache-"+key; }}); System.out.println(loadingCache.getUnchecked("key1"));
    System.out.println(loadingCache.getUnchecked("key1"));

    System.out.println(loadingCache.getUnchecked("key2"));
    System.out.println(loadingCache.getUnchecked("key2")); } corresponding output: key1 is really computed! Cache-key1 Cache-key1 key2 cache-key2 cache-key2Copy the code

In this example, we pass in an anonymous class to the Build method of the CacheBuilder, whose load method has the logic to calculate how the cache value of a cache key will be computed if it does not exist in the cache when it is acquired. From the output, we can see that only the first time the cache is accessed is the value actually evaluated, and each cache key is evaluated the same way.

Callable

Once you get to know your cache reader, you might be wondering: What if I calculate cache values differently for different cache keys? Don’t worry, Callable will escort you:

@Test
public void testCallable(a) throws ExecutionException {
    Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
    Object cacheKey1 = cache.get("key1", () -> {
        System.out.println("Key1 really calculates.");
        return "Key1 calculation method 1";
    });
    System.out.println(cacheKey1);

    cacheKey1 = cache.get("key1",()->{
        System.out.println("Key1 really calculates.");
        return "Key1 calculation method 1";
    });
    System.out.println(cacheKey1);

    Object cacheKey2 = cache.get("key2", () -> {
        System.out.println("Key1 really calculates.");
        return "Key1 Calculation method 2";
    });
    System.out.println(cacheKey2);

    cacheKey2 = cache.get("key2",()->{
        System.out.println("Key1 really calculates.");
        return "Key1 Calculation method 2"; }); System.out.println(cacheKey2); } output: key1 actually evaluates the key1 calculation1Key1 Calculation mode1Key1 actually computes key12Key1 Calculation mode2
Copy the code

As you can see from the example, when you call GET, you can pass in a Callable to provide a special cache value calculation for this cache key.

Directly inserted into the

The logic for calculating cached values in this way is no longer managed by the Guava Cache. Instead, the caller can call PUT (key,value) to insert the cached value directly.

@Test
public void testDirectInsert(a) throws ExecutionException {
    Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
    cache.put("key1"."cache-key1");
    System.out.println(cache.get("key1", () - >"callable cache-key1")); } Output: cache-key1Copy the code

Cache flushing mechanism

The harsh truth is that we often don’t have enough memory to support our caches, so we need to use our expensive memory efficiently. That is, we need to eliminate the Cache that is not frequently used. Guava Cache provides three methods for eliminating caches: Size based culling, cache time based culling, reference based culling.

Size based culling

When the number of cache keys reaches the specified number, the cache keys will be culled according to LRU.

@Test
public void testSizeBasedEviction(a){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(3)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"Really calculated.");
                return "cached-"+ key; }}); System.out.println("First visit");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("Second visit");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("Start culling.");
    loadingCache.getUnchecked("key4");

    System.out.println("Third Visit");
    loadingCache.getUnchecked("key3");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key1"); } output: the first call to key1 actually evaluates key2 really evaluates key3 really evaluates the second call to start removing key4 really evaluates the third call to Key1 really evaluatesCopy the code

In the example above, set the maximum cache entry is 3, then, in turn, adds three cache entries, and in turn, you can see when visiting for the first time, because the cache didn’t value, so the calculation, the second visit, because value of the cache so read directly from the cache, to start out stage, The attempt is to retrieve the previously unaccessed key4, and since the maximum cache entry is 3, a value needs to be removed from the cache. Who should be removed? Following the LRU algorithm, key1 is the least frequently used recently, so it is key1 that is removed, as we can verify from the third access to the output.

Note: If maximumSize is passed 0, all keys will not be cached!

In addition to maximumSize, we can also specify the maximumWeight by maximumWeight, that is, each cached key needs to return a weight. If the sum of the weights of all cached keys is greater than the maximumWeight we specify, then LRU elimination will be implemented:

@Test
public void testWeightBasedEviction(a){
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumWeight(6).weigher((key,value)->{
        if (key.equals("key1")) {return 1;
        }
        if (key.equals("key2")) {return 2;
        }
        if (key.equals("key3")) {return 3;
        }

        if (key.equals("key4")) {return 1;
        }
        return 0;
    })
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"Really calculated.");
                return "cached-"+ key; }}); System.out.println("First visit");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("Second visit");
    loadingCache.getUnchecked("key1");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key3");

    System.out.println("Start culling.");
    loadingCache.getUnchecked("key4");
    loadingCache.getUnchecked("key3");
    loadingCache.getUnchecked("key2");
    loadingCache.getUnchecked("key1"); } output: The first access to key1 actually computes key2 really computes key3 really computes key4 really computes key1 really computes second access starts to remove key4 really computes key1 really computesCopy the code

This is not much explanation, according to the output of their own think…

Time-based culling

Guava Cache provides two methods for CacheBuilder: expireAfterAccess(long, TimeUnit) and expireAfterWrite(long, TimeUnit).

  • expireAfterAccess

As the name implies, a cache key is invalidated when a specified amount of time has elapsed since it was last accessed (read or written to).

@Test
public void testExpiredAfterAccess(a) throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterAccess(3,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"Really calculated.");
                return "cached-"+ key; }}); System.out.println("First access (write)");
    loadingCache.getUnchecked("key1");

    System.out.println("Second visit");
    loadingCache.getUnchecked("key1");

    TimeUnit.SECONDS.sleep(3);
    System.out.println("Visit in 3 seconds.");
    loadingCache.getUnchecked("key1"); } output: The first access (write) key1 really calculates the second access over3Seconds later access key1 is actually computedCopy the code

In this example, we set the cache to expire more than 3 seconds after the last access (or write), and you can see from the output that it does.

  • expireAfterWrite

As the name implies, the cache key will expire after a certain amount of time since it was last written:

@Test
public void testExpiredAfterWrite(a) throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().expireAfterWrite(3,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key+"Really calculated.");
                return "cached-"+ key; }});for (int i = 0; i < 4; i++) {
        System.out.println(new Date());
        loadingCache.getUnchecked("key1"); // Write the first time
        TimeUnit.SECONDS.sleep(1); }} Output: Sat Oct02 20:06:47 CST 2021Key1 actually calculates the Sat Oct02 20:06:48 CST 2021
Sat Oct 02 20:06:49 CST 2021
Sat Oct 02 20:06:50 CST 2021Key1 actually computesCopy the code

Again, this should be understandable based on the program and output.

Reference-based culling

Java has four major references, strong, soft, weak, virtual, if you are not familiar with these several references can first go to see my article: 😺Java four reference types: strong, soft, weak, virtual

Guava Cache provides a reference-based culling strategy, which reminds you how ThreadLocal prevents memory leaks. If you don’t know, don’t worry, keep reading the quote I posted above. Guava Cache provides three reference-based culling strategies:

  • CacheBuilder.weakKeys()

When weakKeys() is used, Guava cache will store cache keys as weak references. According to the definition of weak references, when garbage collection occurs, weak references will be collected regardless of whether the current system resources are sufficient. Example:

@Test
public void testWeakKeys(a) throws InterruptedException {
    LoadingCache<MyKey, String> loadingCache = CacheBuilder.newBuilder().weakKeys()
        .build(new CacheLoader<MyKey, String>() {
            @Override
            public String load(MyKey key) throws Exception {
                System.out.println(key.getKey()+"Really calculated.");
                return "cached-"+ key.getKey(); }}); MyKey key =new MyKey("key1");
    System.out.println("First visit");
    loadingCache.getUnchecked(key);
    System.out.println(loadingCache.asMap());

    System.out.println("Second visit");
    loadingCache.getUnchecked(key);
    System.out.println(loadingCache.asMap());

    System.out.println(Key accessed after losing strong reference GC);
    key = null;
    System.gc();
    TimeUnit.SECONDS.sleep(3);
    System.out.println(loadingCache.asMap());

}

@Data
private static class MyKey{
    String key;

    public MyKey(String key) {
        this.key = key; }}Copy the code
  • CacheBuilder.weakValues()

CacheBuilder. WeakKeys (); CacheBuilder. WeakValues (); Again, this time for cached values!

  • CacheBuilder.softValues()

With cacheBuilder.softValues () as the basis for a cacheBuilder.weakValues () weakValues() system, cacheBuilder.softValues () functions as a cat’s cat. If a weak reference is garbage collected, it will only be collected if the system resources are insufficient. .

Take the initiative to remove

In addition to the passive culling strategy described above, we can also actively call methods to clear the cache.

  • Cache.invalidate(key)
  • Cache.invalidateAll(keys)
  • Cache.invalidateAll()

Cache invalidation listeners

Sometimes we hope are eliminated when the cache invalidation, could do something afterward, at this point, we can through CacheBuilder. RemovalListener (removalListener) to specify a cache invalidation listener, when cache invalidation, the callback our listeners:

@Test
public void testRemovalListener(a) throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().removalListener(notification -> {
        System.out.println(String
            .format("Cache %s because %s is invalid, its value is %s", notification.getKey(), notification.getCause(),
                notification.getValue()));
    }).expireAfterAccess(3, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            System.out.println(key + "Really calculated.");
            return "cached-"+ key; }}); System.out.println("First access (write)");
    loadingCache.getUnchecked("key1");

    System.out.println("Second visit");
    loadingCache.getUnchecked("key1");
    TimeUnit.SECONDS.sleep(3);

    System.out.println("3 seconds");
    loadingCache.getUnchecked("key1"); } output: The first access (write) key1 actually computes the second access3After a second the cache key1 is EXPIRED because EXPIRED and its value is cached-key1 key1 is actually computedCopy the code

What does Guava Cache clean?

This is actually a problem that I discovered in the last section when I experimented with the cache cull listener: if I don’t do anything after the cache has expired, the cache listener will not be called! The Guava cache does not clean the stale caches on its own initiative, but checks and cleans them when the cache is being operated on. So why? Think ah, if you want to take the initiative to clear, it must be a has been running a background thread to perform cleanup, many threads, so means that is no longer a single-threaded program, involve multithreading will consider locking resources protection, this will undoubtedly we consume resources and impact performance, and take the initiative to clear and is not a must, waiting for you to operate the clear again, was not a bit late!

The Guava cache also provides an active cleanUp method: cache.cleanup (), which is up to us to weigh up.

Cache refresh

RefreshAfterWrite is provided in the CacheBuilder to specify how long a cache key is written before it is recalculated and cached:

@Test
public void testRefresh(a) throws InterruptedException {
    LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().refreshAfterWrite(1,TimeUnit.SECONDS)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                System.out.println(key + "Really calculated.");
                return "cached-"+ key; }});for (int i = 0; i < 3; i++) {
        loadingCache.getUnchecked("key1");
        TimeUnit.SECONDS.sleep(2); }} Output key1 actually evaluates key1Copy the code

In this example, we specify that the cache key is flushed after writing more than one second, and then we access the cache key every two seconds, and you can see that it is recalculated each time!

summary

This article provides a detailed introduction to the use of Guava Cache through a number of code examples, but did you think it would stop there? Due to the length of the reason, this article for the use of the principle, the next will be introduced, our purpose is to draw the essence from the source code design of these bigwigs, the so-called know yourself know your enemy ~