Redis Mastery Series - Remove Old and Welcome LRU

This article is included in the column

❤️❤️

Thousands of people praise the collection, a complete set of Redis learning materials, and essential factory skills!

Catalog

1. Introduction

2. maxmemory configuration

3. What if memory reaches maxmemory

4. Implementation of LRU algorithm

5. Redis's Approximate LRU

6. Problems

1. Introduction

Redis is a key-value database based on memory storage. We know that memory is fast but small, and when the physical memory reaches the maximum, the system runs very slowly because the swap mechanism transfers part of the memory data to swap partitions and ensures that the system continues to run by swap; swap is hard disk storage and is much faster than memory, especially for swapIn Redis, a very high QPS service, this happens and cannot be received. (Note that if swap partition memory is full, system errors will occur!)

The swap size can be viewed through free-m on Linux operating systems:

So it's important to prevent this from happening to Redis (interviewers ask Redis hardly without asking this point of knowledge).

2. maxmemory configuration

Redis provides a maxmemory configuration for these issues, which specifies the maximum dataset of Redis storage, typically in the redis.conf file, or at run time using the CONFIG SET command for one-time configuration.
Configuration item diagram in redis.conf file:

The default maxmemory configuration item is not enabled. Redis officially recommends that 64-bit operating systems have no memory limitation by default and 32-bit operating systems have 3GB implicit memory configuration by default. If maxmemory is 0, memory is unlimited.

So when we do the cache architecture, we need to make the appropriate maxmemory configuration based on hardware resources + business requirements.

3. What if memory reaches maxmemory

It is clear that maximum memory is configured, and Redis can't stop working when maxmemory reaches its maximum, so how does Redis handle this problem? This is the focus of this article. Redis provides a maxmemory-policy phase-out strategy (LRU does not involve LFU in this article, LFU in the next article). Remove key s that meet the criteria and say goodbye to the old.
maxmemory-policy phase-out strategy:

  • noeviction: An error is returned when a memory limit is reached and a client attempts to execute a command that may result in more memory being used. Simply speaking, read operations are still allowed, but new data must not be written. Delete requests are possible.
  • allkeys-lru: Eliminate from all keys by LRU (Least Recently Used - least recently used) algorithm
  • allkeys-random: Randomly eliminate from all keys
  • volatile-lru: Eliminate by LRU (Least Recently Used - least recently used) algorithm from all key s with expiration time set, which ensures that data that needs to be persisted without expiration time set is not selected for Elimination
  • volatile-random: Random elimination from all key s with expiration time set
  • volatile-ttl: From all keys with expiration time set, by comparing the values of TTL for the remaining expiration time of key, the smaller the TTL, the earlier it will be eliminated

There is also volatile-lfu/allkeys-lfu left to discuss below, the two algorithms are different!

random phase-out only needs to delete some keys randomly to free up memory space; small TTL expiration time, first phase-out can also be compared with the size of ttl, delete keys with small TTL value to free up memory space.
So how does LRU work? How does Redis know which key s have been used recently and which have not been used recently?

4. Implementation of LRU algorithm

First, we implement a simple LRU algorithm using Java containers, we use ConcurrentHashMap to map elements to key-value results, and ConcurrentLinkedDeque to maintain key access order.
LRU implementation code:

package com.lizba.redis.lru;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;

/**
 * <p>
 *      LRU Simple implementation
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/17 23:47
 */
public class SimpleLru {

    /** Data Cache*/
    private ConcurrentHashMap<String, Object> cacheData;
    /** Access Sequence Record*/
    private ConcurrentLinkedDeque<String> sequence;
    /** Cache capacity*/
    private int capacity;

    public SimpleLru(int capacity) {
        this.capacity = capacity;
        cacheData = new ConcurrentHashMap(capacity);
        sequence = new ConcurrentLinkedDeque();
    }


