Структура данных кэша объектов с «истечением срока действия объекта» - PullRequest
3 голосов
/ 08 января 2012

Какая структура данных в Java лучше всего подходит для реализации кэша объектов в памяти, где объекты имеют индивидуальный срок действия?

В основном для кеша я мог бы использовать Map (где ключом может быть String), который предлагает методы put и get, и использовать упорядоченный список пар "timestamp" + "object" для управления временем истечения. Таким образом, поток очистки может проверить первую запись списка и удалить объект по истечении времени его истечения. (Удаление первого элемента должно быть за O (1) раз)

Ответы [ 6 ]

10 голосов
/ 14 марта 2015

То, что вы описываете здание, в основном ExpiringMap .Существуют и другие подобные реализации, такие как Guava (см. CacheBuilder ) - хотя я не верю, что он поддерживает истечение срока действия для каждой записи, как ExpiringMap.

6 голосов
/ 08 января 2012

Фреймворки кеширования сейчас достаточно развиты:

Однако, если вы настаиваете назаново изобретая колесо, не забудьте учесть использование памяти.Слишком часто я вижу, что плохо реализованный кэш (HashMap) фактически превращается в утечку памяти.

См. Ответ Коуэна здесь: WeakHashMap Java и кеширование: почему он ссылается на ключи, а не на значения

4 голосов
/ 18 марта 2015

Guava Cachebuilder:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
       .maximumSize(10000)
       .expireAfterWrite(10, TimeUnit.MINUTES)
       .removalListener(MY_LISTENER)
       .build(
           new CacheLoader<Key, Graph>() {
             public Graph load(Key key) throws AnyException {
               return createExpensiveGraph(key);
             }
           });

Так как WeekHashmap не подходит для кэширования, но вы всегда можете использовать Map<K,WeakReference<V>>, значение которого становится подходящим для GC для ссылок на неделю.

Прежде всего, у нас всегда есть EhCache , Memcached и согласованность в качестве популярного выбора.

4 голосов
/ 08 января 2012

Я бы подумал об использовании существующей библиотеки, такой как ehcache.

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

Я бы использовал LinkedHashMap, если вам просто нужен кэш LRU.Однако, если вы хотите истечения времени, я бы использовал HashMap с PriorityQueue (чтобы вы могли проверить, истек ли срок действия следующей записи)

1 голос
/ 20 марта 2015

Как упоминалось в предыдущих ответах, лучше использовать один из популярных кэшей в памяти, таких как EhCache, Memcached и т.д.функция и меньшая сложность по времени, я пытался реализовать ее следующим образом - (любые отзывы / предложения тестов высоко ценится) ..

public class ObjectCache<K, V> {

    private volatile boolean shutdown;
    private final long maxObjects;
    private final long timeToLive;
    private final long removalThreadRunDelay;
    private final long objectsToRemovePerRemovalThreadRun;
    private final AtomicLong objectsCount;
    private final Map<K, CacheEntryWrapper> cachedDataStore;
    private final BlockingQueue<CacheEntryReference> queue;
    private final Object lock = new Object();
    private ScheduledExecutorService executorService;

    public ObjectCache(long maxObjects, long timeToLive, long removalThreadRunDelay, long objectsToRemovePerRemovalThreadRun) {
        this.maxObjects = maxObjects;
        this.timeToLive = timeToLive;
        this.removalThreadRunDelay = removalThreadRunDelay;
        this.objectsToRemovePerRemovalThreadRun = objectsToRemovePerRemovalThreadRun;
        this.objectsCount = new AtomicLong(0);
        this.cachedDataStore = new HashMap<K, CacheEntryWrapper>();
        this.queue = new LinkedBlockingQueue<CacheEntryReference>();
    }

    public void put(K key, V value) {
        if (key == null || value == null) {
            throw new IllegalArgumentException("Key and Value both should be not null");
        }
        if (objectsCount.get() + 1 > maxObjects) {
            throw new RuntimeException("Max objects limit reached. Can not store more objects in cache.");
        }
        // create a value wrapper and add it to data store map
        CacheEntryWrapper entryWrapper = new CacheEntryWrapper(key, value);
        synchronized (lock) {
            cachedDataStore.put(key, entryWrapper);
        }
        // add the cache entry reference to queue which will be used by removal thread
        queue.add(entryWrapper.getCacheEntryReference());
        objectsCount.incrementAndGet();
        // start the removal thread if not started already
        if (executorService == null) {
            synchronized (lock) {
                if (executorService == null) {
                    executorService = Executors.newSingleThreadScheduledExecutor();
                    executorService.scheduleWithFixedDelay(new CacheEntryRemover(), 0, removalThreadRunDelay, TimeUnit.MILLISECONDS);
                }
            }
        }
    }

