Mapa / caché basado en el tiempo de Java con claves que caducan [cerrado]

253

¿Alguno de ustedes sabe de un Java Map o un almacén de datos estándar similar que purga automáticamente las entradas después de un tiempo de espera determinado? Esto significa envejecimiento, donde las entradas caducadas antiguas "caducan" automáticamente.

¿Preferiblemente en una biblioteca de código abierto que sea accesible a través de Maven?

Sé de maneras de implementar la funcionalidad yo mismo y lo he hecho varias veces en el pasado, por lo que no estoy pidiendo consejos al respecto, sino sugerencias para una buena implementación de referencia.

Las soluciones basadas en WeakReference como WeakHashMap no son una opción, porque es probable que mis claves sean cadenas no internadas y quiero un tiempo de espera configurable que no dependa del recolector de basura.

Ehcache también es una opción en la que no me gustaría confiar porque necesita archivos de configuración externos. Estoy buscando una solución de solo código.

Sean Patrick Floyd
fuente
1
Echa un vistazo a Google Collections (ahora llamado Guava). Tiene un mapa que puede agotar las entradas automáticamente.
dty
3
Qué extraño que se haya cerrado una pregunta con 253 votos a favor y 176k vistas, que ocupa un lugar súper alto en los motores de búsqueda para este tema, ya que no cumple con las pautas
Brian

Respuestas:

320

Si. Google Collections, o Guava como se llama ahora, tiene algo llamado MapMaker que puede hacer exactamente eso.

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

Actualizar:

A partir de la guayaba 10.0 (lanzada el 28 de septiembre de 2011), muchos de estos métodos de MapMaker han quedado en desuso a favor del nuevo 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
fuente
55
Increíble, sabía que Guava tenía una respuesta, ¡pero no pude encontrarla! (+1)
Sean Patrick Floyd
12
A partir de la v10, debería usar CacheBuilder en su lugar ( guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/… ) ya que la caducidad, etc., ha quedado en desuso en MapMaker
wwadge
49
Advertencia ! El uso weakKeys()implica que las claves se comparan utilizando la semántica ==, no equals(). Perdí 30 minutos descubriendo por qué mi caché con cadenas no funcionaba :)
Laurent Grégoire
3
Amigos, lo que mencionó @Laurent weakKeys()es importante. weakKeys()No se requiere el 90% del tiempo.
Manu Manjunath
3
@ShervinAsgari por el bien de los principiantes (incluido yo mismo), ¿podría cambiar su ejemplo actualizado de guayaba a uno que use Cache en lugar de LoadingCache? Encajaría mejor con la pregunta (ya que LoadingCache tiene características que exceden un mapa con entradas que caducan y es mucho más complicado de crear), consulte github.com/google/guava/wiki/CachesExplained#from-a-callable
Jeutnarg el
29

Esta es una implementación de muestra que hice para el mismo requisito y la concurrencia funciona bien. Puede ser útil para alguien.

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


Enlace Git Repo (con implementación de escucha)

https://github.com/vivekjustthink/WeakConcurrentHashMap

¡¡Salud!!

Vivek
fuente
¿Por qué ejecutas la cleanMap()mitad del tiempo especificado?
EliuX
Bcoz se asegura de que las claves hayan caducado (eliminado) y evita que el hilo se repita.
Vivek
@Vivek, pero con esta implementación puede haber un número máximo (expiryInMillis / 2) de entradas que ya caducaron pero todavía están presentes en la memoria caché. A medida que el hilo elimina las entradas después del período de
expiración
19

Puede probar mi implementación de un mapa hash que caduca por sí mismo. Esta implementación no utiliza subprocesos para eliminar entradas caducadas, sino que utiliza DelayQueue que se limpia automáticamente en cada operación.

pcan
fuente
Me gusta más la versión de Guava, pero +1 por agregar integridad a la imagen
Sean Patrick Floyd
@ piero86 Diría que la llamada a delayQueue.poll () en el método expireKey (ExpiringKey <K> delayedKey) es incorrecta. Puede perder una ExpiringKey arbitraria que luego no se puede utilizar en cleanup (), una fuga.
Stefan Zobel
1
Otro problema: no puede poner la misma clave dos veces con vidas diferentes. Después de a) put (1, 1, shortLived), luego b) put (1, 2, longLived) la entrada del Mapa para la clave 1 desaparecerá después de shortLived ms, sin importar cuánto tiempo sea LongLived.
Stefan Zobel
Gracias por tu perspicacia. ¿Podría informar estos problemas como comentarios en la esencia, por favor?
pcan
Solucionado de acuerdo a sus sugerencias. Gracias.
pcan
19

