简体   繁体   中英

How do I store a Map in a Guava Cache

I had a Map<Range<Double>, String> that checks where a particular Double value (score) is mapped to a String (level). The end users want to be able to dynamically change this mapping, in the long term we would like for there to be a web based GUI where they control this but for the short term they're happy for a file to be in S3 and to editing that whenever a change is needed. I don't want to hit S3 for each request and want to cache this as it doesn't change too frequently(Once a week or so). I don't want to have to make a code change and bounce my service either.

Here is what I have come up with -

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); }
  } 
}

The obvious problem with this approach is in the load method when I do Map<Range<Double>, String> scoreToLevelMap = readMappingFromS3(); For each key I'm loading the entire map over and over. This isn't a performance issue but it could become one when the size increases, in any case this is not an efficient approach.

I think that keeping this entire map in the cache would be better, but I'm not sure how to do that here. Can anyone help with this or suggest a more elegant way of achieving this.

Guava has a different mechanism for "a cache that only ever contains one value"; it's called 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.
}

Do not mix caching and business logic. Unless your score mapping is huge AND you can load individual pieces, eg using readMappingFromS3(Double d) - simply cache the whole map.

    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);
        }
    }

Fetch level name like this

    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);
            }
        }
        ...
    }

Here is a solution that reads entire map at once and keeps refreshing it asynchronouzly as needed via asyncReloading .

It would return old value during refresh without blocking multiple reader threads like Suppliers.memoizeWithExpiration does .

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);
    }
}

Also consider replacing Map<Range<Double>, String> with RangeMap <Double, String> to perform effective ranged lookups.

so far i couldn't find a way where you could dynamically store map value cached by guava, looks all values needs to be loaded once during initialization of cached map. while you requires to have a map loaded over time rather a solution is to use "PassiveExpiringMap" from org.apache.commons.collections4.map library

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

Which cache keys for given time as they got added to the map.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM