В приложении Spring у меня есть служба, которая выполняет вычисление некоторого Index
. Index
относительно дорого рассчитать (скажем, 1 с), но относительно дешево проверить на актуальность (скажем, 20 мс). Фактический код не имеет значения, он имеет следующий вид:
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()
. Если бы значение было обновлено сразу после выселения, это спасло бы меня спустя 1 секунду.
Я пробовал @CachePut
, но это также не дает мне желаемого эффекта. Значение обновляется, но метод всегда выполняется, независимо от того, что condition
или unless
.
Единственный способ, который я вижу в данный момент, - дважды позвонить getIndex()
(второй раз асинхронно/неблокировать). Но это глупо.