Java handwritten redis from scratch (7) detailed explanation of LRU cache elimination strategy

Time:2020-10-31

preface

Java implements redis from zero handwriting?

Java realizes redis from zero handwriting

How to restart memory data without losing it?

Java realizes redis from zero handwriting

Another way to realize the expiration policy of redis (5) from zero handwriting in Java

Java realizes redis from zero handwriting

We have implemented several features of redis,Java implements redis from zero handwriting?The first in first out (FIFO) drive strategy is realized.

But in practice, LRU / LFU is generally recommended.

Java handwritten redis from scratch (7) detailed explanation of LRU cache elimination strategy

LRU Basics

Extended learning

Apache Commons lrumap source code details

Redis is used as LRU map

What is LRU

LRU is composed of the first letter of least recently used, which means the least recently used meaning. It is generally used in object elimination algorithm.

It is also a common elimination algorithm.

Its core idea isIf data has been accessed recently, it is more likely to be accessed in the future

Continuity

In computer science, there is a guiding principle: Continuity criterion.

Time continuity: access to information that has been accessed recently is likely to be accessed again. Caching is based on this concept for data obsolescence.

Spatial continuity: for the access of disk information, it is very likely to access continuous spatial information. Therefore, there will be page prefetching to improve performance.

Implementation steps

  1. The new data is inserted into the head of the linked list;
  2. Whenever the cache hits (that is, the cache data is accessed), the data is moved to the head of the linked list;
  3. When the list is full, the data at the end of the list is discarded.

In fact, it is relatively simple. Compared with the FIFO queue, we can introduce a linked list.

A little thought

Let’s consider the above three sentences sentence by sentence to see if there are any points or pits worthy of optimization.

How to judge whether it is new data?

(1) The new data is inserted into the head of the linked list;

We use linked lists.

The simplest way to judge whether new data exists is to traverse. For linked lists, this is an O (n) time complexity.

In fact, the performance is relatively poor.

Of course, we can also consider space for time, such as introducing a set, but the pressure on space will be doubled.

What is a cache hit

(2) Whenever the cache hits (that is, the cache data is accessed), the data is moved to the head of the linked list;

Put (key, value) is a new element. If this element already exists, you can delete it and then add it. Refer to the above processing.

In the case of get (key), for element access, delete the existing element and put the new element in the header.

Remove (key) remove an element and delete the existing element directly.

Keyset() valueset() entryset() these belong to indifference access. We do not adjust the queue.

remove

(3) When the list is full, the data at the end of the list is discarded.

There is only one scenario when a linked list is full, that is, when an element is added, that is, when a put (key, value) is executed.

Delete the corresponding key directly.

Java code implementation

Interface definition

The interface of FIFO is consistent with that of FIFO, and the calling place is also unchanged.

For the subsequent LRU / LFU implementation, two methods of remove / update are added.

public interface ICacheEvict<K, V> {

    /**
     *Expulsion strategy
     *
     *@ param context context
     * @since 0.0.2
     *@ return do you want to expel
     */
    boolean evict(final ICacheEvictContext<K, V> context);

    /**
     *Update key information
     * @param key key
     * @since 0.0.11
     */
    void update(final K key);

    /**
     *Delete key information
     * @param key key
     * @since 0.0.11
     */
    void remove(final K key);

}

LRU implementation

Directly based on LinkedList:

/**
 *Discard policy - LRU least recently used
 * @author binbin.hou
 * @since 0.0.11
 */
public class CacheEvictLRU<K,V> implements ICacheEvict<K,V> {

    private static final Log log = LogFactory.getLog(CacheEvictLRU.class);

    /**
     *List information
     * @since 0.0.11
     */
    private final List<K> list = new LinkedList<>();

    @Override
    public boolean evict(ICacheEvictContext<K, V> context) {
        boolean result = false;
        final ICache<K,V> cache = context.cache();
        //Remove the element at the end of the team if the limit is exceeded
        if(cache.size() >= context.size()) {
            K evictKey = list.get(list.size()-1);
            //Remove the corresponding element
            cache.remove(evictKey);
            result = true;
        }
        return result;
    }


    /**
     *Put in the element
     *(1) Delete existing
     *(2) Put the new element in the head of the element
     *
     *@ param key element
     * @since 0.0.11
     */
    @Override
    public void update(final K key) {
        this.list.remove(key);
        this.list.add(0, key);
    }

