4.2. キャッシュの抽象化(Cache Abstraction)

4.2.1. Overview

本ガイドラインでは、データ永続層のスケール性の確保 にて紹介しているキャッシュ方式の実現により、データアクセス処理の負荷低減および高速化する方法を紹介する。 キャッシュ方式の実現方法にはSpring Cache Abstractionを採用する。Spring Cache Abstractionによるキャッシュ実装の抽象化により、キャッシュ実装の置換を容易化したり、コーディング方法を統一化することによる習得コストの低減などが見込める。

セッションを外部管理化するためにキャッシュを利用する方法については、 セッション外部管理 を参照されたい。

Springのガイドについては、 Spring Cache Abstraction を参照されたい。

4.2.1.1. ローカルヒープを使用したキャッシュ

以下で、キャッシュ実装にローカルヒープ領域を使用した場合の処理の流れを示す。

../../_images/cache-abstraction.png
項番 説明
(1)
ControllerまたはServiceは、引数を渡しキャッシュ定義されたDomain LayerのServiceメソッドを呼び出す。
(2)

Cache AOPは、引数を元にキャッシュキーを特定しSimpleCacheManagerを使用してConcurrentHashMapから既に登録済のキャッシュデータを取得する。

キャッシュデータが取得出来た場合はControllerまたはServiceへキャッシュデータを返却し、キャッシュデータが取得出来ない場合は(3)および(4)を実行する。

(3)

Cache AOPは、引数を渡しキャッシュ定義されたDomain LayerのServiceメソッドを実行し戻り値を取得する。

Cache AOPは、(2)で特定したキャッシュキーで、取得した戻り値をSimpleCacheManagerを使用してConcurrentHashMapへキャッシュデータとして格納する。

(4)
Cache AOPは、ControllerまたはServiceへ取得した戻り値を返却する。

4.2.1.2. Redisを使用したキャッシュ

以下で、キャッシュ実装にRedisを使用した場合の処理の流れを示す。 キャッシュを行う仕組みはローカルヒープを使用したキャッシュと同一であるが、CacheManagerとしてRedisCacheManagerを使用する。

../../_images/cache-abstraction-redis.png
項番 説明
(1)
Cache AOPは、特定したキャッシュキーで、取得した戻り値をRedisCacheManagerを使用してRedisへキャッシュデータとして格納する。また、ControllerまたはServiceへ取得した戻り値を返却する。 Redisへのアクセスは、Spring Data Redisを使用して行われる。

4.2.1.3. キャッシュ方式の選択

本ガイドラインでは、ローカルヒープを使用したキャッシュとRedisを使用したキャッシュを紹介するが、各キャッシュ方式によって特徴が異なる。 そのため、アプリケーションの要件に応じて適したキャッシュ方式を選択されたい。

キャッシュ方式 特徴 適したアプリケーション
ローカルヒープを使用したキャッシュ
Redisを使用したキャッシュよりも高速であるが、APサーバインスタンスのローカルヒープにキャッシュを保持するため、サーバインスタンス間のキャッシュを同期することができない。また、キャッシュするデータ量に応じてサーバインスタンスのメモリ消費量が大きくなる。
キャッシュ対象とするデータがマスタデータ等の運用中に更新されることが想定されないデータに限定されるアプリケーションなどに適している。
Redisを使用したキャッシュ
キャッシュをRedisに保持するため、キャッシュデータを複数のサーバインスタンス間で同期することが可能である。また、キャッシュデータ量が大きくなってもサーバインスタンスのメモリ消費量が大きくなることはない。
キャッシュ対象とするデータ量が大きく、運用中の更新が見込まれるアプリケーションなどに適している。

Note

Spring Cache Abstractionを使用したキャッシュの実装では、複数のキャッシュ方式を併用することが可能である。 詳細は、複数のキャッシュマネージャを併用する を参照されたい。

4.2.2. How to use

以下でSpring Cache Abstractionの利用にあたり、事前に必要な設定、およびアプリケーションでキャッシュデータへアクセスする方法を説明する。

4.2.2.1. Spring Cache Abstractionの設定

4.2.2.1.1. ローカルヒープを使用したキャッシュの設定

キャッシュの機能を有効にするには、キャッシュマネージャーの設定が必要になる。 以下に、ローカルヒープを使用したキャッシュマネージャーの設定例を示す。

・・・
<!-- (1) -->
<cache:annotation-driven order="-1" />
・・・
<!-- (2) -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <!-- (3) -->
    <property name="caches">
        <set>
            <bean
                class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
                <property name="name" value="members" />
            </bean>
            ・・・
        </set>
    </property>
</bean>
項番 説明
(1)

アノテーションでのキャッシュを有効にする。

