SpringBootでの複数キャッシュサーバがある場合の切り替え

エキサイト株式会社エンジニアの佐々木です。エキサイトホールディングスのカレンダー | Advent Calendar 2023 - Qiitaの9日目を担当させていただきます。SpringBootでキャッシュ先を複数にする場合の設定をご紹介します。

前提

下記、環境で動作検証をしています。

## Java
$ java --version
openjdk 17.0.9 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-17.0.9.8.1 (build 17.0.9+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.9.8.1 (build 17.0.9+8-LTS, mixed mode, sharing)

## Gradle
$ ./gradlew --version
------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.9 (Amazon.com Inc. 17.0.9+8-LTS)
OS:           Mac OS X 12.5 aarch64

## SpringBoot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)

モチベーション

サービスを提供するにあたって、RedisやMemcachedなどの複数キャッシュサーバを使うモチベーションですが下記が考えられます。

  1. トラフィックの増大によりキャッシュといえど負荷やレイテンシーが気になる
  2. セッション情報とコンテンツのキャッシュは分けたい

1. トラフィックの増大によりキャッシュといえど負荷やレイテンシーが気になる を解決する際にリモートキャッシュとローカルキャッシュを併用する場合が考えられます。SpringBootでは、設定ファイル(application.yml)に設定を書くことで単体のキャッシュサーバはすぐに動作するようになるのですが、キャッシュサーバが複数になると設定をコードで書く必要があるので、それをご紹介します。

コード

キャッシュ設定は下記のみになります。

@Configuration
public class CacheConfig {

    @Bean
    @Primary  // <- デフォルトの設定を明示するためにつけています
    public CaffeineCacheManager caffeineCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        Arrays.stream(CacheType.values()).forEach(e -> {
            Cache<Object, Object> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(e.getTtl())).build();
            cacheManager.registerCustomCache(e.getName(), cache);
        });
        return cacheManager;
    }


    @Bean(CacheLocalType.LOCAL_CACHE_MANAGER_NAME) // キャッシュ名を明示して定義します
    public CacheManager localCacheManager() {
        ConcurrentMapCacheManager concurrentMapCacheManager = new ConcurrentMapCacheManager();
        List<String> list = Arrays.stream(CacheLocalType.values()).map(e -> e.getName()).toList();
        concurrentMapCacheManager.setCacheNames(list);
        return concurrentMapCacheManager;
    }

}



// ここから下は、キャッシュキーをタイプセーフにしたいのでenumにて定義しているだけです
/**
* 通常キャッシュ用
*/
enum CacheType {

    SECOND_5(CacheType.SECOND_10_KEY, 10), SECOND_10(CacheType.SECOND_20_KEY, 20);

    public static final String SECOND_10_KEY = "SECOND_10";
    public static final String SECOND_20_KEY = "SECOND_20";

    @Getter
    private final String name;
    @Getter
    private final long ttl;

    CacheType(String name, long ttl) {
        this.name = name;
        this.ttl = ttl;
    }
}

/**
* ローカルキャッシュ用
*/

enum CacheLocalType {
    SECOND_5(CacheLocalType.FOREVER);

    public static final String FOREVER = "FOREVER";

    public static final String LOCAL_CACHE_MANAGER_NAME = "localCacheManager";

    @Getter
    private final String name;

    CacheLocalType(String name) {
        this.name = name;
    }
}

上記のように複数キャッシュの設定はあわせても20行以内のコードになります。各キャッシュサーバの設定をメソッド内で行うだけになります。下記は、使用例になります。SpringBootでキャッシュを使う場合は、アノテーションを使用したキャッシュ指定が便利です。@Cacheableは、キャッシュがあれば使用する、なければメソッドを実行し、結果をキャッシュに保存してくれる便利なアノテーションになります。

    @Cacheable(cacheNames = CacheType.SECOND_10_KEY)    // cacheManagerの設定がない場合は、@Primaryの設定が使用されます。
    @Override
    public String useMainCache() {
        return "MainCache created at:" + LocalDateTime.now();
    }

    @Cacheable(cacheNames = CacheLocalType.FOREVER, cacheManager = CacheLocalType.LOCAL_CACHE_MANAGER_NAME)  // cacheManagerの設定がある場合は、@Beanで設定したBean名の設定が使用されます。
    @Override
    public String useLocalCache() {
        return "LocalCache created at:" + LocalDateTime.now();
    }

上記のように@Cacheable などのアノテーションで指定したCacheManagerが採用されてキャッシュされます。宣言的でコードを汚さなくて済むので我々の事業部ではよく使用します。

まとめ

サービス規模が大きくなったり、トラフィックが増えるとこのように複数のミドルウェアを使用することがよくあります。ローカルキャッシュやリモートキャッシュを適宜使い分けができるとより様々な要件に対応できるようになるかと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com