【问题标题】:Object cache data structure with "object expiration"带有“对象过期”的对象缓存数据结构
【发布时间】:2012-02-05 09:15:55
【问题描述】:

Java 中哪种数据结构最适合实现内存中对象缓存,其中对象具有单独的过期时间?

基本上,对于缓存,我可以使用提供 put 和 get 方法的 Map(其中键可以是 String),并使用“时间戳”+“对象”对的有序列表来管理过期时间。因此,清理线程可以检查第一个列表条目并在其过期时间过后删除该对象。 (删除第一个元素应该在 O(1) 时间内)

【问题讨论】:

    标签: java caching data-structures collections


    【解决方案1】:

    您所描述的建筑基本上是ExpiringMap。还有其他类似的实现,例如 Guava(请参阅 CacheBuilder)——尽管我不相信它像 ExpiringMap 那样支持按条目过期。

    【讨论】:

    • +1 表示 Guava 的 CacheBuilder。我认为这是最合适的建议,因为它易于使用且重量轻。
    • 赞成,因为 OP 不是要求缓存服务器场,而是要求内存中的缓存结构。 Guava Cache 最适合这里。
    • 如何使用番石榴为每个对象设置过期时间?
    • @sodik 我认为 Guava 不支持按条目过期。 ExpiringMap 确实如此。更新了答案以反映这一点。
    【解决方案2】:

    缓存框架现在已经相当成熟了:

    但是,如果您坚持重新发明轮子,请记住考虑内存使用情况。我经常看到一个执行不当的缓存 (HashMap) 实际上变成了内存泄漏。

    在这里查看考恩的回答:Java's WeakHashMap and caching: Why is it referencing the keys, not the values?

    【讨论】:

      【解决方案3】:

      我会考虑使用现有的库,例如 ehcache

      但是,如果您想自己编写,除非您需要,否则我不会使用后台线程,因为它会增加复杂性。相反,我会让前台线程删除过期的条目。

      如果您只需要 LRU 缓存,我会使用 LinkedHashMap。但是,如果您想要定时到期,我会使用 HashMapPriorityQueue(这样您就可以检查下一个到期条目是否已过期)

      【讨论】:

      • 确实,LinkedHashMap 是一个很好的选择。但是要澄清一件事:要删除过期的条目,您可以将LinkedHashMap 与一些执行此工作的线程结合起来,对吗?如果是,你不认为它会稍微降低这种缓存的性能吗?其次:为什么是前台线程而不是后台线程?
      • @GrzesiekD。您可以在访问地图时删除过期项目,而不是使用后台线程。
      【解决方案4】:

      番石榴缓存构建器:

      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&lt;K,WeakReference&lt;V&gt;&gt; 其值符合 GC 的条件用于周参考。

      最重要的是,我们始终将 EhCacheMemcachedcoherence 作为热门选择。

      【讨论】:

        【解决方案5】:

        我认为你的决定是正确的。 确切地说,我会使用 HashMap。

        【讨论】:

        • 糟糕的选择,LinkedHashMap 在这里会更好,因为它可以通过删除过时的条目来减少内存消耗。
        【解决方案6】:

        正如前面的答案中提到的,最好使用流行的内存缓存之一,如 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();
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2020-09-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-10
          相关资源
          最近更新 更多