    /**
     *Remove element
     *@ param key element
     * @since 0.0.11
     */
    @Override
    public void remove(final K key) {
        this.list.remove(key);
    }

}

Compared with FIFO, it has three more methods

Update(): let’s simplify it a little. We think that as long as it is to access, it is to delete it and insert it to the head of the team.

Remove (): delete is to delete directly.

These three methods are used to update recent usage.

When is it called?

Annotation Properties

To ensure the core process, we implement it based on annotations.

Add attribute:

/**
 *Do you want to perform eviction update
 *
 *It is mainly used to drive out LRU
 *@ return Yes No
 * @since 0.0.11
 */
boolean evict() default false;

Annotation usage

What methods need to be used?

@Override
@CacheInterceptor(refresh = true, evict = true)
public boolean containsKey(Object key) {
    return map.containsKey(key);
}

@Override
@CacheInterceptor(evict = true)
@SuppressWarnings("unchecked")
public V get(Object key) {
    //1. Refresh all expired information
    K genericKey = (K) key;
    this.expire.refreshExpire(Collections.singletonList(genericKey));
    return map.get(key);
}

@Override
@CacheInterceptor(aof = true, evict = true)
public V put(K key, V value) {
    //...
}

@Override
@CacheInterceptor(aof = true, evict = true)
public V remove(Object key) {
    return map.remove(key);
}

Implementation of annotation elimination interceptor

Execution order: update after the method, otherwise the key of each current operation will be put at the top.

/**
 *Kill strategy interceptor
 * 
 * @author binbin.hou
 * @since 0.0.11
 */
public class CacheInterceptorEvict<K,V> implements ICacheInterceptor<K, V> {

    private static final Log log = LogFactory.getLog(CacheInterceptorEvict.class);

    @Override
    public void before(ICacheInterceptorContext<K,V> context) {
    }

    @Override
    @SuppressWarnings("all")
    public void after(ICacheInterceptorContext<K,V> context) {
        ICacheEvict<K,V> evict = context.cache().evict();

        Method method = context.method();
        final K key = (K) context.params()[0];
        if("remove".equals(method.getName())) {
            evict.remove(key);
        } else {
            evict.update(key);
        }
    }

}

We only make a special judgment on the remove method. Other methods use update to update the information.

Parameter takes the first parameter directly.

test

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lru())
        .build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");

//Visit a
cache.get("A");
cache.put("D", "LRU");
Assert.assertEquals(3, cache.size());

System.out.println(cache.keySet());
  • log information
[D, A, C]

From the removelistener log, you can also see that B has been removed

[DEBUG] [2020-10-02 21:33:44.578] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict

Summary

In fact, the elimination strategy of redis LRU is not the real LRU.

A big problem with LRU is that every time o (n) is searched, it is still very slow when the number of keys is particularly large.

If redis is designed like this, it will be very slow.

Personal understanding is that you can exchange space for time, such as adding oneMap<String, Integer>The speed of searching keys and subscripts, O (1), stored in the list, has doubled the space complexity.

But it’s worth the sacrifice. This kind of follow-up unified optimization, all kinds of optimization points are considered in a unified way, so as to coordinate the overall situation and facilitate unified adjustment in the later stage.

In the next section, we will work together to implement the following improved versions of the LRU.

What redis does is to make the seemingly simple things to the extreme, which is worth learning from every open source software.

This paper mainly describes the ideas, the implementation part because of space constraints, not all pasted out.

Open source address:https://github.com/houbb/cache

If you feel that this article is helpful to you, you are welcome to comment on it~

Your encouragement is my biggest motivation~

Java handwritten redis from scratch (7) detailed explanation of LRU cache elimination strategy

Recommended Today

Comparison and analysis of Py = > redis and python operation redis syntax

preface R: For redis cli P: Redis for Python get ready pip install redis pool = redis.ConnectionPool(host=’39.107.86.223′, port=6379, db=1) redis = redis.Redis(connection_pool=pool) Redis. All commands I have omitted all the following commands. If there are conflicts with Python built-in functions, I will add redis Global command Dbsize (number of returned keys) R: dbsize P: print(redis.dbsize()) […]