キャッシュデータの管理をするためorder="-1"を設定し、キャッシュインタセプターがトランザクションインタセプターより先に動作する設定とする。これにより、キャッシュデータの参照はトランザクション開始前に、登録と削除はトランザクションの終了後に行う。

(2)
キャッシュデータの格納場所にローカルヒープ領域を使用する場合は、Springが提供するSimpleCacheManagerをキャッシュマネージャとして使用する。
(3)
cachesプロパティに、実際にキャッシュデータを格納する「入れ物(Cache)」をBean定義する。SimpleCacheManagerを使用する場合は、後述する@CacheConfigアノテーションに対応した数だけBean定義が必要になる。「入れ物」の実装にJDK標準のConcurrentHashMapを使用する場合はConcurrentMapCacheFactoryBeanを使用する。@CacheConfigとの関連付けのため、nameプロパティには@CacheConfigcacheNamesに指定するキャッシュ名を設定する

Note

ローカルヒープ領域における「入れ物」の実装は、ConcurrentMapCacheFactoryBean以外のものもSpringに用意されている。 詳細は Springのリファレンス Configuring the cache storage を参照されたい。

4.2.2.1.2. Redisを使用したキャッシュの設定

以下に、Redisを使用したキャッシュマネージャーの設定例を示す。 RedisへのアクセスはSpring Data Redisを使用するが、セッション外部管理 で説明したspring-boot-starter-data-redisへの依存関係が追加されていることが前提となる。

・・・
<!-- (1) -->
<cache:annotation-driven order="-1" />
・・・
<!-- (2) -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager" c:redisOperations-ref="redisTemplate" />
項番 説明
(1)
アノテーションでのキャッシュを有効にする。ローカルヒープを使用したキャッシュと同様にorder="-1"を設定する。
(2)

キャッシュデータの格納場所にRedisを使用する場合は、Spring Data Redisが提供するRedisCacheManagerをキャッシュマネージャとして使用する。 RedisCacheManagerは、spring-boot-starter-data-redisによるAutoConfigurationにて設定されるRedisTemplateを使用してRedis接続を行う。

Spring Data Redisの設定方法については、 Spring Data Redisの設定 を参照のこと。

4.2.2.2. キャッシュするデータの選択

以下にキャッシュしたいデータを定義する方法を説明する。 Spring Cache Abstractionでは、メソッドにアノテーションを定義することでキャッシュ対象データを選択する方式をとる。 対象メソッドの戻り値がキャッシュ対象のデータとなる。

// omitted...
// (1)
@CacheConfig(cacheNames = "members")
public class MemberUpdateServiceImpl implements MemberUpdateService {
  // omitted...
  @Transactional(readOnly = true)
  // (2)
  @Cacheable(key = "'member/' + #customerNo")
  public Member findMember(String customerNo) throws IOException {
    // omitted...
  }
  // omitted...
}
項番 説明
(1)

CacheConfigアノテーションをクラスへ付与する。

このクラス内のキャッシュアノテーションの属性cacheNamesを設定する。

(2)

Cacheableアノテーションをキャッシュ対象の参照メソッドへ付与する。

属性key(キャッシュキー)を設定する。この例では、文字列引数(customerNo)の値にプレフィックス’member/’を付けてキーにしている。例えば customerNo=000001 の場合、キャッシュキーは「member/000001」となり、キャッシュされる値はメソッドの戻り値となる。

Warning

Spring Cache Abstractionのアノテーションを使用する場合は、@Cacheable@CachePut@CacheEvictアノテーションの属性 value (または cacheNames )の値は、Spring frameworkがキャッシュオペレーション作成時に必須の値となるため設定すること。

また、インタフェースにSpring Cache Abstractionのアノテーションを付与することは基本的に推奨していない。理由としては、インタフェースのメソッドの引数名はデフォルトでは取得できないためである。この制約に対しSpringは代替手段として、インタフェースしか実装しない場合(Proxyとなるインタフェース、例えばDynamoDBのリポジトリ等)にメソッドの@Cacheableを付与する際は、メソッド引数のインデックスを指定することで引数へのアクセスを実現することが可能である。

以下に、インデックス指定の例を示す。

@CacheConfig(cacheNames = "shardids")
@EnableScan
public interface AccountShardKeyRepository
                                        extends
                                        CrudRepository<ShardingAccount, String> {
  @Override
  // (1)
  @Cacheable(key = "'shardid/' + #a0")
  ShardingAccount findOne(String id);
}
項番 説明
(1)

Cacheableアノテーションの属性keyで設定している、#a0がメソッドfindOneの引数0番目(id)を指定している。

詳細は Springのリファレンス Available caching SpEL evaluation context を参照されたい。

4.2.2.3. キャッシュしたデータの削除

