Ehcache - 使用List<Integer>作为缓存值

7

我要解决的问题是:我有一个带有两个整数字段的对象,我想将其缓存。

public class MyObject {
   int x;
   int y;
   ....
}

现在我主要根据字段x进行匹配,但是可能存在重复情况,这种情况下我希望回退到第二个字段(使得this.x = that.x且this.y = that.y)。y只能有25个不同的值。现在我知道我可以将两者组合成一个字符串并将其用作缓存键,但是那样我就必须尝试x + [25个可能的值]才能确定它不在缓存中,从而使缓存未命中变得非常昂贵。我考虑尝试将List<Integer>作为字段x的缓存值存储,然后如果有多个,则迭代列表并查找y的匹配项。
现在,如果我使用ConcurrentList(或Set,如果我关心重复项-暂时忽略),多个线程是否能够添加到它中,并在没有竞争条件的情况下将其放回缓存?Ehcache是否可能向两个线程返回两个不同的List对象,然后当它们将新值添加到列表并尝试将其放回缓存时,我可能会得到不确定的结果?您是否看到构建此缓存的更好方法?
编辑:感谢下面的答案,但是每个人似乎都错过了主要观点。这个方法行得通吗?Ehcache是否可能为相同的缓存键返回两个不同的对象(例如,在调用期间对象在磁盘上并且它被序列化了两次,每次调用一次)。

正如我在问题中所述,这意味着对于任何给定的x值,我都必须检查缓存可能25次才能确定它不在缓存中。我想匹配x,无论y的值如何 - 但如果有多个x,则选择最佳的y值。 - Gandalf
是的,使用您提到的方法可以将值添加两次。不过最好使用ConcurrentMap。 - Keegan Carruthers-Smith
是的,我明白 Keegan(我会修改问题以反映这一点)- 真正的问题是是否可能创建两个完全不同的 List(或者像你指出的 Set)对象。 - Gandalf
取决于您用于从x -> Foo的映射的数据结构以及您如何使用它(其中Foo可以是ConcurrentList或ConcurrentMap)。假设您使用线程安全的映射,只要您从未替换现有的(键,值)并且正确地将插入操作执行到线程安全的映射中,一切都会很好。 - Keegan Carruthers-Smith
如果有多个对象具有相同的x值,但没有一个与给定的y值匹配,您想选择哪个对象?根据您的问题,我假设在重复的x值情况下,您会选择具有给定y值的对象,并且如果没有,则会出现缓存未命中。 - Christian Semrau
显示剩余2条评论
5个回答

5

您完全有可能获得两个不同的列表实例(或任何可序列化对象的实例)!试试这个:

public static void main(final String[] args) throws Exception {
    final Cache cache = CacheManager.getInstance().getCache("smallCache");

    final List<String> list = new ArrayList<String>();
    cache.put(new Element("A", list));

    /* We put in a second element. Since maxElementsInMemory="1", this means
     * that "A" will be evicted from memory and written to disk. */
    cache.put(new Element("B", new ArrayList<String>())); 
    Thread.sleep(2000); // We need to wait a bit, until "A" is evicted.

    /* Imagine, the following happens in Thread 1: */
        final List<String> retrievedList1 =
                   (List<String>) cache.get("A").getValue();
        retrievedList1.add("From Thread 1");

    /* Meanwhile, someone puts something in the cache: */
        cache.put(new Element("C", new ArrayList<String>())); 

    Thread.sleep(2000); // Once again, we wait a bit, until "A" is evicted.

    /* Now the following happens in Thread 2: */
        final List<String> retrievedList2 =
                   (List<String>) cache.get("A").getValue();
        retrievedList2.add("From Thread 2");
        cache.put(new Element("A", retrievedList2));

    /* Meanwhile in Thread 1: */    
        cache.put(new Element("A", retrievedList1));

    /* Now let's see the result: */
    final List<String> resultingList =
                        (List<String>) cache.get("A").getValue();
    for (final String string : resultingList) {
        System.out.println(string);
    } /* Prints only "From Thread 1". "From Thread 2" is lost.
                 But try it with maxElementsInMemory="3", too!! */

    CacheManager.getInstance().shutdown();
}

