使用番石榴缓存(内存表)维护多个索引

Posted

技术标签:

【中文标题】使用番石榴缓存(内存表)维护多个索引【英文标题】:Maintaining multiple indexes with guava cache (in-memory table) 【发布时间】:2012-06-04 05:23:51 【问题描述】:

我正在尝试实现一个简化的内存缓存“表”,其中有两种类型的索引:主索引和辅助索引。

主索引将单个键(主键)映射到唯一值(映射接口)

二级索引将单个键映射到值集合(Multimap 符合要求)

与 RDBMS 世界中的表非常相似,其中一个表具有多个查找列。有时您想按 PK 搜索,有时返回基于公共属性的行列表。目前,除了等于 (=) 之外,不需要其他操作(即没有范围查询或模式匹配)。

将缓存语义添加到上述数据结构(驱逐、数据填充/缓存加载器、刷新等),这就是我们所需要的。

我想就如何最好地解决给定问题征求您的意见。应该是 Cache per index 还是 Cache (for PK) + (synchronized) Multimap for secondary index ?

非常感谢任何帮助。

问候。

【问题讨论】:

安德烈,给出的答案有多大帮助? 对不起,但我看不出你是如何假装缓存你的数据结构的。如我所见,如果您按 PK 搜索,它将访问您的地图并在 O(1) 中找到正确的元素(如果一切正常),对吗?你想缓存什么? 嗨普林尼奥。假设我尝试缓存用户。它们可能具有userIdgroup 属性。当我通过userId 搜索时,只会返回 1 个元素,因为它是 PK。通过group 查找将返回Collection<User>(例如,管理组中的所有用户)。添加 UseruserId:123, group:'admin' 将更新两个索引 @kevin-bourrillion 还没有明确的答案。 【参考方案1】:

您可以将 Map 替换为 Guava com.google.common.cache.Cache。它不支持 Multimap 类型语义,所以你必须使用

Cache<K, ? extends List<V>> 

在这种情况下。

为了简单起见,我会将“主索引”设置为二级索引的子集 - 即您有一个索引,它返回给定键的值列表,而主键只返回一个具有单个值的列表.

【讨论】:

您好弗拉基米尔,问题更多是关于如何保持这些结构同步。一个缓存的变化应该意味着另一个缓存的修改。通过主键添加元素应该更新二级索引等。由于我不需要 ACID 属性,我希望可以实现比内存 RDBMS 更快的东西。 @Andrei 但是您需要什么样的一致性和同步性?如果你通过二级索引查询你的缓存,你想要所有对应的行吗?可能是。如果一行被主键加载,是否应该以某种方式将其放入匹配的二级索引中?如果使用一个索引访问一行,是否应该防止它在所有其他索引中过期? @maaartinus 可能仅应在 PK + RemovalListener 上设置过期时间,它为二级索引执行此操作。现在我在想是否让 secondaryIndex 包含 Collection 作为值并基于 PK 执行第二次查找以更新驱逐统计信息是否更有意义。你怎么看? 我的意见是“可能”。这意味着两次访问,并且可能错过了两倍的缓存。如果您有一个包含 100 个 PK 且其中一半丢失的二级索引,您最终可能会向 DB 发出 50 个单独的查询(类似于the n+1 selects problem。OTOH,存储整行可能需要更多内存,我看不到如何处理驱逐。或者我可能会......我稍后再回来查看。 如果你足够小心,可以保持所有索引同步(使用 RemovalListener),即二级键查找没有缓存未命中【参考方案2】:

这里的挑战是保持两个索引的完整性,无论您是使用两个缓存还是一个缓存来进行 PK + multimap。

也许你应该创建一个扩展 com.google.common.cache.Cache 的新缓存类(比如 TableCache),在内部这个类可以为二级索引维护一个多映射实例变量(可以是 ConcurrentHashMap)。

然后您可以覆盖缓存方法(put、get、invalidate 等)以保持二级索引同步。

当然,你必须提供一个 get 函数来根据二级索引检索值。

这种方法使您能够维护主索引和二级索引的完整性。

public class TableCache<K, V> extends Cache<K, V> 

    Map<K, List<V>> secondaryIndex = new ConcurrentHashMap<K, List<V>>();

    public void put(K key, V value) 
        super.put(key, value);
        // Update secondaryIndex
    


【讨论】:

Sprumal,这更接近我的需要,但仍有一些问题需要解决。当我只想在内存中保留一部分元素时该怎么办?可能使用 CacheLoader。问题是 CacheLoader 仅通过 PK 加载。像 group=admin 这样对不在 LocalCache 中的元素的搜索会发生什么?这是否意味着每个索引(包括二级索引)都必须有一个 CacheLoader ? 在这个数据结构中,我们试图模拟一个数据库算法。基于搜索键,无论是基于主键还是辅助键,算法都应该将数据加载到内存中以完成请求的查询。 Comcopy">评论继续...所以我们需要两个加载器,一个基于主键加载数据,另一个基于辅助键加载。使用主键很简单,如果它不在内存中,它会从数据存储中加载键。如果查询是基于辅助键的,那么加载器应该足够聪明,可以从数据存储中加载数据。 Sprumal,是的,我们似乎需要几个 CacheLoader(缓存),但是驱逐策略仍然存在一个问题。正如@maaartinus 所质疑的那样,我们是从主索引/二级索引(缓存)中独立删除元素还是仅从 PK 强制驱逐?大概是后者。现在实现变得更加棘手,我不确定我是否在为这样的用例滥用 guava 缓存(或一般的 K/V 存储) 更简单的解决方案是创建一个 CompositeCache,它可以维护两个缓存,一个用于主缓存,一个用于辅助缓存。 CompositeCache 可以监听对单个缓存所做的更改并更新其他缓存。在这种方法中,您可以将数据的子集保留在内存中,而无需任何复杂的算法。【参考方案3】:

我自己也遇到过很多次这个问题。

如果 Java 有更好的STM support,那么解决这个问题的方法是。制作非阻塞原子数据结构非常困难。我见过的最好的是multiverse。

因此@vladimir 的答案可能是最好的,但我会说存储的集合应该是不可变的,并且您必须在刷新/缓存未命中等时检索整个集合......另外,如果您更改其中一个成员多重设置您将很难知道如何更新其父级并使缓存无效。

否则我会考虑像 Redis 这样的大型数据集,它支持地图和列表组合上的原子操作。

【讨论】:

以上是关于使用番石榴缓存(内存表)维护多个索引的主要内容,如果未能解决你的问题,请参考以下文章

高性能内存对象缓存Memcached

MySQL内存使用-全局共享

缓存专题

Memcached(高性能内存对象缓存)

Memcached缓存框架的使用

高性能内存对象缓存——Memcached