java redis manuscrite à partir de zéro (dix) algorithme d'élimination du cache LFU fréquence la moins utilisée

Préface

Java implémente redis à la main à partir de zéro (1) Comment obtenir un cache de taille fixe?

Java implémente Redis à la main à partir de zéro (trois) principe d'expiration de Redis Expire

Java implémente redis à la main à partir de zéro (3) Comment redémarrer sans perdre de données mémoire?

Java implémente Redis à partir de zéro à la main (quatre) Add Listener

Une autre façon de mettre en œuvre la stratégie d'expiration de redis (5) à partir de zéro

Java implémente Redis à la main à partir de zéro (6) Principe de persistance AOF détaillé et implémentation

Java implémente le redis à la main à partir de zéro (7) Explication détaillée de la stratégie d'élimination du cache LRU

Redis écriture manuscrite à partir de zéro (huit) Optimisation des performances de l'algorithme d'élimination LRU simple

Dans cette section, découvrons un autre algorithme d'élimination de cache couramment utilisé, l'algorithme de fréquence le moins utilisé de LFU.

Principes de base du LFU

concept

LFU (le moins fréquemment utilisé) est le moins fréquemment utilisé récemment. Il suffit de regarder le nom et vous saurez qu'il s'agit d'un algorithme basé sur la fréquence d'accès.

LRU est basé sur le temps et éliminera les données les moins fréquemment consultées dans le temps et les placera en tête de liste en termes de performances des algorithmes; LFU élimine les données les moins fréquemment consultées en fréquence.

Comme il est basé sur la fréquence, il est nécessaire de stocker le nombre de fois que chaque accès aux données.

En termes d'espace de stockage, il y aura plus d'espace pour les comptes de stockage que LRU.

idée principale

Si une donnée a rarement été utilisée au cours de la période récente, il est peu probable qu'elle soit utilisée à l'avenir.

Idée de réalisation

Suppression O (N)

Afin d'éliminer les données les moins utilisées, mon premier instinct est d'en faire directement une HashMap<String, Interger>, String correspond à l'information clé, et Integer correspond au nombre de fois.

Accédez à +1 à chaque visite, la complexité temporelle du réglage et de la lecture est O (1); mais la suppression est plus gênante, vous devez parcourir toutes les comparaisons, et la complexité temporelle est O (n);

Suppression O (logn)

Une autre idée d'implémentation est d'utiliser le petit tas supérieur + hashmap, les opérations d'insertion et de suppression du petit tas supérieur peuvent atteindre une complexité de temps O (logn), donc l'efficacité est plus efficace que la première méthode d'implémentation. Tels que TreeMap.

Suppression O (1)

Peut-il être encore optimisé?

En fait, il existe des algorithmes O (1), voir cet article:

Un algorithme O (1) pour implémenter le schéma d'éviction de cache LFU

Parlez brièvement de vos pensées personnelles:

Si nous voulons réaliser l'opération O (1), nous ne devons pas nous passer de l'opération Hash. Dans notre suppression O (N), nous réalisons O (1) put / get.

Mais les performances de suppression sont médiocres, car il faut du temps pour trouver le moins de fois possible.

private Map<K, Node> map; // key和数据的映射
private Map<Integer, LinkedHashSet<Node>> freqMap; // 数据频率和对应数据组成的链表

class Node {
    K key;
    V value;
    int frequency = 1;
}

Nous pouvons fondamentalement résoudre ce problème en utilisant le double Hash.

La carte stocke la relation de mappage entre les clés et les nœuds. Put / get sont définitivement O (1).

Le nœud mappé par la clé a les informations de fréquence correspondantes; la même fréquence sera associée via freqMap, et la liste chaînée correspondante peut être rapidement obtenue par fréquence.

La suppression est également devenue très simple: en gros, on peut déterminer que la fréquence la plus basse de suppression est 1. Si elle n'est pas au plus 1 ... n, la fréquence minimale sélectionne le premier élément de la liste chaînée et commence la suppression.

Quant à la priorité de la liste chaînée elle-même, elle peut être basée sur FIFO ou d'autres méthodes que vous aimez.

