Spring Cache刷新过时的值

时间:2017-02-20 11:02:13

标签: java spring caching

在基于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,但它也没有给我带来预期的效果。该值已刷新,但无论conditionunless是什么,都始终执行该方法。

我目前看到的唯一方法是拨打getIndex()两次(第二次异步/非阻止)。但那是一种愚蠢的行为。

5 个答案:

答案 0 :(得分:6)

我想说做你需要的最简单的方法就是创建一个自定义的Aspect,它可以透明地完成所有的魔法,并且可以在更多的地方重复使用。

假设你的类路径有spring-aopaspectj依赖关系,那么下面的方面就可以了。

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

需要注意几点

  1. 由于您的getIndex()方法没有参数,因此会将其存储在密钥SimpleKey.EMPTY
  2. 的缓存中
  3. 该代码假定IndexService位于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,如下面的代码示例所示:

&#13;
&#13;
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;
&#13;
&#13;

您可以通过调用Guava的方法创建异步重新加载缓存加载器:

&#13;
&#13;
public abstract class CacheLoader<K, V> {
...

  public static <K, V> CacheLoader<K, V> asyncReloading(
      final CacheLoader<K, V> loader, final Executor executor) {
      ...
      
  }
}
&#13;
&#13;
&#13;

诀窍是使用ThreadPoolExecutor在单独的线程中运行重载操作,例如:

  • 第一次调用时,缓存由load()方法填充,因此可能需要一些时间才能回答,
  • 在后续调用中,当需要刷新值时,它会在仍然提供过时值的同时异步计算。刷新完成后,它将提供更新的值。

答案 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();
}