如何在Guava Cache中存储Map

7
我有一个检查特定的Double值(分数)映射到一个String(级别)的Map, String>。最终用户希望能够动态更改此映射,从长远来看,我们希望有一个基于Web的GUI,让他们控制此操作,但在短期内,只需将文件放在S3中,并在需要更改时进行编辑即可满足他们的需求。我不想为每个请求都访问S3,并且希望将其缓存,因为它不会经常更改(大约一周一次)。我也不想进行代码更改并重新启动我的服务。
以下是我的解决方案-
public class Mapper() {
    private LoadingCache<Score, String> scoreToLevelCache;

public Mapper() {
    scoreToLevelCache = CacheBuilder.newBuilder()
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build(new CacheLoader<Score, String>() {
                public String load(Score score) {
                    Map<Range<Double>, String> scoreToLevelMap = readMappingFromS3(); //readMappingFromS3 omitted for brevity
                    for(Range<Double> key : scoreToLevelMap.keySet()) {
                        if(key.contains(score.getCount())) { return scoreToLevelMap.get(key); }
                    }
                    throw new IllegalArgumentException("The score couldn't be mapped to a level. Either the score passed in was incorrect or the mapping is incorrect");
                }
            }); 
}

public String getContentLevelForScore(Score Score) {
    try {
        return scoreToLevelCache.get(Score);
    } catch (ExecutionException e) { throw new InternalServerException(e); }
  } 
}

这种方法的明显问题在于load方法,当我执行Map<Range<Double>, String> scoreToLevelMap = readMappingFromS3();时,每个键都会重复加载整个映射。虽然这不是性能问题,但当大小增加时可能会变成一个性能问题,无论如何,这都不是一种有效的方法。
我认为将整个映射保存在缓存中会更好,但我不确定如何在这里做到这一点。有人可以帮忙解决这个问题或者提出更优雅的解决方案吗?
4个回答

8

Guava有一种不同的机制用于“仅包含一个值的缓存”; 它被称为Suppliers.memoizeWithExpiration

private Supplier<Map<Range<Double>, String> cachedMap = 
    Suppliers.memoizeWithExpiration(
        new Supplier<Map<Range<Double>, String>() {
            public Map<Range<Double>, String> get() {
                return readMappingFromS3();
            }
        }, 10, TimeUnit.MINUTES);

public String getContentLevelForScore(Score score) {
    Map<Range<Double>, String> scoreMap = cachedMap.get();
    // etc.
}

1
这正是我一直在寻找的,我在这里没有看到供应商 - https://code.google.com/p/guava-libraries/wiki/GuavaExplained。是否有一些秘密的地方可以找到更多Guava的隐藏宝石? - nikhil
@nikhil,别忘了标记问题已回答。 - harshtuna
请注意,该解决方案会在过期时阻止多个读取器线程。请查看我的答案,了解如何通过使用刷新的Guava缓存来避免此问题。 - Vadzim

2
不要混淆缓存和业务逻辑。除非您的得分映射非常庞大并且可以加载单个部分,例如使用readMappingFromS3(Double d) - 否则请缓存整个映射表。
    public static final String MAGIC_WORD = "oh please please give me my data!!!";
    private final LoadingCache<String, Map<Range<Double>, String>> scoreToLevelCache;


    public Mapper() {
        scoreToLevelCache = CacheBuilder.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Map<Range<Double>, String>>() {
                    public Map<Range<Double>, String> load(String score) {
                        return readMappingFromS3(); //readMappingFromS3 omitted for brevity
                    }
                });
    }

    public Map<Range<Double>, String> getScoreMap() {
        try {
            return scoreToLevelCache.get(MAGIC_WORD);
        } catch (ExecutionException e) {
            throw new InternalServerException(e);
        }
    }

像这样获取级别名称

    public String findLevel(final Double score) {
        final Map<Range<Double>, String> scoreMap = getScoreMap();
        for (final Range<Double> key : scoreMap.keySet()) {
            if (key.contains(score)) {
                return scoreMap.get(key);
            }
        }
        ...
    }

加1并感谢,如果David没有告诉我供应商,我会这样做。 - nikhil

1

这里有一个解决方案,它一次性读取整个地图,并通过asyncReloading异步地进行根据需要的刷新

刷新时,它会返回旧值,而不会像Suppliers.memoizeWithExpiration{{link4:那样}阻塞多个读取线程。

private static final Object DUMMY_KEY = new Object();
private static final LoadingCache<Object, Map<Range<Double>, String>> scoreToLevelCache = 
        CacheBuilder.newBuilder()
        .maximumSize(1)
        .refreshAfterWrite(10, TimeUnit.MINUTES)
        .build(CacheLoader.asyncReloading(
                new CacheLoader<Object, Map<Range<Double>, String>>() {
                    public Map<Range<Double>, String> load(Object key) {
                        return readMappingFromS3();
                    }
                },
                Executors.newSingleThreadExecutor()));

public String getContentLevelForScore(Score score) {
    try {
        Map<Range<Double>, String> scoreMap = scoreToLevelCache.get(DUMMY_KEY);
        // traverse scoreMap ranges
        return level;
    } catch (ExecutionException e) {
        Throwables.throwIfUnchecked(e);
        throw new IllegalStateException(e);
    }
}

此外,考虑将Map<Range<Double>, String>替换为RangeMap<Double, String>以执行有效的范围查找。

0

到目前为止,我还没有找到一种方法可以动态存储由Guava缓存的映射值,似乎所有的值都需要在缓存映射的初始化期间加载一次。而您需要随着时间的推移加载地图,而解决方案是使用org.apache.commons.collections4.map库中的“PassiveExpiringMap”。

private static Map<String, String> cachedMap = new PassiveExpiringMap<>(30, TimeUnit.SECONDS);

根据它们添加到映射中的时间,返回给定时间的缓存键。


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