    public V get(K key) {
        if (key == null) {
            throw new IllegalArgumentException("Key can not be null");
        }
        CacheEntryWrapper entryWrapper;
        synchronized (lock) {
            entryWrapper = cachedDataStore.get(key);
            if (entryWrapper != null) {
                // reset the last access time
                entryWrapper.resetLastAccessedTime();
                // reset the reference (so the weak reference is cleared)
                entryWrapper.resetCacheEntryReference();
                // add the new reference to queue
                queue.add(entryWrapper.getCacheEntryReference());
            }
        }
        return entryWrapper == null ? null : entryWrapper.getValue();
    }

    public void remove(K key) {
        if (key == null) {
            throw new IllegalArgumentException("Key can not be null");
        }
        CacheEntryWrapper entryWrapper;
        synchronized (lock) {
            entryWrapper = cachedDataStore.remove(key);
            if (entryWrapper != null) {
                // reset the reference (so the weak reference is cleared)
                entryWrapper.resetCacheEntryReference();
            }
        }
        objectsCount.decrementAndGet();
    }

    public void shutdown() {
        shutdown = true;
        executorService.shutdown();
        queue.clear();
        cachedDataStore.clear();
    }

    public static void main(String[] args) throws Exception {
        ObjectCache<Long, Long> cache = new ObjectCache<>(1000000, 60000, 1000, 1000);
        long i = 0;
        while (i++ < 10000) {
            cache.put(i, i);
        }
        i = 0;
        while(i++ < 100) {
            Thread.sleep(1000);
            System.out.println("Data store size: " + cache.cachedDataStore.size() + ", queue size: " + cache.queue.size());
        }
        cache.shutdown();
    }

    private class CacheEntryRemover implements Runnable {
        public void run() {
            if (!shutdown) {
                try {
                    int count = 0;
                    CacheEntryReference entryReference;
                    while ((entryReference = queue.peek()) != null && count++ < objectsToRemovePerRemovalThreadRun) {
                        long currentTime = System.currentTimeMillis();
                        CacheEntryWrapper cacheEntryWrapper = entryReference.getWeakReference().get();
                        if (cacheEntryWrapper == null || !cachedDataStore.containsKey(cacheEntryWrapper.getKey())) {
                            queue.poll(100, TimeUnit.MILLISECONDS); // remove the reference object from queue as value is removed from cache
                        } else if (currentTime - cacheEntryWrapper.getLastAccessedTime().get() > timeToLive) {
                            synchronized (lock) {
                                // get the cacheEntryWrapper again just to find if put() has overridden the same key or remove() has removed it already
                                CacheEntryWrapper newCacheEntryWrapper = cachedDataStore.get(cacheEntryWrapper.getKey());
                                // poll the queue if -
                                // case 1 - value is removed from cache
                                // case 2 - value is overridden by new value
                                // case 3 - value is still in cache but it is old now
                                if (newCacheEntryWrapper == null || newCacheEntryWrapper != cacheEntryWrapper || currentTime - cacheEntryWrapper.getLastAccessedTime().get() > timeToLive) {
                                    queue.poll(100, TimeUnit.MILLISECONDS);
                                    newCacheEntryWrapper = newCacheEntryWrapper == null ? cacheEntryWrapper : newCacheEntryWrapper;
                                    if (currentTime - newCacheEntryWrapper.getLastAccessedTime().get() > timeToLive) {
                                        remove(newCacheEntryWrapper.getKey());
                                    }
                                } else {
                                    break; // try next time
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private class CacheEntryWrapper {
        private K key;
        private V value;
        private AtomicLong lastAccessedTime;
        private CacheEntryReference cacheEntryReference;

        public CacheEntryWrapper(K key, V value) {
            this.key = key;
            this.value = value;
            this.lastAccessedTime = new AtomicLong(System.currentTimeMillis());
            this.cacheEntryReference = new CacheEntryReference(this);
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }

        public AtomicLong getLastAccessedTime() {
            return lastAccessedTime;
        }

        public CacheEntryReference getCacheEntryReference() {
            return cacheEntryReference;
        }

        public void resetLastAccessedTime() {
            lastAccessedTime.set(System.currentTimeMillis());
        }

        public void resetCacheEntryReference() {
            cacheEntryReference.clear();
            cacheEntryReference = new CacheEntryReference(this);
        }
    }

    private class CacheEntryReference {
        private WeakReference<CacheEntryWrapper> weakReference;

        public CacheEntryReference(CacheEntryWrapper entryWrapper) {
            this.weakReference = new WeakReference<CacheEntryWrapper>(entryWrapper);
        }

        public WeakReference<CacheEntryWrapper> getWeakReference() {
            return weakReference;
        }

        public void clear() {
            weakReference.clear();
        }
    }
}
1 голос
/ 08 января 2012

Я думаю, что ваше решение правильно.Я бы использовал HashMap, если быть точным.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...