在基于Spring的应用程序中,我有一个服务来执行某些Index
的计算。 Index
的计算成本相对较高(例如1s),但检查现状(比如20ms)相对便宜。实际代码无关紧要,它遵循以下几行:
public Index getIndex() {
return calculateIndex();
}
public Index calculateIndex() {
// 1 second or more
}
public boolean isIndexActual(Index index) {
// 20ms or less
}
我使用Spring Cache通过@Cacheable
注释缓存计算出的索引:
@Cacheable(cacheNames = CacheConfiguration.INDEX_CACHE_NAME)
public Index getIndex() {
return calculateIndex();
}
我们目前将GuavaCache
配置为缓存实现:
@Bean
public Cache indexCache() {
return new GuavaCache(INDEX_CACHE_NAME, CacheBuilder.newBuilder()
.expireAfterWrite(indexCacheExpireAfterWriteSeconds, TimeUnit.SECONDS)
.build());
}
@Bean
public CacheManager indexCacheManager(List<Cache> caches) {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(caches);
return cacheManager;
}
我还需要检查缓存的值是否仍然是实际值并刷新它(理想情况下是异步的)如果不是。理想情况下应该如下:
getIndex()
时,Spring会检查缓存中是否有值。
calculateIndex()
加载新值并将其存储在缓存isIndexActual(...)
检查现有值的实际情况。
基本上我想快速提供缓存中的值(即使它已经过时),但也会立即触发刷新。
到目前为止,我所做的工作是检查现状和驱逐:
@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result)")
public Index getIndex() {
return calculateIndex();
}
如果结果已过时,此检查会触发驱逐并立即返回旧值,即使是这种情况。但这不会刷新缓存中的值。
有没有办法配置Spring Cache以在驱逐后主动刷新过时的值?
更新
这里是MCVE。
public static class Index {
private final long timestamp;
public Index(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
public interface IndexCalculator {
public Index calculateIndex();
public long getCurrentTimestamp();
}
@Service
public static class IndexService {
@Autowired
private IndexCalculator indexCalculator;
@Cacheable(cacheNames = "index")
@CacheEvict(cacheNames = "index", condition = "target.isObsolete(#result)")
public Index getIndex() {
return indexCalculator.calculateIndex();
}
public boolean isObsolete(Index index) {
long indexTimestamp = index.getTimestamp();
long currentTimestamp = indexCalculator.getCurrentTimestamp();
if (index == null || indexTimestamp < currentTimestamp) {
return true;
} else {
return false;
}
}
}
现在测试:
@Test
public void test() {
final Index index100 = new Index(100);
final Index index200 = new Index(200);
when(indexCalculator.calculateIndex()).thenReturn(index100);
when(indexCalculator.getCurrentTimestamp()).thenReturn(100L);
assertThat(indexService.getIndex()).isSameAs(index100);
verify(indexCalculator).calculateIndex();
verify(indexCalculator).getCurrentTimestamp();
when(indexCalculator.getCurrentTimestamp()).thenReturn(200L);
when(indexCalculator.calculateIndex()).thenReturn(index200);
assertThat(indexService.getIndex()).isSameAs(index100);
verify(indexCalculator, times(2)).getCurrentTimestamp();
// I'd like to see indexCalculator.calculateIndex() called after
// indexService.getIndex() returns the old value but it does not happen
// verify(indexCalculator, times(2)).calculateIndex();
assertThat(indexService.getIndex()).isSameAs(index200);
// Instead, indexCalculator.calculateIndex() os called on
// the next call to indexService.getIndex()
// I'd like to have it earlier
verify(indexCalculator, times(2)).calculateIndex();
verify(indexCalculator, times(3)).getCurrentTimestamp();
verifyNoMoreInteractions(indexCalculator);
}
我希望在从缓存中逐出后立即刷新该值。目前,在getIndex()
的下一次呼叫中首先刷新它。如果在驱逐后立即刷新了这个值,这将在以后保存我。
我已经尝试了@CachePut
,但它也没有给我带来预期的效果。该值已刷新,但无论condition
或unless
是什么,都始终执行该方法。
我目前看到的唯一方法是拨打getIndex()
两次(第二次异步/非阻止)。但那是一种愚蠢的行为。
答案 0 :(得分:6)
我想说做你需要的最简单的方法就是创建一个自定义的Aspect,它可以透明地完成所有的魔法,并且可以在更多的地方重复使用。
假设你的类路径有spring-aop
和aspectj
依赖关系,那么下面的方面就可以了。
@Aspect
@Component
public class IndexEvictorAspect {
@Autowired
private Cache cache;
@Autowired
private IndexService indexService;
private final ReentrantLock lock = new ReentrantLock();
@AfterReturning(pointcut="hello.IndexService.getIndex()", returning="index")
public void afterGetIndex(Object index) {
if(indexService.isObsolete((Index) index) && lock.tryLock()){
try {
Index newIndex = indexService.calculateIndex();
cache.put(SimpleKey.EMPTY, newIndex);
} finally {
lock.unlock();
}
}
}
}
需要注意几点
getIndex()
方法没有参数,因此会将其存储在密钥SimpleKey.EMPTY
hello
包中。答案 1 :(得分:1)
以下内容可以按所需方式刷新缓存,并使实现简单明了。
如果满足要求,那么编写清晰简单的代码就没有错误。
@Service
public static class IndexService {
@Autowired
private IndexCalculator indexCalculator;
public Index getIndex() {
Index cachedIndex = getCachedIndex();
if (isObsolete(cachedIndex)) {
evictCache();
asyncRefreshCache();
}
return cachedIndex;
}
@Cacheable(cacheNames = "index")
public Index getCachedIndex() {
return indexCalculator.calculateIndex();
}
public void asyncRefreshCache() {
CompletableFuture.runAsync(this::getCachedIndex);
}
@CacheEvict(cacheNames = "index")
public void evictCache() { }
public boolean isObsolete(Index index) {
long indexTimestamp = index.getTimestamp();
long currentTimestamp = indexCalculator.getCurrentTimestamp();
if (index == null || indexTimestamp < currentTimestamp) {
return true;
} else {
return false;
}
}
}
答案 2 :(得分:0)
EDIT1:
基于@Cacheable
和@CacheEvict
的缓存抽象在这种情况下不起作用。这些行为如下:在@Cacheable
调用期间,如果值在缓存中 - 从缓存返回值,否则计算并放入缓存然后返回;在@CacheEvict
期间,该值将从缓存中删除,因此从此刻起缓存中没有值,因此@Cacheable
上的第一个传入呼叫将强制重新计算并进入缓存。使用@CacheEvict(condition="")
将仅根据此条件执行此调用期间从缓存值中删除的条件检查。因此,在每次失效后,@Cacheable
方法将运行此重量级例程来填充缓存。
让值beign存储在缓存管理器中,并异步更新,我建议重用以下例程:
@Inject
@Qualifier("my-configured-caching")
private Cache cache;
private ReentrantLock lock = new ReentrantLock();
public Index getIndex() {
synchronized (this) {
Index storedCache = cache.get("singleKey_Or_AnythingYouWant", Index.class);
if (storedCache == null ) {
this.lock.lock();
storedCache = indexCalculator.calculateIndex();
this.cache.put("singleKey_Or_AnythingYouWant", storedCache);
this.lock.unlock();
}
}
if (isObsolete(storedCache)) {
if (!lock.isLocked()) {
lock.lock();
this.asyncUpgrade()
}
}
return storedCache;
}
第一个构造是sycnhronized,只是为了阻止所有即将到来的调用,等待第一个调用填充缓存。
然后系统检查是否应该重新生成缓存。如果是,则调用单个异步更新值的调用,并且当前线程返回缓存值。一旦缓存处于重新计算状态,即将进行的调用将简单地从缓存中返回最近的值。等等。
使用这样的解决方案,您将能够重用大量内存,比如说使用hazelcast缓存管理器,以及多个基于密钥的缓存存储,并保持缓存实现和驱逐的复杂逻辑。
或者如果您喜欢@Cacheable
注释,可以按照以下方式执行此操作:
@Cacheable(cacheNames = "index", sync = true)
public Index getCachedIndex() {
return new Index();
}
@CachePut(cacheNames = "index")
public Index putIntoCache() {
return new Index();
}
public Index getIndex() {
Index latestIndex = getCachedIndex();
if (isObsolete(latestIndex)) {
recalculateCache();
}
return latestIndex;
}
private ReentrantLock lock = new ReentrantLock();
@Async
public void recalculateCache() {
if (!lock.isLocked()) {
lock.lock();
putIntoCache();
lock.unlock();
}
}
与上面几乎相同,但重用了spring的缓存注释抽象。
ORIGINAL: 为什么要尝试通过缓存来解决这个问题?如果这是简单的值(不是基于密钥的,您可以以更简单的方式组织代码,请记住,默认情况下spring服务是单例)
类似的东西:
@Service
public static class IndexService {
@Autowired
private IndexCalculator indexCalculator;
private Index storedCache;
private ReentrantLock lock = new ReentrantLock();
public Index getIndex() {
if (storedCache == null ) {
synchronized (this) {
this.lock.lock();
Index result = indexCalculator.calculateIndex();
this.storedCache = result;
this.lock.unlock();
}
}
if (isObsolete()) {
if (!lock.isLocked()) {
lock.lock();
this.asyncUpgrade()
}
}
return storedCache;
}
@Async
public void asyncUpgrade() {
Index result = indexCalculator.calculateIndex();
synchronized (this) {
this.storedCache = result;
}
this.lock.unlock();
}
public boolean isObsolete() {
long currentTimestamp = indexCalculator.getCurrentTimestamp();
if (storedCache == null || storedCache.getTimestamp() < currentTimestamp) {
return true;
} else {
return false;
}
}
}
即。第一个呼叫已同步,您必须等到结果填充完毕。然后,如果存储的值已过时,系统将执行值的异步更新,但当前线程将接收存储的&#34;缓存&#34;值。
我还引入了可重入锁定来限制存储索引的单次升级。
答案 3 :(得分:0)
我会在索引服务中使用Guava LoadingCache,如下面的代码示例所示:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return getGraphFromDatabase(key);
}
public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prevGraph);
} else {
// asynchronous!
ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
public Graph call() {
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
&#13;
您可以通过调用Guava的方法创建异步重新加载缓存加载器:
public abstract class CacheLoader<K, V> {
...
public static <K, V> CacheLoader<K, V> asyncReloading(
final CacheLoader<K, V> loader, final Executor executor) {
...
}
}
&#13;
诀窍是使用ThreadPoolExecutor在单独的线程中运行重载操作,例如:
答案 4 :(得分:0)
我认为它可能像
@Autowired
IndexService indexService; // self injection
@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result) && @indexService.calculateIndexAsync()")
public Index getIndex() {
return calculateIndex();
}
public boolean calculateIndexAsync() {
someAsyncService.run(new Runable() {
public void run() {
indexService.updateIndex(); // require self reference to use Spring caching proxy
}
});
return true;
}
@CachePut(cacheNames = INDEX_CACHE_NAME)
public Index updateIndex() {
return calculateIndex();
}
以上代码存在问题,如果您在更新时再次致电getIndex()
,则会再次计算。为防止出现这种情况,最好不要使用@CacheEvict
并让@Cacheable
返回过时的值,直到索引计算完毕为止。
@Autowired
IndexService indexService; // self injection
@Cacheable(cacheNames = INDEX_CACHE_NAME, condition = "!(target.isObsolete(#result) && @indexService.calculateIndexAsync())")
public Index getIndex() {
return calculateIndex();
}
public boolean calculateIndexAsync() {
if (!someThreadSafeService.isIndexBeingUpdated()) {
someAsyncService.run(new Runable() {
public void run() {
indexService.updateIndex(); // require self reference to use Spring caching proxy
}
});
}
return false;
}
@CachePut(cacheNames = INDEX_CACHE_NAME)
public Index updateIndex() {
return calculateIndex();
}