Introduction au contenu de base du papier

Les pierres d'autres collines peuvent apprendre.

Avant d'implémenter le code, lisons cet article O (1).

introduction

La structure de cet article est la suivante.

Une description du cas d'utilisation LFU, qui peut s'avérer supérieur à d'autres algorithmes d'éviction de cache

L'implémentation du cache LFU doit prendre en charge les opérations de dictionnaire. Ce sont les opérations qui déterminent la complexité de l'exécution de la stratégie

Description de l'algorithme LFU le plus connu et de sa complexité d'exécution

Description de l'algorithme LFU proposé; la complexité d'exécution de chaque opération est O (1)

Utilisations de LFU

Envisagez une application de proxy Web de mise en cache pour le protocole HTTP.

Le proxy est généralement situé entre Internet et l'utilisateur ou un groupe d'utilisateurs.

Il garantit que tous les utilisateurs peuvent accéder à Internet et réaliser le partage de toutes les ressources partageables pour obtenir la meilleure utilisation du réseau et la meilleure vitesse de réponse.

Un tel agent de mise en cache devrait essayer de maximiser la quantité de données qu'il peut mettre en cache dans la quantité limitée de stockage ou de mémoire à sa disposition.

En règle générale, les ressources statiques (telles que les images, les feuilles de style CSS et le code javascript) peuvent être facilement mises en cache pendant une longue période avant de les remplacer par des versions plus récentes.

Ces ressources statiques ou ce que les programmeurs appellent des «actifs» sont contenues dans presque toutes les pages. Leur mise en cache est donc la plus avantageuse car elles seront nécessaires pour presque toutes les requêtes.

De plus, étant donné que les agents réseau sont tenus de traiter des milliers de requêtes par seconde, la surcharge requise pour ce faire doit être réduite au minimum.

Pour cette raison, il ne doit expulser que les ressources qui ne sont pas fréquemment utilisées.

Par conséquent, vous devez conserver les ressources fréquemment utilisées sur des ressources moins fréquemment utilisées, car la première s'est avérée utile sur une période de temps.

Bien sûr, il y a un dicton à l'effet contraire, il dit qu'il n'y aura peut-être pas beaucoup de ressources utilisées à l'avenir, mais nous avons constaté que ce n'est pas le cas dans la plupart des cas.

Par exemple, les ressources statiques des pages fréquemment utilisées sont toujours demandées par chaque utilisateur de la page.

Par conséquent, lorsque la mémoire est insuffisante, ces agents de mise en cache peuvent utiliser la stratégie de remplacement de cache LFU pour expulser les éléments les moins utilisés de leur cache.

LRU peut également être une stratégie applicable ici, mais lorsque le mode de demande fait que tous les éléments demandés ne rentrent pas dans le cache et que ces éléments sont demandés de manière circulaire, LRU échouera.

ps: La demande cyclique de données fera que LRU ne s'adaptera tout simplement pas à ce scénario.

Dans le cas de LRU, les éléments continueront d'entrer et de sortir du cache, et aucun utilisateur ne demandera d'accéder au cache.

Cependant, dans les mêmes conditions, les performances de l'algorithme LFU seront meilleures et la plupart des éléments du cache provoqueront des hits de cache.

Le comportement pathologique de l'algorithme LFU n'est pas impossible.

Nous ne présentons pas ici le cas du LFU, mais essayons de prouver que si le LFU est une stratégie applicable, il existe une meilleure méthode de mise en œuvre que la méthode publiée précédemment.

Opérations de dictionnaire prises en charge par le cache LFU

Lorsque nous parlons de l'algorithme d'éviction du cache, nous devons principalement effectuer 3 opérations différentes sur les données mises en cache.

  1. Définir (ou insérer) des éléments dans le cache

  2. Récupérer (ou trouver) des éléments dans le cache; en même temps augmenter leur nombre d'utilisation (pour LFU)

  3. Expulser (ou supprimer) du cache le moins utilisé (ou comme stratégie pour l'algorithme d'éviction)

La complexité actuelle la plus connue de l'algorithme LFU

