Carte / cache basée sur le temps Java avec des clés qui expirent [fermé]

253

L'un d'entre vous connaît-il une carte Java ou un magasin de données standard similaire qui purge automatiquement les entrées après un délai donné? Cela signifie le vieillissement, où les anciennes entrées expirées «vieillissent» automatiquement.

De préférence dans une bibliothèque open source accessible via Maven?

Je connais des moyens d'implémenter la fonctionnalité moi-même et je l'ai fait plusieurs fois dans le passé, donc je ne demande pas de conseils à cet égard, mais des pointeurs vers une bonne implémentation de référence.

Les solutions basées sur WeakReference comme WeakHashMap ne sont pas une option, car mes clés sont probablement des chaînes non internes et je veux un délai configurable qui ne dépend pas du garbage collector.

Ehcache est également une option sur laquelle je ne voudrais pas compter car il a besoin de fichiers de configuration externes. Je recherche une solution uniquement codée.

Sean Patrick Floyd
la source
1
Découvrez Google Collections (maintenant appelé Guava). Il a une carte qui peut expirer automatiquement les entrées.
dty
3
Quelle bizarrerie qu'une question avec 253 votes positifs et 176k vues - qui se classe très haut dans les moteurs de recherche pour ce sujet - a été fermée car ne respecte pas les directives
Brian

Réponses:

320

Oui. Google Collections, ou Guava comme il est nommé, a maintenant quelque chose appelé MapMaker qui peut faire exactement cela.

ConcurrentMap<Key, Graph> graphs = new MapMaker()
   .concurrencyLevel(4)
   .softKeys()
   .weakValues()
   .maximumSize(10000)
   .expiration(10, TimeUnit.MINUTES)
   .makeComputingMap(
       new Function<Key, Graph>() {
         public Graph apply(Key key) {
           return createExpensiveGraph(key);
         }
       });

Mettre à jour:

À partir de la goyave 10.0 (publiée le 28 septembre 2011), beaucoup de ces méthodes MapMaker sont obsolètes au profit du nouveau CacheBuilder :

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(
        new CacheLoader<Key, Graph>() {
          public Graph load(Key key) throws AnyException {
            return createExpensiveGraph(key);
          }
        });
Shervin Asgari
la source
5
Génial, je savais que Guava avait une réponse mais je ne l'ai pas trouvée! (+1)
Sean Patrick Floyd
12
À partir de la version 10, vous devez plutôt utiliser CacheBuilder ( guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/… ) car l'expiration, etc. a été déconseillée dans MapMaker
wwadge
49
Attention ! L'utilisation weakKeys()implique que les clés sont comparées à l'aide de la sémantique ==, non equals(). J'ai perdu 30 minutes pour comprendre pourquoi mon cache à chaînes ne fonctionnait pas :)
Laurent Grégoire
3
Chers amis, ce dont @Laurent a parlé weakKeys()est important. weakKeys()n'est pas requis 90% du temps.
Manu Manjunath
3
@ShervinAsgari pour le bien des débutants (moi y compris), pourriez-vous changer votre exemple de goyave mis à jour pour un autre qui utilise Cache au lieu de LoadingCache? Cela correspondrait mieux à la question (puisque LoadingCache a des fonctionnalités qui dépassent une carte avec des entrées expirantes et est beaucoup plus compliquée à créer) voir github.com/google/guava/wiki/CachesExplained#from-a-callable
Jeutnarg
29

Il s'agit d'un exemple d'implémentation que j'ai fait pour la même exigence et la concurrence fonctionne bien. Pourrait être utile à quelqu'un.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 
 * @author Vivekananthan M
 *
 * @param <K>
 * @param <V>
 */
public class WeakConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {

    private static final long serialVersionUID = 1L;

    private Map<K, Long> timeMap = new ConcurrentHashMap<K, Long>();
    private long expiryInMillis = 1000;
    private static final SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss:SSS");

    public WeakConcurrentHashMap() {
        initialize();
    }

    public WeakConcurrentHashMap(long expiryInMillis) {
        this.expiryInMillis = expiryInMillis;
        initialize();
    }

    void initialize() {
        new CleanerThread().start();
    }

    @Override
    public V put(K key, V value) {
        Date date = new Date();
        timeMap.put(key, date.getTime());
        System.out.println("Inserting : " + sdf.format(date) + " : " + key + " : " + value);
        V returnVal = super.put(key, value);
        return returnVal;
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (K key : m.keySet()) {
            put(key, m.get(key));
        }
    }

