Spring Cache刷新过时的值

lex*_*ore 16 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
}
Run Code Online (Sandbox Code Playgroud)

我正在使用Spring Cache通过@Cacheable注释缓存计算的索引:

@Cacheable(cacheNames = CacheConfiguration.INDEX_CACHE_NAME)
public Index getIndex() {
    return calculateIndex();
}
Run Code Online (Sandbox Code Playgroud)

我们目前配置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;
}
Run Code Online (Sandbox Code Playgroud)

我还需要检查缓存的值是否仍然是实际值并刷新它(理想情况下是异步的)如果不是.理想情况下应该如下:

  • getIndex()被调用时,弹簧会检查是否在高速缓存中的值.
    • 如果不是,则通过calculateIndex()缓存加载新值并将其存储在缓存中
    • 如果是,则通过现场检查现有值isIndexActual(...).
      • 如果旧值是实际值,则返回.
      • 如果旧值不是实际值,则返回该值,但是从缓存中删除并且还会触发新值的加载.

基本上我想快速提供缓存中的值(即使它已经过时),但也会立即触发刷新.

到目前为止我所做的工作是检查现状和驱逐:

@Cacheable(cacheNames = INDEX_CACHE_NAME)
@CacheEvict(cacheNames = INDEX_CACHE_NAME, condition = "target.isObsolete(#result)")
public Index getIndex() {
    return calculateIndex();
}
Run Code Online (Sandbox Code Playgroud)

如果结果已过时,则此检查会触发驱逐,并立即返回旧值,即使是这种情况.但这不会刷新缓存中的值.

是否有办法配置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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在测试:

@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);
}
Run Code Online (Sandbox Code Playgroud)

我希望在从缓存中逐出后不久刷新值.目前,它会在下一次调用时刷新getIndex().如果在驱逐后立即刷新了这个值,那么这将节省我1秒钟.

我试过了@CachePut,但它也没有给我带来预期的效果.刷新该值,但无论是什么condition或方法,都始终执行该方法unless.

我目前看到的唯一方法是调用getIndex()两次(第二次异步/非阻塞).但那有点愚蠢.

Bab*_*abl 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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

有几点需要注意

  1. 由于您的getIndex()方法没有参数,因此它存储在密钥缓存中SimpleKey.EMPTY
  2. 该代码假定IndexService在hello包中.


Ily*_*hin 1

编辑1:

基于@Cacheable和的缓存抽象@CacheEvict在这种情况下将不起作用。这些行为如下:在@Cacheable调用过程中,如果该值在缓存中,则从缓存中返回值,否则计算并放入缓存中,然后返回;在该值从缓存中删除期间@CacheEvict,因此从此时起缓存中没有值,因此第一个传入调用@Cacheable将强制重新计算并放入缓存中。使用@CacheEvict(condition="") 只会根据此条件在调用期间检查是否从缓存值中删除。因此,在每次失效之后,该@Cacheable方法将运行这个重量级例程来填充缓存。

为了将值存储在缓存管理器中并异步更新,我建议重用以下例程:

@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;
}
Run Code Online (Sandbox Code Playgroud)

第一个构造是同步的,只是为了阻止所有即将到来的调用,等待第一个调用填充缓存。

然后系统检查是否应该重新生成缓存。如果是,则调用该值的异步更新的单个调用,并且当前线程正在返回缓存的值。一旦缓存处于重新计算状态,即将进行的调用将仅返回缓存中的最新值。等等。

通过这样的解决方案,您将能够重用大量内存,例如 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

与上面几乎相同,但是重用了 spring 的 Caching 注释抽象。

原文:为什么你试图通过缓存来解决这个问题?如果这是简单的值(不是基于键的,您可以以更简单的方式组织代码,请记住 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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

即第一个调用是同步的,您必须等待结果填充。然后,如果存储的值已过时,系统将执行该值的异步更新,但当前线程将接收存储的“缓存”值。

我还引入了可重入锁来限制存储索引的单次升级。