web-dev-qa-db-ja.com

Spring Cacheが古い値を更新する

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()を2回呼び出すことです(2回目は非同期/非ブロッキング)。しかし、それは一種の愚かです。

16
lexicore

必要なことを行う最も簡単な方法は、すべての魔法を透過的に実行し、より多くの場所で再利用できるカスタムアスペクトを作成することです。

したがって、クラスパスに_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();
            }
        }
    }
}
_

注意すべきいくつかのこと

  1. getIndex()メソッドにはパラメーターがないため、キー_SimpleKey.EMPTY_のキャッシュに保存されます
  2. このコードは、IndexServiceがhelloパッケージに含まれていることを前提としています。
6
Babl

次のようなものは、希望する方法でキャッシュをリフレッシュし、実装を単純かつ簡単に保つことができます。

要件を満たしていれば、明確でシンプルなコードを書くことについて間違ったはありません。

@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
Thomas Kabassis

編集1:

この場合、_@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;
}
_

最初の構成は同期化されており、最初の呼び出しがキャッシュに追加されるまで待機するすべての今後の呼び出しをブロックするだけです。

次に、システムはキャッシュを再生成する必要があるかどうかをチェックします。はいの場合、値の非同期更新の単一の呼び出しが呼び出され、現在のスレッドがキャッシュされた値を返します。キャッシュが再計算の状態になると、次の呼び出しは単にキャッシュから最新の値を返します。等々。

このようなソリューションを使用すると、大量のメモリを再利用できます。たとえば、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のキャッシングアノテーション抽象化を再利用します。

オリジナル:なぜこれをキャッシングで解決しようとしているのですか?これが単純な値(キーベースではない)の場合、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;
        }
    }
}
_

つまり、最初の呼び出しは同期され、結果が入力されるまで待つ必要があります。その後、保存された値が廃止された場合、システムは値の非同期更新を実行しますが、現在のスレッドは保存された「キャッシュされた」値を受け取ります。

格納されたインデックスの単一のアップグレードを制限するために、リエントラントロックも導入しました。

0
Ilya Dyoshin

それは次のようなものになると思います

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

以下のコードサンプルに示すように、インデックスサービスで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;
         }
       }
     });

Guavaのメソッドを呼び出すことにより、非同期リロードキャッシュローダーを作成できます。

public abstract class CacheLoader<K, V> {
...

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

トリックは、たとえばThreadPoolExecutorを使用して、別のスレッドで再ロード操作を実行することです。

  • 最初の呼び出しでは、キャッシュはload()メソッドによって生成されるため、応答に時間がかかる場合があります。
  • 後続の呼び出しでは、値を更新する必要がある場合、古い値を提供しながら非同期的に計算されます。更新が完了すると、更新された値が提供されます。
0
Jerome L