弹簧缓存刷新过时的值

2022-09-02 04:46:22

在基于Spring的应用程序中,我有一个服务,可以执行一些. 计算起来相对昂贵(例如,1s),但检查实际情况相对便宜(例如,20ms)。实际代码无关紧要,它遵循以下行:IndexIndex

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;
}

我还需要检查缓存的值是否仍然实际,如果不是,则刷新它(理想情况下是异步的)。因此,理想情况下,它应该如下所示:

  • 调用 时,Spring 会检查缓存中是否有值。getIndex()
    • 否则,新值将通过缓存加载并存储在缓存中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);
}

我希望在从缓存中逐出该值后不久刷新该值。目前,它将在 first 的下一个调用中刷新。如果在逐出后立即刷新该值,这将在以后为我节省1秒。getIndex()

我已经尝试过,但它也没有得到我想要的效果。该值将刷新,但该方法始终被执行,无论什么或是什么。@CachePutconditionunless

我现在看到的唯一方法是调用两次(第二次异步/非阻塞)。但这有点愚蠢。getIndex()


答案 1

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

因此,假设您在类路径上具有依赖性,则以下方面将起作用。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. 该代码假定 IndexService 位于包中。hello

答案 2

如下所示的内容可以以所需的方式刷新缓存,并使实现简单明了。

编写清晰简单的代码没有,只要它满足要求。

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