我在ehcache.xml中使用了以下内容:
<cache name="smallCache"
       maxElementsInMemory="1"
       eternal="true"
       overflowToDisk="true"
       diskPersistent="true"
       maxElementsOnDisk="200"
       memoryStoreEvictionPolicy="LRU"
       transactionalMode="off"
       >
</cache>

一种解决方案可能是使用显式锁定,这似乎也适用于独立(非Terracotta)缓存,自ehcache 2.1以来。

另一种解决方案是只有一个线程可以修改列表。如果您有多个线程可以修改它,并且您不在缓存上使用锁定,则可以获得您所描述的不确定结果!


这并不显示缓存返回列表的两个不同副本,而是显示您持有一个副本,然后从磁盘读取一个副本。 - Gandalf
@Gandalf:只需做两次相同的事情:再做一次flush() + Thread.sleep(),然后再次检索它。你会得到两个不同的副本。 - Chris Lercher
@Gandalf:我对示例进行了进一步编辑(模拟多个线程),以便更清楚地说明问题是如何发生的。现在它有点长,但它确切地展示了你在问题中所担心的情况。请尝试将maxElementsInMemory设置为“3”,并查看差异! - Chris Lercher

2

我有一个不同的方法,是我在一篇关于地理范围搜索的文章中读到的。

将两个键值对放入缓存中:一个只有 x 作为键,另一个则同时包含 x 和 y 作为键。当你查找缓存时,首先查找 x 和 y 的组合键。如果存在,则找到了完全匹配。如果不存在,则查找仅包含 x 的键,并可能找到具有不同 y 值的匹配项。


1
缓存大约有1500万个对象 - 所以我不想将其增加到3000万,除非没有其他办法。不过我会记住这一点的。 - Gandalf
另外,缓存如何仅使用“x”作为键工作,因为我可以有多个具有相同“x”值(但不同“y”)的对象? - Gandalf
最坏情况下这并没有变得更好。如果我找不到x+y,那么我会搜索x - 如果我找到了x,那么我必须继续搜索下一个x+y选项(并通过所有选项直到找到一个)。 - Gandalf
如果你正在使用SortedMap,假设你搜索(x,y)并且没有找到它,但是(x, -1)存在(因为某个y存在于映射中使得(x, y)在其中)。那么你可以使用Iterator foo = sortedMap.tailMap((x, -1)).values().iterator(); foo.next(); return foo.next(); 这有点取巧,但可以优化一下 :) 。我不确定Java的SortedMap如何工作,但sortedMap.tailMap((x, 0)).firstKey()可能会起作用,这比之前说的要简洁很多。 - Keegan Carruthers-Smith
只有键的数量会增加,而值的数量不会增加,除非缓存复制对象。键的数量将增长1/25到1倍的因子,因为如果有多个具有相同x值的对象,则只有其中一个将使用仅x键进行缓存。 - Christian Semrau
显示剩余2条评论

1

我会创建一个方法来获取你的对象值。使用信号量来限制对该方法的访问(或使用synchronized)。

在你的方法中,测试X-only匹配,并且如果返回多个结果,则测试XY匹配。

一旦对象离开缓存,对对象所做的任何修改都将同时修改缓存中的对象(因为它们指向同一实例)。

如果你想要非常小心谨慎,请使用synchronized方法来获取/设置MyObject中的成员变量,并包含一个锁,即MyObject实例。

public void setX( int x ) {
     synchronized( this ) {
         this.x = x;
     }
}

0

0
  • 创建一个包含 xy 的 Key 类,即 class Key { int x,y }
  • 为 "字典序" 实现一个单独的比较操作,用于比较 xy
  • 将其放入一个 Map<Key,Value>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接