问题
I have a Java class that has a Guava LoadingCache<String, Integer>
and in that cache, I'm planning to store two things: the average time active employees have worked for the day and their efficiency. I am caching these values because it would be expensive to compute every time a request comes in. Also, the contents of the cache will be refreshed (refreshAfterWrite
) every minute.
I was thinking of using a CacheLoader
for this situation, however, its load method only loads one value per key. In my CacheLoader
, I was planning to do something like:
private Service service = new Service();
public Integer load(String key) throws Exception {
if (key.equals("employeeAvg"))
return calculateEmployeeAvg(service.getAllEmployees());
if (key.equals("employeeEff"))
return calculateEmployeeEff(service.getAllEmployees());
return -1;
}
For me, I find this very inefficient since in order to load both values, I have to invoke service.getAllEmployees()
twice because, correct me if I'm wrong, CacheLoader
's should be stateless.
Which made me think to use the LoadingCache.put(key, value)
method so I can just create a utility method that invokes service.getAllEmployees()
once and calculate the values on the fly. However, if I do use LoadingCache.put()
, I won't have the refreshAfterWrite
feature since it's dependent on a cache loader.
How do I make this more efficient?
回答1:
It seems like your problem stems from using strings to represent value types (Effective Java Item 50). Instead, consider defining a proper value type that stores this data, and use a memoizing Supplier to avoid recomputing them.
public static class EmployeeStatistics {
private final int average;
private final int efficiency;
// constructor, getters and setters
}
Supplier<EmployeeStatistics> statistics = Suppliers.memoize(
new Supplier<EmployeeStatistics>() {
@Override
public EmployeeStatistics get() {
List<Employee> employees = new Service().getAllEmployees();
return new EmployeeStatistics(
calculateEmployeeAvg(employees),
calculateEmployeeEff(employees));
}});
You could even move these calculation methods inside EmployeeStatistics
and simply pass in all employees to the constructor and let it compute the appropriate data.
If you need to configure your caching behavior more than Suppliers.memoize()
or Suppliers.memoizeWithExpiration() can provide, consider this similar pattern, which hides the fact that you're using a Cache
inside a Supplier
:
Supplier<EmployeeStatistics> statistics = new Supplier<EmployeeStatistics>() {
private final Object key = new Object();
private final LoadingCache<Object, EmployeeStatistics> cache =
CacheBuilder.newBuilder()
// configure your builder
.build(
new CacheLoader<Object, EmployeeStatistics>() {
public EmployeeStatistics load(Object key) {
// same behavior as the Supplier above
}});
@Override
public EmployeeStatistics get() {
return cache.get(key);
}};
回答2:
However, if I do use
LoadingCache.put()
, I won't have therefreshAfterWrite
feature since it's dependent on a cache loader.
I'm not sure, but you might be able to call it from inside the load
method. I mean, compute the requested value as you do and put
in the other. However, this feels hacky.
If service.getAllEmployees
is expensive, then you could cache it. If both calculateEmployeeAvg
and calculateEmployeeEff
are cheap, then recompute them when needed. Otherwise, it looks like you could use two caches.
I guess, a method computing both values at once could be a reasonable solution. Create a tiny Pair-like class aggregating them and use it as the cache value. There'll be a single key only.
Concerning your own solution, it could be as trivial as
class EmployeeStatsCache {
private long validUntil;
private List<Employee> employeeList;
private Integer employeeAvg;
private Integer employeeEff;
private boolean isValid() {
return System.currentTimeMillis() <= validUntil;
}
private synchronized List<Employee> getEmployeeList() {
if (!isValid || employeeList==null) {
employeeList = service.getAllEmployees();
validUntil = System.currentTimeMillis() + VALIDITY_MILLIS;
}
return employeeList;
}
public synchronized int getEmployeeAvg() {
if (!isValid || employeeAvg==null) {
employeeAvg = calculateEmployeeAvg(getEmployeeList());
}
return employeeAvg;
}
public synchronized int getEmployeeEff() {
if (!isValid || employeeAvg==null) {
employeeAvg = calculateEmployeeEff(getEmployeeList());
}
return employeeAvg;
}
}
Instead of synchronized
methods you may want to synchronize on a private final field. There are other possibilities (e.g. Atomic*
), but the basic design is probably simpler than adapting Guava's Cache
.
Now, I see that there's Suppliers#memoizeWithExpiration
in Guava. That's probably even simpler.
来源:https://stackoverflow.com/questions/31576136/how-should-i-implement-guava-cache-when-i-plan-to-cache-multiple-values-efficien