Au moment de la rédaction de cet article, les environnements d'exécution les plus connus pour chacune des opérations ci-dessus pour la stratégie d'éviction du cache LFU sont les suivants:

Insérer: O (log n)

Recherche: O (log n)

Supprimer: O (log n)

Ces valeurs de complexité sont obtenues directement à partir de l'implémentation du tas binomial et de la table de hachage sans collision standard.

L'utilisation de la structure de données de tas minimum et du graphe de hachage peut facilement et efficacement implémenter la stratégie de mise en cache LFU.

Le tas minimum est créé en fonction du nombre d'utilisation (de l'élément) et la table de hachage est indexée par la clé de l'élément.

L'ordre de toutes les opérations sur la table de hachage sans collision est O (1), de sorte que le temps d'exécution du cache LFU est contrôlé par le temps d'exécution de l'opération sur le plus petit tas.

Lorsqu'un élément est inséré dans le cache, il entrera avec un compte d'utilisation de 1. Comme la surcharge d'insertion du tas minimum est O (log n), il faut O (log n) temps pour l'insérer dans le cache LFU.

Lors de la recherche d'un élément, l'élément peut être trouvé via une fonction de hachage, qui hache la clé de l'élément réel. Dans le même temps, le nombre d'utilisation (le nombre dans le plus grand tas) est augmenté de un, ce qui provoque la réorganisation du plus petit tas et l'élément est éloigné de la racine.

Puisque l'élément peut descendre au niveau log (n) à n'importe quel stade, cette opération prend également le temps O (log n).

Lorsqu'un élément est sélectionné pour être expulsé et finalement supprimé du tas, cela peut provoquer une réorganisation majeure de la structure de données du tas.

L'élément avec le plus petit nombre d'utilisation est à la racine du plus petit tas.

La suppression de la racine du plus petit tas implique de remplacer le nœud racine par le dernier nœud feuille du tas et de faire bouillonner le nœud à la bonne position.

La complexité d'exécution de cette opération est également O (log n).

Algorithme LFU proposé

Pour chaque opération de dictionnaire (insertion, recherche et suppression) qui peut être effectuée sur le cache LFU, la complexité d'exécution de l'algorithme LFU proposé est O (1).

Ceci est réalisé en conservant deux listes chaînées. L'un concerne la fréquence d'accès et l'autre tous les éléments ayant la même fréquence d'accès.

Les tables de hachage sont utilisées pour accéder aux éléments par clé (non illustré dans la figure ci-dessous pour plus de clarté).

Une liste à double lien est utilisée pour relier les nœuds qui représentent un groupe de nœuds avec la même fréquence d'accès (représentés par des blocs rectangulaires dans la figure ci-dessous).

Nous appelons cette double liste chaînée une liste de fréquences. Le groupe de nœuds avec la même fréquence d'accès est en fait une liste chaînée bidirectionnelle de ces nœuds (représentée sous forme de nœuds circulaires dans la figure ci-dessous).

Nous appelons cette liste chaînée bidirectionnelle (locale à une fréquence spécifique) une liste de nœuds.

Chaque nœud de la liste de nœuds a un pointeur vers son nœud parent.

Liste de fréquences (non représentée sur la figure pour plus de clarté). Donc le nœud x et vous aurez un pointeur vers le nœud 1, les nœuds z et a auront un pointeur vers le nœud 2, et ainsi de suite ...

Entrez la description de l'image

Le pseudo code ci-dessous montre comment initialiser le cache LFU.

La table de hachage utilisée pour localiser les éléments par clé est représentée par des variables clés.

Pour simplifier l'implémentation, nous utilisons SET au lieu de la liste chaînée pour stocker des éléments avec la même fréquence d'accès.

L'élément variable est une structure de données SET standard, qui contient les clés de ces éléments avec la même fréquence d'accès.

Sa complexité d'exécution d'insertion, de recherche et de suppression est O (1).

Entrez la description de l'image

Faux code

Voici quelques pseudo-codes, nous sommes nationaux.

Comprenez simplement son idée de base, passons au vrai code ci-dessous.

