Spring Bootで@CacheEvictを使ってキャッシュを削除する

こんにちは、エキサイト株式会社の平石です。

今回は、Spring Bootで一度作成したキャッシュをTTLが過ぎる前に明示的に削除する方法をご紹介します。

キャッシュを削除したいとき

キャッシュという仕組みでは、DBなどの情報源にアクセスした結果を高速にアクセスできる領域に保存しておきます。
そして、同じアクセスが来たときにもう一度情報源に問い合わせるのではなく、結果を保存した領域からデータを取得します。
このようにすることで、情報源の負荷を軽減したり、レスポンスを高速にしたりすることができます。

便利な仕組みですが、DB等内の情報が更新されてもキャッシュが削除されるまでは同じデータを返し続けてしまうという欠点もあります。
特に、普段は滅多に更新されないデータは、TTL(キャッシュを保持する期間)を長めにしておくことが多く、その場合情報源での更新がなかなか反映されません。

このような場合に対応するために、キャッシュを明示的に削除する方法をご紹介します。

なお、今回はデータへの変更を加える操作もJavaで行われるという前提で使える方法をご紹介します。

環境

今回のブログにおけるソースコードはSpring Boot v3.2.2, Java 21で動作確認をしています。

以下のような、ニュース記事のIDとその記事のタイトルを管理する場合を考えます。
なお、例をシンプルにするためDB等は使わずにデータはハードコーディングしています。

@Service
@RequiredArgsConstructor
public class NewsArticleServiceImpl implements NewsArticleService {

    private final Map<Integer, String> newsArticleTitleMap = new HashMap<>() {
        {
            put(1, "〇〇株式会社が上場");
            put(2, "〇〇地方で大雪");
            put(3, "〇〇が電撃結婚");
        }
    };

    @Override
    @Cacheable(cacheNames = "newsCache")
    public String putCache(final Integer newsId) {
        return newsArticleTitleMap.getOrDefault(newsId, "");
    }
}

putCacheメソッドをnewsId引数と共に呼ぶと、対応するnewsIdを持つ記事のタイトルが返され、設定した「データ領域」にキャッシュが行われます。(詳細は省きますが、今回は裏でRedisを使っています。)

テストのために、3つの記事タイトルのキャッシュをするためのエンドポイントを用意して、呼び出してみます。

@RestController
@RequestMapping("cache/sample")
@RequiredArgsConstructor
public class CacheSampleController {
    private final NewsArticleService newsArticleService;

    @GetMapping("put")
    public void putCache() {
        newsArticleService.putCache(1);
        newsArticleService.putCache(2);
        newsArticleService.putCache(3);
    }
}

valueはSerializeされ、かつ一覧では一部しか表示されていませんが、確かにキャッシュがされているようです。
keyで、newsCache::の後にある数字はnewsIdで、以後例えばnewsId=1でアクセスがあった場合には、newsId::1のエントリからデータを取得してメソッドの返り値として返します。

@CacheEvictでキャッシュを削除する

それでは、キャッシュを削除してみます。
キャッシュを削除するには、@CacheEvictというアノテーションを利用します。
@Cacheableを利用するために追加する'org.springframework.boot:spring-boot-starter-cache'と同じライブラリに含まれているため、追加で依存関係を設定する必要はありません。

基本的な使用法

@Service
@RequiredArgsConstructor
public class NewsArticleServiceImpl implements NewsArticleService {

    〜〜 略 〜〜

    @Override
    @Cacheable(cacheNames = "newsCache")
    public String putCache(final Integer newsId) {
        return newsArticleTitleMap.getOrDefault(newsId, "");
    }

    @Override
    @CacheEvict(cacheNames = "newsCache")
    public void deleteCache(final Integer newsId) {
    }
}

@CacheEvictcacheNamesに削除したいキャッシュのキー名を指定します。
newsId=1deleteCacheメソッドを呼び出すとnewsId::1のエントリが削除されます。

キャッシュを全て削除する

先ほどは、メソッドの引数newsId=1に対応するエントリのみが削除されました。
メソッドの引数に関わらず、newsCacheのすべてのエントリを削除したい場合には、allEntries = trueを指定します。

    @Override
    @CacheEvict(cacheNames = "newsCache", allEntries = true)
    public void deleteCache(final Integer newsId) {
    }

メソッドの引数とkeyが異なる場合

次に、ニュース記事のタイトルを変更する以下のようなメソッドを考えます。
(正確には、すでにタイトルが登録されている場合には更新し、登録されていない場合は新たに追加するメソッドですが。)

    @Override
    @CacheEvict(cacheNames = "newsCache")
    public void updateNewsArticleTitle(final Integer newsId, final String title) {
        newsArticleTitleMap.put(newsId, title);
    }

この時、newsId=2を指定したとしてもnewsCache::2のエントリは削除されません。
なぜなら、@Cacheable@CacheEvictなどのアノテーションは、デフォルトですべての引数をキャッシュに含めようとするためです。
この場合は、newsCache::2,{title引数で指定した文字列}でキャッシュを削除しに行こうとします。
当然、そのようなエントリは存在しません。

titleの値に関わらずキャッシュを削除するには、以下のようにkey属性を指定します。

    @Override
    @CacheEvict(cacheNames = "newsCache", key = "#newsId")
    public void updateNewsArticleTitle(final Integer newsId, final String title) {
        newsArticleTitleMap.put(newsId, title);
    }

keyには、キャッシュのキーに利用する「パラメータや値」を指定します。
複数のパラメータを指定する場合には、"{#newsId, #title}"のように指定できます。

この状態で、newsId=2, title="〇〇地方で大雨"で、updateNewsArticleTitleメソッドを呼び出すと、newsCache::2のエントリが削除されます。

おわりに

キャッシュをする場面と比較して、キャッシュを明示的に削除したい場面は少ないかもしれません。 しかし、もし必要になった場合には、利用してみてください。

では、また次回。