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.