Ressentir

Le cœur de cet algorithme O (1) n'est en fait pas grand-chose, et il doit être considéré comme un problème de difficulté moyenne lorsqu'il est placé dans leetcode.

Cependant, il est étrange que ce papier ait été proposé en 2010, et on estime que O (logn) était la limite avant?

implémentation du code java

Attributs de base

public class CacheEvictLfu<K,V> extends AbstractCacheEvict<K,V> {

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

    /**
     * key 映射信息
     * @since 0.0.14
     */
    private final Map<K, FreqNode<K,V>> keyMap;

    /**
     * 频率 map
     * @since 0.0.14
     */
    private final Map<Integer, LinkedHashSet<FreqNode<K,V>>> freqMap;

    /**
     *
     * 最小频率
     * @since 0.0.14
     */
    private int minFreq;

    public CacheEvictLfu() {
        this.keyMap = new HashMap<>();
        this.freqMap = new HashMap<>();
        this.minFreq = 1;
    }

}

Définition du nœud

  • FreqNode.java
public class FreqNode<K,V> {

    /**
     * 键
     * @since 0.0.14
     */
    private K key;

    /**
     * 值
     * @since 0.0.14
     */
    private V value = null;

    /**
     * 频率
     * @since 0.0.14
     */
    private int frequency = 1;

    public FreqNode(K key) {
        this.key = key;
    }

    //fluent getter & setter
    // toString() equals() hashCode()
}

Supprimer l'élément

/**
 * 移除元素
 *
 * 1. 从 freqMap 中移除
 * 2. 从 keyMap 中移除
 * 3. 更新 minFreq 信息
 *
 * @param key 元素
 * @since 0.0.14
 */
@Override
public void removeKey(final K key) {
    FreqNode<K,V> freqNode = this.keyMap.remove(key);
    //1. 根据 key 获取频率
    int freq = freqNode.frequency();
    LinkedHashSet<FreqNode<K,V>> set = this.freqMap.get(freq);
    //2. 移除频率中对应的节点
    set.remove(freqNode);
    log.debug("freq={} 移除元素节点:{}", freq, freqNode);
    //3. 更新 minFreq
    if(CollectionUtil.isEmpty(set) && minFreq == freq) {
        minFreq--;
        log.debug("minFreq 降低为:{}", minFreq);
    }
}

Mettre à jour l'élément

/**
 * 更新元素,更新 minFreq 信息
 * @param key 元素
 * @since 0.0.14
 */
@Override
public void updateKey(final K key) {
    FreqNode<K,V> freqNode = keyMap.get(key);
    //1. 已经存在
    if(ObjectUtil.isNotNull(freqNode)) {
        //1.1 移除原始的节点信息
        int frequency = freqNode.frequency();
        LinkedHashSet<FreqNode<K,V>> oldSet = freqMap.get(frequency);
        oldSet.remove(freqNode);
        //1.2 更新最小数据频率
        if (minFreq == frequency && oldSet.isEmpty()) {
            minFreq++;
            log.debug("minFreq 增加为:{}", minFreq);
        }
        //1.3 更新频率信息
        frequency++;
        freqNode.frequency(frequency);
        //1.4 放入新的集合
        this.addToFreqMap(frequency, freqNode);
    } else {
        //2. 不存在
        //2.1 构建新的元素
        FreqNode<K,V> newNode = new FreqNode<>(key);
        //2.2 固定放入到频率为1的列表中
        this.addToFreqMap(1, newNode);
        //2.3 更新 minFreq 信息
        this.minFreq = 1;
        //2.4 添加到 keyMap
        this.keyMap.put(key, newNode);
    }
}

/**
 * 加入到频率 MAP
 * @param frequency 频率
 * @param freqNode 节点
 */
private void addToFreqMap(final int frequency, FreqNode<K,V> freqNode) {
    LinkedHashSet<FreqNode<K,V>> set = freqMap.get(frequency);
    if (set == null) {
        set = new LinkedHashSet<>();
    }
    set.add(freqNode);
    freqMap.put(frequency, set);
    log.debug("freq={} 添加元素节点:{}", frequency, freqNode);
}