    @Override
    public V putIfAbsent(K key, V value) {
        if (!containsKey(key))
            return put(key, value);
        else
            return get(key);
    }

    class CleanerThread extends Thread {
        @Override
        public void run() {
            System.out.println("Initiating Cleaner Thread..");
            while (true) {
                cleanMap();
                try {
                    Thread.sleep(expiryInMillis / 2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void cleanMap() {
            long currentTime = new Date().getTime();
            for (K key : timeMap.keySet()) {
                if (currentTime > (timeMap.get(key) + expiryInMillis)) {
                    V value = remove(key);
                    timeMap.remove(key);
                    System.out.println("Removing : " + sdf.format(new Date()) + " : " + key + " : " + value);
                }
            }
        }
    }
}


Git Repo Link (avec implémentation du récepteur)

https://github.com/vivekjustthink/WeakConcurrentHashMap

À votre santé!!

Vivek
la source
Pourquoi exécutez-vous la cleanMap()moitié du temps spécifié?
EliuX
Bcoz, il s'assure que les clés sont expirées (retirées) et évite au thread de boucler à l'extrême.
Vivek
@Vivek mais avec cette implémentation, il peut y avoir au maximum (expiryInMillis / 2) nombre d'entrées qui sont déjà expirées mais toujours présentes dans le cache. Comme le fil supprime les entrées après expirationInMillis / 2 period
rishi007bansod
19

Vous pouvez essayer mon implémentation d'une carte de hachage à expiration automatique. Cette implémentation n'utilise pas de threads pour supprimer les entrées expirées, mais utilise à la place DelayQueue qui est nettoyée à chaque opération.

pcan
la source
J'aime mieux la version de Guava, mais +1 pour ajouter de l'exhaustivité à l'image
Sean Patrick Floyd
@ piero86 Je dirais que l'appel à delayQueue.poll () dans la méthode expireKey (ExpiringKey <K> delayKey) est incorrect. Vous pouvez perdre une ExpiringKey arbitraire qui ne pourra plus être utilisée par la suite dans cleanup () - une fuite.
Stefan Zobel
1
Autre problème: vous ne pouvez pas mettre deux fois la même clé avec des durées de vie différentes. Après a) put (1, 1, shortLived), puis b) put (1, 2, longLived), l'entrée Map pour la clé 1 disparaîtra après shortLived ms, quelle que soit la durée de longLived.
Stefan Zobel
Merci pour votre perspicacité. Pourriez-vous signaler ces problèmes sous forme de commentaires dans l'essentiel, s'il vous plaît?
pcan
Fixé selon vos suggestions. Merci.
pcan
19

Apache Commons a un décorateur pour que les entrées de la carte expirent: PassiveExpiringMap C'est plus simple que les caches de Guava.

PS attention, ce n'est pas synchronisé.

Guram Savinov
la source
1
C'est simple, mais il ne vérifie le délai d'expiration qu'après avoir accédé à une entrée.
Badie
Selon le Javadoc : lors de l'appel de méthodes qui impliquent l'accès à l'intégralité du contenu de la carte (c'est-à-dire containsKey (Object), entrySet (), etc.), ce décorateur supprime toutes les entrées expirées avant de terminer l'appel.
NS du Toit
Si vous voulez voir quelle est la dernière version de cette bibliothèque (Apache commons commons-collections4), voici un lien vers la bibliothèque appropriée sur mvnrepository
NS du Toit
3

Cela ressemble à ehcache est exagéré pour ce que vous voulez, mais notez qu'il n'a pas besoin de fichiers de configuration externes.

C'est généralement une bonne idée de déplacer la configuration dans des fichiers de configuration déclaratifs (vous n'avez donc pas besoin de recompiler lorsqu'une nouvelle installation nécessite un délai d'expiration différent), mais ce n'est pas du tout requis, vous pouvez toujours le configurer par programme. http://www.ehcache.org/documentation/user-guide/configuration

dan carter
la source
2