キャッシュデータは、対象データが更新または、削除された場合にキャッシュデータの削除が必要になる。

以下にキャッシュしたデータを削除する定義方法を説明する。 Spring Cache Abstractionでは、メソッドにアノテーションを定義することでキャッシュ対象データを削除する方式をとる。 メソッドで定義したアノテーションキーが削除対象データのキーとなる。

// omitted...
// (1)
@CacheConfig(cacheNames = "members")
public class MemberUpdateServiceImpl implements MemberUpdateService {
  // omitted...
  @Transactional(readOnly = true)
  // (2)
  @Cacheable(key = "'member/' + #customerNo")
  public Member findMember(String customerNo) throws IOException {
    // omitted...
  }
  // (3)
  // omitted...
  @CacheEvict(key = "'member/' + #member.customerNo")
  public void updateMember(Member member) {
    // omitted...
  }
}
項番 説明
(1)
CacheConfigアノテーションをクラスへ付与する。
(2)
キャッシュするデータの選択で説明したキャッシュデータを登録または参照するメソッド定義。
(3)

CacheEvictアノテーションをキャッシュ対象の更新メソッドへ付与する。

属性key(キャッシュキー)を設定する。この例では、引数であるMemberオブジェクトのフィールド(customerNo)の値にプレフィックス’member/’を付けてキーにしている。例えば customerNo=000001 の場合、キャッシュキーは「member/000001」となり、(2)でキャッシュされたキーを同じになるため、(2)でキャッシュされた値を削除する。

Warning

トランザクショナルなDBの値をキャッシュデータにしている場合は、DBの値更新時に完全なデータの同期が出来ない事に注意が必要である。

DBの値が更新され、コミットされてからキャッシュデータが削除されるまでの間のデータ参照は古いキャッシュデータが参照される。

4.2.2.4. 注意事項

  • ローカルヒープ領域を利用した場合は、キャッシュが共有される範囲は同一のDIコンテナ内のみである。
  • 特にローカルヒープ領域をキャッシュ格納場所に使用する場合は、キャッシュ対象データのサイズに注意すること。ヒープサイズに見合わない量のデータをキャッシュした場合、パフォーマンスが低下したりメモリ不足に陥る可能性がある。そのような場合には、ローカルヒープ領域外を格納場所として使用するなどを検討すること。
  • 本ガイドラインで説明しているセッションの外部管理の方法と、Redisを使用したキャッシュの設定 にて説明した方法でRedisを使用したキャッシュを併用する場合、接続先のRedisは同一のRedisとなる。高負荷での運用が想定されるアプリケーションについては、セッション情報とキャッシュを格納するRedisを別にすることでコネクション数の枯渇を回避することが出来る。キャッシュ格納用のRedisの設定方法については、 目的別に接続先Redisを指定する を参照。

4.2.3. How to extend

4.2.3.1. 目的別に接続先Redisを指定する

高負荷での運用が想定される場合等、セッションの外部管理やキャッシュの格納等の複数の目的でRedisを使用する際に、利用目的別に接続先のRedisを別にすることが望ましいケースがある。

以下に、キャッシュ格納用のRedisをSpring Bootデフォルトのプロパティキーによる指定とは別に設定する方法を示す。 Spring Data Redisを使用してRedisへのアクセスを行う場合、通常ではSpring Bootデフォルトのプロパティキーを使用して接続情報を指定するが、キャッシュ格納用のRedisを別に指定する場合はここで紹介する方法で接続設定を行うことが可能である。

Spring Bootデフォルトのプロパティキーについては、Spring Boot Reference Guideの Common application propertiesの# REDIS (RedisProperties)を参照されたい。

・・・
<cache:annotation-driven order="-1" />
・・・
<!-- (1) -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
    <constructor-arg ref="redisTemplateForCache" />
</bean>

<!-- (2) -->
<bean id="redisTemplateForCache" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="jedisConnectionFactoryForCache" />

<!-- (3) -->
<bean id="jedisConnectionFactoryForCache"
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <constructor-arg index="0">
        <!-- (4) -->
        <bean
            class="org.springframework.data.redis.connection.RedisClusterConfiguration">
            <constructor-arg>
                <list>
                    <value>${redis.cache.cluster.node1}</value>
                    <value>${redis.cache.cluster.node2}</value>
                    <value>${redis.cache.cluster.node3}</value>
                </list>
            </constructor-arg>
        </bean>
    </constructor-arg>
    <constructor-arg index="1">
        <!-- (5) -->
        <bean class="redis.clients.jedis.JedisPoolConfig">
            <property name="maxTotal" value="${redis.cache.maxTotal}" />
            <property name="maxIdle" value="${redis.cache.maxIdle}" />
            <property name="maxWaitMillis" value="${redis.cache.maxWaitMillis}" />
            <property name="minIdle" value="${redis.cache.minIdle}" />
        </bean>
    </constructor-arg>