Élimination des données

@Override
protected ICacheEntry<K, V> doEvict(ICacheEvictContext<K, V> context) {
    ICacheEntry<K, V> result = null;
    final ICache<K,V> cache = context.cache();
    // 超过限制,移除频次最低的元素
    if(cache.size() >= context.size()) {
        FreqNode<K,V> evictNode = this.getMinFreqNode();
        K evictKey = evictNode.key();
        V evictValue = cache.remove(evictKey);
        log.debug("淘汰最小频率信息, key: {}, value: {}, freq: {}",
                evictKey, evictValue, evictNode.frequency());
        result = new CacheEntry<>(evictKey, evictValue);
    }
    return result;
}

/**
 * 获取最小频率的节点
 *
 * @return 结果
 * @since 0.0.14
 */
private FreqNode<K, V> getMinFreqNode() {
    LinkedHashSet<FreqNode<K,V>> set = freqMap.get(minFreq);
    if(CollectionUtil.isNotEmpty(set)) {
        return set.iterator().next();
    }
    throw new CacheRuntimeException("未发现最小频率的 Key");
}

tester

Code

ICache<String, String> cache = CacheBs.<String,String>newInstance()
        .size(3)
        .evict(CacheEvicts.<String, String>lfu())
        .build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");
// 访问一次A
cache.get("A");
cache.put("D", "LRU");

Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());

Journal

[DEBUG] [2020-10-03 21:23:43.722] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=A, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.723] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=B, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.725] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=C, value=null, frequency=1}
[DEBUG] [2020-10-03 21:23:43.727] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=2 添加元素节点:FreqNode{key=A, value=null, frequency=2}
[DEBUG] [2020-10-03 21:23:43.728] [main] [c.g.h.c.c.s.e.CacheEvictLfu.doEvict] - 淘汰最小频率信息, key: B, value: world, freq: 1
[DEBUG] [2020-10-03 21:23:43.731] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[DEBUG] [2020-10-03 21:23:43.732] [main] [c.g.h.c.c.s.e.CacheEvictLfu.addToFreqMap] - freq=1 添加元素节点:FreqNode{key=D, value=null, frequency=1}
[D, A, C]

LFU vs LRU

la différence

LFU est un mode basé sur la fréquence d'accès, tandis que LRU est un mode basé sur le temps d'accès.

Avantage

Lorsque l'accès aux données est conforme à une distribution normale, le taux de réussite du cache de l'algorithme LFU est supérieur à celui de l'algorithme LRU.

Désavantage

  • La complexité du LFU est supérieure à celle du LRU.

  • La fréquence d'accès aux données doit être maintenue et chaque accès doit être mis à jour.

  • Les premières données sont plus faciles à mettre en cache que les données ultérieures, ce qui rend difficile la mise en cache des données ultérieures.

  • Les données nouvellement ajoutées au cache sont facilement supprimées, telles que le «jittering» à la fin du cache.

résumé

Cependant, dans la pratique réelle, les scénarios d'application de LFU ne sont pas aussi étendus.

Parce que les données réelles sont biaisées et que les données chaudes sont la norme, les performances de LRU sont généralement meilleures que celles de LFU.

Adresse open source:https://github.com/houbb/cache

Si vous pensez que cet article vous est utile, veuillez aimer, commenter, collecter et suivre une vague. Vos encouragements sont ma plus grande motivation ~

À l'heure actuelle, nous avons implémenté les algorithmes LRU et LFU avec d'excellentes performances, mais le système d'exploitation utilise effectivement ces deux algorithmes.Dans la section suivante, nous découvrirons l'algorithme d'élimination d'horloge privilégié par le système d'exploitation.

Je ne sais pas ce que vous avez gagné? Ou si vous avez plus d'idées, n'hésitez pas à discuter avec moi dans la zone de message et attendez avec impatience de rencontrer vos pensées.

L'apprentissage en profondeur

Je suppose que tu aimes

Origine blog.51cto.com/9250070/2540213
conseillé
Classement