    /**
     * Set Value
     *
     * @param key
     * @param value
     * @return
     */
    public Object setValue(String key, Object value) {
        //Determine if LRU phase-out is required
        this.maxMemoryHandle();
        //Include removes elements, and newly accessed elements remain at the top of the queue
        if (sequence.contains(key)) {
            sequence.remove();
        }
        sequence.addFirst(key);
        cacheData.put(key, value);
        return value;
    }


    /**
     * Maximum memory, eliminating the least recently used key
     */
    private void maxMemoryHandle() {
        while (sequence.size() >= capacity) {
            String lruKey = sequence.removeLast();
            cacheData.remove(lruKey);
            System.out.println("key: " + lruKey + "Was eliminated!");
        }
    }


    /**
     * Get access to LRU order
     *
     * @return
     */
    public List<String> getAll() {
        return Arrays.asList(sequence.toArray(new String[] {}));
    }
}

Test code:

package com.lizba.redis.lru;

/**
 * <p>
 *      Test least recently used
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/18 0:00
 */
public class TestSimpleLru {

    public static void main(String[] args) {
        SimpleLru lru = new SimpleLru(8);
        for (int i = 0; i < 10; i++) {
            lru.setValue(i+"", i);
        }
        System.out.println(lru.getAll());
    }

}

Test results:

From the above test results, we can see that key0 was added first, key1 was eliminated, and the last key added was also the latest key stored in the queue head of sequence.
With this scheme, the LRU algorithm can be easily implemented; however, the drawback is obvious. The scheme requires additional data structures to preserve the access order of key s, which increases the memory consumption of Redis and consumes a lot of memory to optimize the memory itself, which is obviously not possible.
 

5. Redis's Approximate LRU

In this case, Redis uses an approximate LRU algorithm, which does not completely and accurately eliminate the most recently used key s, but overall accuracy can be guaranteed.
The approximate LRU algorithm is very simple. In the Redis key object, 24 bits are added to store the last access system time stamp. When a client sends a write-related request to the Redis server, it finds that the memory reaches maxmemory, which triggers lazy deletion. The Redis service selects five keys that meet the criteria through random sampling.(Note that this random sample allkeys-lru is randomly sampled from all keys, and volatile-lru is randomly sampled from all keys with an expiration time set). By comparing the recent access time stamps recorded in the key object, the oldest of the five keys is eliminated; if memory is still insufficient, repeat this step.

Note that 5 is Redis's default random sample size and can be configured with maxmemory_samples in redis.conf:

For the above random LRU algorithm, Redis officially gives a data map of test accuracy:

  • The top light gray indicates the key being eliminated. Fig. 1 is a diagram of the elimination of the standard LRU algorithm.
  • The middle dark gray layer represents old key s that have not been phased out
  • The bottom light green indicates the recently visited key

By the time Redis 3.0 maxmemory_samples was set to 10, Redis's approximate LRU algorithm was very close to the real LRU algorithm, but it was clear that setting maxmemory_samples to 10 would consume more CPU computation time than setting maxmemory_samples to 5, because the calculation time would increase with each sample data being sampled.
Redis 3.0's LRU is more accurate than Redis 2.8's LRU algorithm because Redis 3.0 adds a phase-out pool the same size as maxmemory_samples. Each time you phase out a key, you first compare it with the key waiting to be phased out in the phase-out pool, and finally eliminate the oldest key. In fact, you put the selected and eliminated keys together and then compare them to eliminate the oldest one.

6. Problems

The LRU algorithm seems to be useful, but there are also unreasonable things, such as A and B key s, which were added to Redis at the same time an hour before the phase-out happened. A was visited 1000 times in the first 49 minutes but not 11 minutes; B was visited only once in the 59 minutes in that hour; at this time, if you use the LRU algorithm, if A and B were selected by Redis samplingOf course, A will be eliminated, which is obviously unreasonable.
Redis 4.0 adds the least frequently used LFU algorithm, which is more reasonable than LRU. We'll learn to phase out the algorithm later, so pay attention to my column if you need to.

Tags: Java Redis Interview

Posted on Sun, 19 Sep 2021 12:22:50 -0400 by duny