</bean>

<!-- (6) -->
<bean id="jedisConnectionFactoryForSession" primary="true"
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <constructor-arg index="0">
        <bean
            class="org.springframework.data.redis.connection.RedisClusterConfiguration">
            <constructor-arg>
                <list>
                    <value>${redis.session.cluster.node1}</value>
                </list>
            </constructor-arg>
        </bean>
    </constructor-arg>
    <constructor-arg index="1">
        <bean class="redis.clients.jedis.JedisPoolConfig">
            <property name="maxTotal" value="${redis.session.maxTotal}" />
            <property name="maxIdle" value="${redis.session.maxIdle}" />
            <property name="maxWaitMillis" value="${redis.session.maxWaitMillis}" />
            <property name="minIdle" value="${redis.session.minIdle}" />
        </bean>
    </constructor-arg>
</bean>
項番 説明
(1)
キャッシュマネージャとしてRedisCacheManagerを使用する。参照するRedisTemplateをAutoConfigurationにて設定されるものではなく、独自にBean定義したものを設定する。
(2)
RedisTemplateのBean定義を行う。コネクションファクトリの参照設定を行う。AutoConfigurationによるBean定義との重複を避けるため、idを”redisTemplate”とは異なる文字列とする。
(3)

JedisConnectionFactoryを使用したコネクションファクトリのBean定を行う。コンストラクタにRedisClusterConfigurationJedisPoolConfigを指定し、接続先のRedisクラスタの指定および接続設定を行う。AutoConfigurationによるBean定義との重複を避けるため、idを”jedisConnectionFactory”とは異なる文字列とする。

Note

JedisConnectionFactoryのコンストラクタにRedisClusterConfigurationを指定する場合、接続先のRedisはクラスタ構成であることが必須となる。スタンドアローン構成のRedisに接続する場合はJedisConnectionFactoryに直接接続先のServerとPortを指定すること。 指定方法については、Spring Data Redisのリファレンス を参照されたい。

(4)
RedisClusterConfigurationのBean定義を行う。コンストラクタの引数に接続先のノードを指定する。
(5)

JedisPoolConfigのBean定義を行う。JedisPoolConfigに設定するプロパティとSpring Bootデフォルトのプロパティキーとの対応関係は以下のとおり。

  • maxTotal : spring.redis.pool.max-active
  • maxIdle : spring.redis.pool.max-idle
  • maxWaitMillis : spring.redis.pool.max-wait
  • min-idle : spring.redis.pool.minIdle
(6)
RedisConnectionFactoryインターフェースの実装クラスをBean定義すると、AutoConfigurationによるRedisConnectionFactoryのBean定義が無効となる。そのため、Spring Sessionがセッション情報をRedisに格納するために使用するJedisConnectionFactoryのBean定義を行う。primary=”true”を指定し、Spring SessionがRedisTemplateを生成する際に優先的に使用させる。

プロパティキーに対応する値の設定を行う。

redis:
  cache:
    cluster:
      # (1)
      node1:
        127.0.0.1:30001
      node2:
        127.0.0.1:30002
      node3:
        127.0.0.1:30003
    # (2)
    maxTotal: 8
    maxIdle: 8
    maxWaitMillis: -1
    minIdle: 0
  session:
    cluster:
      # (3)
      node1:
        127.0.0.1:30004
      node2:
        127.0.0.1:30005
      node3:
        127.0.0.1:30006
    # (4)
    maxTotal: 8
    maxIdle: 8
    maxWaitMillis: -1
    minIdle: 0
項番 説明
(1)
キャッシュを格納するRedisの接続先ノードを指定する。
(2)
キャッシュを格納するRedisのJedisPoolConfigに設定するプロパティを指定する。
(3)
セッションを格納するRedisの接続先ノードを指定する。
(4)
セッションを格納するRedisのJedisPoolConfigに設定するプロパティを指定する。

Note

本ガイドラインでは、Spring Bootデフォルトのプロパティキーとは別に設定する方法について、一部のプロパティについて紹介した。より詳細なカスタマイズを行いたい場合は、 RedisProperties および RedisAutoConfiguration の実装を参照し、設定を検討されたい。

4.2.3.2. 複数のキャッシュマネージャを併用する

Spring Cache Abstractionでは、複数のキャッシュマネージャを別のBean名で定義しておき、@CacheableアノテーションのcacheManager属性に指定することで、キャッシュ対象データ毎に使用するキャッシュマネージャを指定することが可能である。 詳細は、Custom cache resolution を参照されたい。