Java-тайм-карта / кеш с истекающими ключами [закрыто]

253

Знает ли кто-нибудь из вас о карте Java или аналогичном стандартном хранилище данных, которое автоматически удаляет записи после заданного времени ожидания? Это означает старение, когда старые просроченные записи автоматически «устаревают».

Желательно в библиотеке с открытым исходным кодом, которая доступна через Maven?

Я сам знаю способы реализации функциональности и делал это несколько раз в прошлом, так что я не прошу совета в этом отношении, но обращаюсь к указателям на хорошую справочную реализацию.

Решения на основе WeakReference, такие как WeakHashMap , не подходят, потому что мои ключи, скорее всего, не являются интернированными строками, и я хочу настраиваемое время ожидания, которое не зависит от сборщика мусора.

Ehcache также вариант, на который я не хотел бы полагаться, потому что ему нужны внешние файлы конфигурации. Я ищу решение только для кода.

Шон Патрик Флойд
источник
1
Проверьте Google Коллекции (теперь называется Guava). У него есть карта, которая может тайм-аут записи автоматически.
DTY
3
Как странно, что вопрос с 253 ответами и 176 тысячами просмотров - который занимает очень высокое место в поисковых системах по этой теме - был закрыт, так как не соответствует рекомендациям
Брайан,

Ответы:

320

Да. В Google Collections, или Guava, как его теперь называют, есть что-то под названием MapMaker, которое может сделать именно это.

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

Обновить:

Начиная с версии 10.0 (выпущена 28 сентября 2011 г.), многие из этих методов MapMaker устарели в пользу нового 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
Круто, я знал, что у Гуавы есть ответ, но я не смог его найти! (+1)
Шон Патрик Флойд
12
Начиная с версии 10, вместо этого вы должны использовать CacheBuilder ( guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/… ), так как срок действия и т.д. устарел в MapMaker
wwadge
49
Предупреждение ! Использование weakKeys()подразумевает, что ключи сравниваются с использованием семантики ==, а не equals(). Я потерял 30 минут, чтобы выяснить, почему мой кеш со строковым ключом не работал :)
Лоран Грегуар
3
Люди, вещь, о которой упоминал @Laurent weakKeys(), важна. weakKeys()не требуется 90% времени.
Ману Манджунат
3
@ShervinAsgari ради начинающих (включая меня), не могли бы вы переключить свой обновленный пример гуавы на тот, который использует Cache вместо LoadingCache? Это соответствовало бы вопрос лучше (поскольку LoadingCache имеет функции , которые превышают карту с истекающим записи и гораздо более сложно создать) см github.com/google/guava/wiki/CachesExplained#from-a-callable
Jeutnarg
29

Это пример реализации, который я сделал для того же требования, и параллелизм работает хорошо. Может быть полезно для кого-то.

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 (с реализацией слушателя)

https://github.com/vivekjustthink/WeakConcurrentHashMap

Ура !!

Вивек
источник
Почему вы выполняете cleanMap()половину указанного времени?
EliuX
Bcoz это гарантирует, что ключи истекли (удалены) и избегает резьбы от экстремального зацикливания.
Вивек
@Vivek, но с этой реализацией может быть максимально (expiryInMillis / 2) количество записей, которые уже просрочены, но все еще присутствуют в кэше. Как поток удаляет записи после истечения срока действияInMillis / 2
rishi007bansod
19

Вы можете попробовать мою реализацию самодействующей хэш-карты. Эта реализация не использует потоки для удаления записей с истекшим сроком, вместо этого она использует DelayQueue, который очищается при каждой операции автоматически.

PCAN
источник
Мне больше нравится версия Guava, но +1 за добавление полноты к картине
Шон Патрик Флойд
@ piero86 Я бы сказал, что вызов delayQueue.poll () в методе expireKey (ExpiringKey <K> delayedKey) неправильный. Вы можете потерять произвольный ExpiringKey, который позже не может быть использован в cleanup () - утечка.
Стефан Зобель
1
Еще одна проблема: вы не можете поставить один и тот же ключ дважды с разным временем жизни. После a) put (1, 1, shortLived), затем b) put (1, 2, longLived) запись Map для ключа 1 будет удалена после shortLived ms независимо от того, как долго будет longLived.
Стефан Зобель
Спасибо за ваше понимание. Не могли бы вы сообщить об этих проблемах в виде комментариев в суть, пожалуйста?
pcan
Исправлено в соответствии с вашими предложениями. Спасибо.
pcan
19