Apache Commons tiene un decorador para que el Mapa expire las entradas: PassiveExpiringMap Es más simple que los cachés de Guava.

PD: ten cuidado, no está sincronizado.

Guram Savinov
fuente
1
Es simple, pero verifica el tiempo de vencimiento solo después de acceder a una entrada.
Badie
Según el Javadoc : al invocar métodos que implican acceder a todo el contenido del mapa (es decir, contieneKey (Object), entrySet (), etc.) este decorador elimina todas las entradas caducadas antes de completar la invocación.
NS du Toit
Si desea ver cuál es la última versión de esta biblioteca (Apache commons commons-collections4) aquí hay un enlace a la biblioteca relevante en mvnrepository
NS du Toit
3

Parece que ehcache es excesivo para lo que desea, sin embargo, tenga en cuenta que no necesita archivos de configuración externos.

En general, es una buena idea mover la configuración a un archivo de configuración declarativo (por lo que no es necesario volver a compilar cuando una nueva instalación requiere un tiempo de vencimiento diferente), pero no es necesario, aún puede configurarlo mediante programación. http://www.ehcache.org/documentation/user-guide/configuration

dan carter
fuente
2

Las colecciones de Google (guayaba) tienen el MapMaker en el que puede establecer un límite de tiempo (para la caducidad) y puede usar referencias suaves o débiles cuando elija utilizando un método de fábrica para crear instancias de su elección.

Emil
fuente
2

Si alguien necesita algo simple, lo siguiente es un conjunto simple de expiración de clave. Podría convertirse fácilmente en un mapa.

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
fuente
2
Esto está bien para una situación de un solo hilo, pero se rompería miserablemente en una situación concurrente.
Sean Patrick Floyd
@SeanPatrickFloyd te refieres a LinkedHashMap en sí mismo ?! "debe estar sincronizado externamente" al igual que LinkedHashMap, HashMap ... lo que sea.
palindrom
sí, como todos esos, pero a diferencia del caché de Guava (la respuesta aceptada)
Sean Patrick Floyd
Además, considere usar System.nanoTime()para calcular las diferencias de tiempo, ya que System.currentTimeMillis () no es coherente, ya que depende de la hora del sistema y podría no ser continuo.
Ercksen
2

Por lo general, un caché debe mantener los objetos en algún momento y exponerlos algún tiempo después. El momento adecuado para sostener un objeto depende del caso de uso. Quería que esto fuera simple, sin hilos ni programadores. Este enfoque funciona para mí. A diferencia de SoftReferences, se garantiza que los objetos estarán disponibles durante un tiempo mínimo. Sin embargo, no se quedan en la memoria hasta que el sol se convierta en un gigante rojo .

Como ejemplo de uso, piense en un sistema de respuesta lenta que será capaz de verificar si una solicitud se ha realizado recientemente y, en ese caso, no realizar la acción solicitada dos veces, incluso si un usuario agitado presiona el botón varias veces. Pero, si se solicita la misma acción algún tiempo después, se realizará nuevamente.

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
fuente
bonito, gracias
bigbadmouse
1
HashMap no es seguro para subprocesos, debido a las condiciones de carrera, la operación de map.put o el cambio de tamaño del mapa pueden conducir a la corrupción de datos. Ver aquí: mailinator.blogspot.com/2009/06/beautiful-race-condition.html
Eugene Maysyuk
Es verdad. De hecho, la mayoría de las clases de Java no son seguras para subprocesos. Si necesita seguridad de subprocesos, debe verificar cada clase afectada de su diseño para ver si cumple con el requisito.
Matthias Ronge
1

La caché de guayaba es fácil de implementar. Podemos expirar la clave en base al tiempo usando la caché de guayaba. He leído completamente la publicación y a continuación da la clave de mi estudio.

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

              }

Referencia: ejemplo de caché de guayaba

Anuj Dhiman
fuente
1
por favor actualice el enlace ya que no funciona ahora
smaiakov