Mappa / cache Java basata sul tempo con chiavi in ​​scadenza [chiuso]


253

Qualcuno di voi conosce una Java Map o un archivio di dati standard simile che elimina automaticamente le voci dopo un determinato timeout? Questo significa invecchiamento, in cui le vecchie voci scadute "invecchiano" automaticamente.

Preferibilmente in una libreria open source accessibile tramite Maven?

Conosco i modi per implementare la funzionalità da solo e l'ho fatto più volte in passato, quindi non sto chiedendo consigli a tale riguardo, ma suggerimenti per una buona implementazione di riferimento.

Le soluzioni basate su WeakReference come WeakHashMap non sono un'opzione, perché è probabile che le mie chiavi siano stringhe non internate e voglio un timeout configurabile che non dipenda dal Garbage Collector.

Ehcache è anche un'opzione su cui non vorrei fare affidamento perché necessita di file di configurazione esterni. Sto cercando una soluzione solo per il codice.


1
Dai un'occhiata a Google Collections (ora chiamato Guava). Ha una mappa che può timeout automaticamente le voci.
dty

3
Com'è strano che una domanda con 253 voti positivi e 176.000 visualizzazioni - che si colloca molto in alto nei motori di ricerca per questo argomento - sia stata chiusa in quanto non conforme alle linee guida
Brian

Risposte:


320

Sì. Google Collections o Guava come viene ora chiamato ha qualcosa chiamato MapMaker che può fare esattamente questo.

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);
         }
       });

Aggiornare:

A partire da guava 10.0 (rilasciato il 28 settembre 2011) molti di questi metodi MapMaker sono stati deprecati a favore del nuovo 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);
          }
        });

5
Fantastico, sapevo che Guava aveva una risposta ma non riuscivo a trovarla! (+1)
Sean Patrick Floyd,

12
A partire dalla v10, dovresti invece utilizzare CacheBuilder ( guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/… ) poiché la scadenza ecc. È stata deprecata in MapMaker
wwadge,

49
Attenzione ! L'uso weakKeys()implica che le chiavi vengano confrontate usando la semantica ==, no equals(). Ho perso 30 minuti per capire perché la mia cache con stringhe non funzionava :)
Laurent Grégoire,

3
Gente, la cosa di cui parla @Laurent weakKeys()è importante. weakKeys()non è richiesto il 90% delle volte.
Manu Manjunath,

3
@ShervinAsgari per il bene dei principianti (me compreso), potresti passare il tuo esempio di guava aggiornato a uno che utilizza Cache invece di LoadingCache? Si abbinerebbe meglio alla domanda (poiché LoadingCache ha funzionalità che superano una mappa con voci in scadenza ed è molto più complicato da creare) vedi github.com/google/guava/wiki/CachesExplained#from-a-callable
Jeutnarg

29

Questa è un'implementazione di esempio che ho fatto per lo stesso requisito e la concorrenza funziona bene. Potrebbe essere utile per qualcuno.

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 (con implementazione listener)

https://github.com/vivekjustthink/WeakConcurrentHashMap

Saluti!!


Perché esegui cleanMap()metà del tempo previsto?
EliuX il

Bcoz si assicura che le chiavi siano scadute (rimosse) ed evita che il thread si estenda.
Vivek,

@Vivek ma con questa implementazione ci può essere al massimo (expiryInMillis / 2) numero di voci che sono già scadute ma ancora presenti nella cache. Poiché il thread elimina le voci dopo expiryInMillis / 2 period
rishi007bansod

19

Puoi provare la mia implementazione di una mappa hash a scadenza automatica. Questa implementazione non utilizza i thread per rimuovere le voci scadute, ma utilizza DelayQueue che viene ripulito automaticamente ad ogni operazione.


Mi piace la versione di Guava meglio, ma +1 per aggiungere completezza all'immagine
Sean Patrick Floyd,

@ piero86 Direi che la chiamata a delayQueue.poll () nel metodo expireKey (ExpiringKey <K> delayedKey) è errata. Potresti perdere un ExpiringKey arbitrario che non può essere successivamente utilizzato in cleanup () - una perdita.
Stefan Zobel,

1
Un altro problema: non è possibile inserire la stessa chiave due volte con vite diverse. Dopo a) put (1, 1, shortLived), quindi b) put (1, 2, longLived) la voce Mappa per la chiave 1 verrà cancellata dopo shortLived ms, indipendentemente dalla durata di longLived.
Stefan Zobel,

