【问题标题】:Guava caching using independent keys使用独立键的 Guava 缓存
【发布时间】:2015-11-18 20:18:49
【问题描述】:

在处理来自数据库的用户对象时,通常有一个 id 和一个用户名,通常通过 id 或用户名搜索用户。

如果我现在想找到用户并喜欢使用 Guava 缓存,我必须创建两个缓存。一种是按 id 缓存,一种是按用户名缓存。
但两者都指向同一个对象。

是否可以只使用一个LoadingCache?

我考虑过使用用户对象本身作为键 LoadingCache<User, User> 并在用户对象中实现等于和哈希码。
在 equals 方法中,如果 id 用户名相等,则很容易说两个 User 对象相等。
但是我怎样才能生成一个适用于这种场景的好的 hashCode 方法呢?

对此有什么想法吗?

【问题讨论】:

  • 我不清楚为什么你只想使用一个缓存。在这种情况下,使用两个缓存似乎是正确的做法。 (让你的 equals 方法像那样工作肯定会破坏事情;equals 应该是可传递的,而你的逻辑不是。)
  • 在 Guava Cache 的前身中,我有一个提供此功能的 indexable cache 示例。很少需要我们没有将这个概念引入 Guava。不过,基本思想可以在您自己的代码中进行调整(例如,使用 Guava 的 Striped)。

标签: java caching guava


【解决方案1】:

在处理来自数据库的用户对象时,通常有一个 id 和一个用户名,通常通过 id 或用户名搜索用户。

备注:“搜索”对我来说意味着不同的东西,然后访问。也许 id 和 username 有不同的使用模式?也许用户名只在登录时需要?

避免在您的应用程序中使用两个不同的概念来引用/访问用户。决定始终使用它。用户名是否唯一?能改吗?

两个缓存:您可以使用两个缓存并使用name2user.put(user.getName(), user)id2user.put(user.getId(), user) 从加载器填充“姐妹缓存”。这样,相同的用户对象在两个缓存中。不过,由于清洁度和一致性问题,我不喜欢它。

第三个问题是数据重复,如果您决定更改为其他解决方案。缓存可以不通过引用存储值,而是将其复制到紧凑的字节数组中并将其存储在堆外(EHCache3、Hazelcast 等)。 (干净的)代码不应该依赖这样一个事实,即缓存通过引用在堆中存储其数据,如果没有真正需要的话。

如上所述,两条访问路径在使用上不会相等。我的建议:

  • 一个缓存用于缓存用户数据:id -> User
  • 仅用于解析 id 的第二个缓存:name -> id

不要介意name 的额外缓存访问。当然,第二个缓存的加载器我已经为此请求了一个用户,所以你可能想用它预填充第一个缓存。

【讨论】:

    【解决方案2】:

    非常感谢您的回答,尤其是来自 Guava 开发人员本身的回答。建议的解决方案对我来说非常有用,我很懒;)。

    因此,如果我不再需要缓存,我决定以这种方式解决它。

    final LoadingCache<Serializable, Optional<ITemplate>> templatesById = CacheBuilder.newBuilder()
            .maximumSize(MAX_CACHE_SIZE).expireAfterAccess(MAX_CACHE_LIFE_TIME, TimeUnit.MINUTES)
            .build(new CacheLoader<Serializable, Optional<ITemplate>>() {
    
                @Override
                public Optional<ITemplate> load(final Serializable id) {
                    final ITemplate template = readInternal(id);
                    final Optional<ITemplate> optional = Optional.ofNullable(template);
                    if (template != null) {
                        templatesByKey.put(template.getKey(), optional);
                    }
                    return optional;
                }
            });
    
    final LoadingCache<String, Optional<ITemplate>> templatesByKey = CacheBuilder.newBuilder()
            .maximumSize(MAX_CACHE_SIZE).expireAfterAccess(MAX_CACHE_LIFE_TIME, TimeUnit.MINUTES)
            .build(new CacheLoader<String, Optional<ITemplate>>() {
    
                @Override
                public Optional<ITemplate> load(final String key) {
                    final ITemplate template = byKeyInternal(key);
                    final Optional<ITemplate> optional = Optional.ofNullable(template);
                    if (template != null) {
                        templatesById.put(template.getId(), optional);
                    }
                    return optional;
                }
            });
    

    这意味着,我不会因为在两个缓存中拥有两个模板实例而浪费内存。所以我只是将一个模板添加到两个缓存中,如果它是从数据库中接收的。

    效果非常好,而且速度非常快。
    唯一的问题是,何时告诉缓存刷新。 在我的场景中,它仅在删除或更新时需要。

    @Override
    @Transactional
    public void update(final ITemplate template) {
        super.update(new DBTemplate(template));
        templatesById.invalidate(template.getId());
        templatesByKey.invalidate(template.getKey());
    }
    

    就是这样。
    有什么相关的吗?

    【讨论】:

    • 这正是我在回答中描述和讨论的解决方案概念“从加载器填充姐妹缓存”,以及缺点。主要问题是一致性,因为没有针对缓存中的两个引用指向特定时间点的不同对象的保护措施。如果您正确设计了事务边界并且 byKeyInternal 和 readInternal 有两个事务上下文,您可能会解决这个问题。
    • 为什么要指向不同的对象?它们仅填充您看到的代码。因此,此代码将一个对象添加到两个缓存中。怎么会有区别?
    • 我说的是一个特定的时间点。这是一个竞赛条件。在加载完成之前,对姐妹缓存的放入已经完成。
    • 如果你只通过你的更新方法和事务注释进行更新,数据库提交是在失效后完成的,那么它就可以工作。只要每次更新都针对此代码,并且只要每个人都了解多年后的含义......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-02
    • 1970-01-01
    • 1970-01-01
    • 2016-10-26
    • 2019-06-18
    • 1970-01-01
    相关资源
    最近更新 更多