В Apache Commons есть декоратор для Map, срок действия которого истекает: PassiveExpiringMap Это проще, чем кеши из Guava.

PS будьте осторожны, это не синхронизировано.

Гурам Савинов
источник
1
Это просто, но он проверяет время истечения только после того, как вы получите доступ к записи.
Badie
Согласно Javadoc : при вызове методов, которые включают в себя доступ ко всему содержимому карты (т. Е. Содержит ключ (объект), запись () и т. Д.), Этот декоратор удаляет все записи с истекшим сроком до фактического завершения вызова.
NS du Toit
Если вы хотите узнать, какая последняя версия этой библиотеки (Apache commons commons-collection4), вот ссылка на соответствующую библиотеку в mvnrepository
NS du Toit
3

Похоже, что ehcache является излишним для того, что вы хотите, однако обратите внимание, что ему не нужны внешние файлы конфигурации.

Как правило, рекомендуется перенести конфигурацию в декларативные файлы конфигурации (поэтому вам не нужно перекомпилировать, когда для новой установки требуется другое время истечения), но это совсем не требуется, вы все равно можете настроить ее программно. http://www.ehcache.org/documentation/user-guide/configuration

Дэн Картер
источник
2

В коллекциях Google (гуава) есть MapMaker, в котором вы можете установить ограничение по времени (для истечения срока действия), и вы можете использовать мягкую или слабую ссылку по своему усмотрению, используя фабричный метод для создания экземпляров по вашему выбору.

Эмиль
источник
2

Если кому-то нужна простая вещь, ниже приведен простой набор ключей, срок действия которых истекает. Это может быть легко преобразовано в карту.

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
источник
2
Это хорошо для ситуации с одним потоком, но это может с треском сломаться в параллельной ситуации.
Шон Патрик Флойд,
@SeanPatrickFloyd ты имеешь в виду как сам LinkedHashMap ?! «оно должно быть синхронизировано извне», как LinkedHashMap, HashMap ... вы называете это.
палиндром
да, как и все те, но в отличие от кэша Гуавы (принятый ответ)
Шон Патрик Флойд
Кроме того, рассмотрите возможность использования System.nanoTime()для вычисления разницы во времени, поскольку System.currentTimeMillis () не согласован, так как он зависит от системного времени и может быть не непрерывным.
Эрксен
2

Как правило, кэш должен хранить объекты в течение некоторого времени и раскрывать их через некоторое время. Какое хорошее время держать объект, зависит от варианта использования. Я хотел, чтобы эта вещь была простой, без потоков или планировщиков. Этот подход работает для меня. В отличие от SoftReferences, объекты гарантированно будут доступны какое-то минимальное количество времени. Однако они не остаются в памяти, пока солнце не превратится в красного гиганта .

В качестве примера использования рассмотрим медленно реагирующую систему, которая должна быть в состоянии проверить, был ли выполнен запрос совсем недавно, и в этом случае не выполнять запрошенное действие дважды, даже если активный пользователь нажимает кнопку несколько раз. Но, если то же действие запрашивается через некоторое время, оно должно быть выполнено снова.

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();
            }
        }
    }
}
Матиас Ронге
источник
хорошо, спасибо
bigbadmouse
1
HashMap не является потокобезопасным, из-за условий гонки, операция map.put или изменение размера карты может привести к повреждению данных. Смотрите здесь: mailinator.blogspot.com/2009/06/beautiful-race-condition.html
Евгений Майсюк
Это правда. Действительно, большинство классов Java не являются поточно-ориентированными. Если вам нужна защита потоков, вам нужно проверить каждый затронутый класс вашего проекта, чтобы убедиться, что он соответствует требованию.
Матиас Ронге
1

Кеш Guava прост в реализации. Мы можем просрочить ключ на временной основе, используя кеш Guava. Я прочитал полностью пост и ниже дает ключ моего исследования.

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

              }

Ссылка: пример кэша гуавы

Анудж Дхиман
источник
1
пожалуйста, обновите ссылку, так как она не работает сейчас
smaiakov