環境

ローカルキャッシュとRedisキャッシュについて考える前に、使用する技術や環境を記載する。

項目 環境
言語 Java 16
フレームワーク Spring Boot 2.5.2
ローカルキャッシュライブラリ caffeine 3.0.3
Redis 6.2.5

関連記事

RedisキャッシュをSpring Bootのインスタンス変数でさらにキャッシュしてリクエスト間で共有する

ローカルキャッシュのメリット

ローカルキャッシュがRedisキャッシュに比べて優れている点について考える。

  1. 高速(ネットワーク通信が不要だから)
  2. 省メモリ(同じメモリのデータを利用するため)

1の高速というのは当たり前だが、2の省メモリについては一見矛盾するため説明する。

省メモリ

JVM内でキャッシュ用にメモリを使うと、アプリケーションサーバーのメモリが枯渇しやすくなり、GCの頻度・時間や捌けるWebリクエスト数に影響が出る可能性がある。そのため、キャッシュ量が多ければRedis等の外部のキャッシュサーバーを利用する。

ただ、キャッシュするデータ量が(将来に渡って)あまり多くない場合は、アプリケーションサーバーのメモリを使用しても問題が発生しない。

そればかりか、逆にアプリケーションサーバーのメモリ使用率が下がりうる。

理由は、RedisキャッシュからJavaがデータを取得した場合、取得するたびに別のデータとしてメモリが使用されるのに対して、ローカルキャッシュであればJavaの呼び出し元に返されるのはキャッシュと同じメモリのデータのため。

同時に大量にリクエストがきた場合のアプリケーションサーバーのメモリ使用量を考えると、Redisキャッシュはリクエスト数 * キャッシュデータのバイト数であるのに対し、ローカルキャッシュはキャッシュデータのバイト数だけである。

実装

実際に動かして確かめてみる。以下にソースコードを記載するが、動作としてはリクエストをするたびにキャッシュされたデータとそのオブジェクトのidentityHashCodeを返す。同じオブジェクトが利用されていればidentityHashCodeの部分まで含めて何度リクエストしても同じ値が返るが、オブジェクトの実体が変わっていればリクエストごとに値が変わる。

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '16'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    //implementation group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.0.3'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

application.properties

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=null
spring.redis.database=0

Java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@EnableCaching
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @RestController
    public class Controller {

        private final CacheComponent cacheComponent;

        public Controller(CacheComponent cacheComponent) {
            this.cacheComponent = cacheComponent;
        }

        @GetMapping("/")
        String get() {
            String value= cacheComponent.get();
            return value + System.identityHashCode(value);
        }
    }

    @CacheConfig(cacheNames = "CacheComponent")
    @Component
    public class CacheComponent {
        @Cacheable(key="'get'")
        public String get() {
            System.out.println("not cached");
            return "s";
        }
    }

}

Redis

まずはRedisで動かすので、build.gradleでcaffeineをコメントアウトしておく。

$ redis-cli -h localhost keys \*
(empty array)

$ curl -w'\n' http://localhost:8080/
s776631558

$ redis-cli -h localhost keys \*
1) "CacheComponent::get"

$ curl -w'\n' http://localhost:8080/
s1320763264
$ curl -w'\n' http://localhost:8080/
s998148521

リクエストごとにオブジェクトの実体が変わっていることがわかる。

ローカルキャッシュ

次にcaffeineで動かすため、build.gradleを修正する。

    //implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: '3.0.3'

Spring Bootを再起動してリクエストする。

$ curl -w'\n' http://localhost:8080/
s1430265816
$ curl -w'\n' http://localhost:8080/
s1430265816
$ curl -w'\n' http://localhost:8080/
s1430265816

何度リクエストしてもオブジェクトの実体が変わらないことがわかる。

ローカルキャッシュのデメリット

実際の仕事でローカルキャッシュが使われることはあまりなく、Redisが使われることが多いとおもう。ローカルキャッシュには先ほど見てきたようなメリットがあるものの、以下のようなデメリットもある。

  1. キャッシュ量が多いとアプリケーションサーバーのメモリが足りなくなる
  2. アプリケーションサーバー間でキャッシュされたデータが異なる可能性がある

1については説明の必要がない。

2について説明する。通常、キャッシュには有効期限を持たせて一定時間が経つとキャッシュが切れるようにするが、キャッシュされるタイミングによってサーバー間で違うデータを持つ可能性がある。

あるユーザーがサーバー1号機にアクセスしてキャッシュされたデータを取得した後、同じページを再度表示したときにはサーバー2号機にロードバランシングされて違うデータを取得することになれば、混乱の元になる。例えば商品の一覧をキャッシュしていたとして一覧ページから商品詳細ページに遷移し、また一覧ページに戻るというのを繰り返すECサイトを例にとると、一覧の内容がリクエストの度に変わりかねず、UXが悪化する。

ローカルキャッシュを採用すべきとき

デメリットで見たようにUXの悪化につながりかねないので、キャッシュデータが変わる頻度やキャッシュデータの重要性を鑑みて、影響が少ないと思われる場合のみローカルキャッシュを利用した方がいい。そして有効期限もできるだけ短く設定すべきだろう。

また影響が少ないと言い切れたとしても、ローカルキャッシュのメリットを十分享受できるときでなければ、将来の仕様変更等で影響が大きくなる可能性を考慮して、ローカルキャッシュを採用しない方がいいだろう。

つまり、アプリケーションサーバーとRedisサーバー間のネットワーク通信を無くしたいほど高速性を追求したい、あるいは大量同時リクエストによるメモリ使用量を減らしたいといった要求があれば、使用を検討したい。