Les collections Google (goyave) ont le MapMaker dans lequel vous pouvez définir un délai (pour l'expiration) et vous pouvez utiliser une référence douce ou faible lorsque vous choisissez en utilisant une méthode d'usine pour créer des instances de votre choix.

Emil
la source
2

Si quelqu'un a besoin d'une chose simple, voici un simple jeu d'expiration de clé. Il peut être facilement converti en carte.

public class CacheSet<K> {
    public static final int TIME_OUT = 86400 * 1000;

    LinkedHashMap<K, Hit> linkedHashMap = new LinkedHashMap<K, Hit>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, Hit> eldest) {
            final long time = System.currentTimeMillis();
            if( time - eldest.getValue().time > TIME_OUT) {
                Iterator<Hit> i = values().iterator();

                i.next();
                do {
                    i.remove();
                } while( i.hasNext() && time - i.next().time > TIME_OUT );
            }
            return false;
        }
    };


    public boolean putIfNotExists(K key) {
        Hit value = linkedHashMap.get(key);
        if( value != null ) {
            return false;
        }

        linkedHashMap.put(key, new Hit());
        return true;
    }

    private static class Hit {
        final long time;


        Hit() {
            this.time = System.currentTimeMillis();
        }
    }
}
palindrom
la source
2
C'est très bien pour une situation à un seul thread, mais cela se briserait misérablement dans une situation simultanée.
Sean Patrick Floyd
@SeanPatrickFloyd tu veux dire comme LinkedHashMap lui-même?! "il doit être synchronisé en externe" tout comme LinkedHashMap, HashMap ... vous l'appelez.
palindrom
oui, comme tous ceux-là, mais contrairement à la cache de Guava (la réponse acceptée)
Sean Patrick Floyd
En outre, pensez à utiliser System.nanoTime()pour calculer les différences de temps car System.currentTimeMillis () n'est pas cohérent car il dépend de l'heure du système et peut ne pas être continu.
Ercksen
2

En règle générale, un cache doit conserver les objets quelque temps et doit en exposer un peu plus tard. Le moment idéal pour tenir un objet dépend du cas d'utilisation. Je voulais que cette chose soit simple, sans fils ni ordonnanceurs. Cette approche fonctionne pour moi. Contrairement à SoftReferences, les objets sont garantis disponibles pendant un minimum de temps. Cependant, ils ne restent pas en mémoire jusqu'à ce que le soleil se transforme en une géante rouge .

Comme exemple d'utilisation, pensez à un système qui répond lentement et qui pourra vérifier si une demande a été effectuée assez récemment, et dans ce cas ne pas effectuer l'action demandée deux fois, même si un utilisateur agité appuie plusieurs fois sur le bouton. Mais, si la même action est demandée quelque temps plus tard, elle doit être exécutée à nouveau.

class Cache<T> {
    long avg, count, created, max, min;
    Map<T, Long> map = new HashMap<T, Long>();

    /**
     * @param min   minimal time [ns] to hold an object
     * @param max   maximal time [ns] to hold an object
     */
    Cache(long min, long max) {
        created = System.nanoTime();
        this.min = min;
        this.max = max;
        avg = (min + max) / 2;
    }

    boolean add(T e) {
        boolean result = map.put(e, Long.valueOf(System.nanoTime())) != null;
        onAccess();
        return result;
    }

    boolean contains(Object o) {
        boolean result = map.containsKey(o);
        onAccess();
        return result;
    }

    private void onAccess() {
        count++;
        long now = System.nanoTime();
        for (Iterator<Entry<T, Long>> it = map.entrySet().iterator(); it.hasNext();) {
            long t = it.next().getValue();
            if (now > t + min && (now > t + max || now + (now - created) / count > t + avg)) {
                it.remove();
            }
        }
    }
}
Matthias Ronge
la source
agréable, merci
bigbadmouse
1
HashMap n'est pas sûr pour les threads, en raison des conditions de concurrence, le fonctionnement de map.put ou le redimensionnement de la carte peut entraîner une corruption des données. Voir ici: mailinator.blogspot.com/2009/06/beautiful-race-condition.html
Eugene Maysyuk
C'est vrai. En effet, la plupart des classes Java ne sont pas thread-safe. Si vous avez besoin de la sécurité des threads, vous devez vérifier chaque classe affectée de votre conception pour voir si elle répond aux exigences.
Matthias Ronge
1

Le cache de goyave est facile à mettre en œuvre. Nous pouvons expirer la clé sur une base de temps en utilisant le cache de goyave. J'ai lu entièrement le post et ci-dessous donne la clé de mon étude.

cache = CacheBuilder.newBuilder().refreshAfterWrite(2,TimeUnit.SECONDS).
              build(new CacheLoader<String, String>(){
                @Override
                public String load(String arg0) throws Exception {
                    // TODO Auto-generated method stub
                    return addcache(arg0);
                }

              }

Référence: exemple de cache de goyave

Anuj Dhiman
la source
1
veuillez mettre à jour le lien car il ne fonctionne pas maintenant
smaiakov