Grazie per la tua comprensione. Potresti segnalare questi problemi come commenti in sintesi, per favore?
pcan,

Risolto secondo i tuoi suggerimenti. Grazie.
pcan,

19

Apache Commons ha un decoratore per Map per far scadere le voci: PassiveExpiringMap È più semplice delle cache di Guava.

PS stai attento, non è sincronizzato.


1
È semplice, ma controlla il tempo di scadenza solo dopo aver effettuato l'accesso a una voce.
Badie,

Come da Javadoc : quando si invocano metodi che prevedono l'accesso all'intero contenuto della mappa (cioè contiene Key (Object), entrySet (), ecc.) Questo decoratore rimuove tutte le voci scadute prima di completare effettivamente l'invocazione.
NS du Toit,

Se vuoi vedere qual è l'ultima versione di questa libreria (Apache commons commons-collections4) ecco un link alla libreria pertinente su mvnrepository
NS du Toit

3

Sembra che ehcache sia eccessivo per quello che vuoi, tuttavia nota che non ha bisogno di file di configurazione esterni.

In genere è una buona idea spostare la configurazione in file di configurazione dichiarativi (quindi non è necessario ricompilare quando una nuova installazione richiede un tempo di scadenza diverso), ma non è affatto necessaria, è comunque possibile configurarla a livello di programmazione. http://www.ehcache.org/documentation/user-guide/configuration


2

Le raccolte Google (guava) hanno MapMaker in cui è possibile impostare un limite di tempo (per la scadenza) e puoi utilizzare riferimenti deboli o deboli mentre scegli utilizzando un metodo di fabbrica per creare istanze di tua scelta.



2

Se qualcuno ha bisogno di una cosa semplice, di seguito è riportato un semplice set con scadenza chiave. Potrebbe essere facilmente convertito in una mappa.

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();
        }
    }
}

2
Questo va bene per una situazione a thread singolo, ma si romperà miseramente in una situazione concorrente.
Sean Patrick Floyd,

@SeanPatrickFloyd intendi come lo stesso LinkedHashMap ?! "deve essere sincronizzato esternamente" proprio come LinkedHashMap, HashMap ... lo chiami.
palindrom,

sì, come tutti quelli, ma a differenza della cache di Guava (la risposta accettata)
Sean Patrick Floyd

Inoltre, considerare l'utilizzo System.nanoTime()per calcolare le differenze di tempo in quanto System.currentTimeMillis () non è coerente in quanto dipende dall'ora del sistema e potrebbe non essere continuo.
Ercksen,

2

In genere, una cache dovrebbe conservare gli oggetti per un po 'di tempo e li esporrà più tardi. Qual è un buon momento per contenere un oggetto dipende dal caso d'uso. Volevo che questa cosa fosse semplice, senza thread o programmatori. Questo approccio funziona per me. A differenza di SoftReferences, gli oggetti sono garantiti per un certo periodo di tempo. Tuttavia, non rimanere in memoria finché il sole non si trasforma in un gigante rosso .

Come esempio di utilizzo pensiamo a un sistema a risposta lenta che deve essere in grado di verificare se una richiesta è stata fatta abbastanza di recente, e in tal caso non eseguire l'azione richiesta due volte, anche se un utente frenetico preme più volte il pulsante. Ma, se la stessa azione viene richiesta qualche tempo dopo, deve essere eseguita di nuovo.

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();
            }
        }
    }
}

bello, grazie
bigbadmouse il

1
HashMap non è sicuro per i thread, a causa delle condizioni di competizione, il funzionamento di map.put o il ridimensionamento della mappa possono causare il danneggiamento dei dati. Vedi qui: mailinator.blogspot.com/2009/06/beautiful-race-condition.html
Eugene Maysyuk

È vero. In effetti, la maggior parte delle classi Java non sono thread-safe. Se hai bisogno della sicurezza del thread, devi controllare ogni classe interessata del tuo progetto per vedere se soddisfa i requisiti.
Matthias Ronge,

1

La cache di Guava è facile da implementare. Possiamo scadere le chiavi sulla base dei tempi usando la cache di Guava. Ho letto completamente post e sotto fornisce la chiave del mio studio.

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);
                }

              }

Riferimento: esempio di cache guava


1
si prega di aggiornare il collegamento in quanto non funziona ora
smaiakov
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.