SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

SpringBootでSpringのキャッシュ機構/ConcurrentHashMap版。STS3+SpringBoot

Springには、非常に優れたChache機構があります。 

今回はこれを使ってみます。

なお、STS3(3.9.6)+SpringBoot2.0+tymeleaf3.0迄動作確認しています。

f:id:arakan_no_boku:20190222012501j:plain

 

キャッシュの利用設定

 

SpringBootスタータープロジェクトで、キャッシュにチェックをつけてプロジェクトを作るとキャッシュ機構が組み込まれます。 

あとで追加するなら、pom.xmlに以下のように書きます。(Mavenの場合のみ)

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

ちゃんと使うなら、外部にキャッシュの格納先を用意した方がより良いみたいですです。 

でも、なくても動かすことはできます。 

ConcurrentHashMapを使うやり方です。 

まず、そちらのやり方をやってみます。

 

CacheManager

 

キャッシュを使うには、CacheManagerが必要です。 

XxxxxApplication.java(Xxxxxはプロジェクト名)がおいてあるのと同じ場所に以下のクラスを作ります。

@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public CacheManager cacheManager() {
         return new ConcurrentMapCacheManager("names");
    }
}

上記の「names」というのがキャッシュの名前になります。 

任意の名前に変更してもらっても大丈夫です。  

後続の処理で、キャッシュを使うときには、この名前を指定することで、どのCacheManagerを使うのかを識別します。  

このクラスを追加することで、Springのキャッシュ機構が使えるようになります。

 

@Cacheableアノテーション

 

@Cacheableアノテーションを使います。

引数として以下を指定します。

  • value="CacheManager生成時につけた名前"
  • key="#キーにしたい引数名"

引数をすべてキャッシュのキーにする場合は、key=を省略して、例えば「@Cacheable("names")」のようにキャッシュ名のみを指定して使うこともできます。 

ただ、あとででてくるキャッシュの更新などを考えると、ここで明示的にkey=を宣言しておいた方が、ソースは見やすくなります。 

例です。

@Cacheable(value="names",key="#uid")
public String getUserName(final String uid){
    Record2<String,String> result = selectUserName(uid);
    if(result == null){
        // nullではなく、""を返すと間違ってキャッシュされてしまうので注意
         return null;
    }else{
        return result.getValue(USER_INFO.UNAME);
    }
}

selectUserName(uid)というメソッドで、uidをキーにしてDBから名前を含んだレコードをとってきていると思ってください。 

DBから名前が取得できなかった場合は「null」を返し 、取得できれば名前を返してます。 

このreturnした名前が、uidに指定されたキー文字列でキャッシュされて、2度め以降の呼び出しで、ヒットすればキャッシュから名前を変換し、メソッドを実行しないという動き方をします。 

ポイントは、取得できなかった時に「null」を返すところです。 

「null」はreturnしてもキャッシュ機構に無視されます。 

だから、エラーになった時に変な値がキャッシュに残ってしまうという事故を防げます。 

安易に、null以外の値を返してしまうと、最初の処理で失敗したら、それがキャッシュされて後はひたすら、エラー文字列ばかりが返されることになってしまいますので、注意したほうが良いです。 

 

動作確認

 

とりあえず、今までの部分をテストしてみます。 

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestUserCache {

    @Autowired
    UserJooqService us;

    @Test
    public void testCache() {

        //nullが返るケースでキャッシュされないのを確認
            System.out.println(us.getUserNameNoCache("Test001"));
        // データを作成
            us.insertUserInfo("Test001", "氏名その一〇", "カナメイイチ", "Test01@mail.jp", "06-333-1231");

        // キャッシュしないケースの時間計測を開始

            StopWatch timer = new StopWatch();
            timer.start();

        // キャッシュしないケース。
            for(int i = 0;i < 10;i++){
                System.out.println(us.getUserNameNoCache("Test001"));
            }

       // キャッシュしないケース。経過時間出力
            timer.stop();
            System.out.println("No Cache:" + timer.getTotalTimeSeconds() + "s");

       // キャッシュの一回目は計測から外す

            us.getUserName("Test001") ;

       // キャッシュするケースの時間計測開始

            StopWatch timer2 = new StopWatch();
            timer2.start();

       // キャッシュするケース
            for(int j = 0;j < 10;j++){
                System.out.println(us.getUserName("Test001"));
            }

        // キャッシュするケースの結果出力
            timer2.stop();
            System.out.println("Cacheable:" + timer2.getTotalTimeSeconds() + "s");

     }

時間計測には、SpringのStopWatchクラスを使います。

www.programcreek.com

 

実行結果

 

何回か、実行した結果は以下のような感じです。

  • No-Cache:最頻値 概ね 0.070秒前後
  • Cache  :最頻値 概ね 0.002 秒位

キャッシュが効いてる感じがしますね。 

これだけなら、超簡単に高速化できて、非常にラッキー・・なのですが、残念ながら、そうは問屋がおろしません。

 

キャッシュの落とし穴 

 

キャッシュに値が保存されると、メソッド呼び出し時にキャッシュを優先して、メソッドを実行しない・・ここに落とし穴があります。 

だって、データは途中で更新されることもありますし、削除されることもありますから。 

それでも、何も手をうたないとキャッシュに残っている限り、いくらメソッドを呼び出しても新たなデータを取得する処理は動かず、ずっと、変更前の値を表示し続けるわけです。 

だから、データが更新された時、データが削除された時には、忘れないようにキャッシュも新しい値に更新したり、キャッシュから削除したりする必要があります。 

それをおこなうアノテーションが、「@CachePut」(更新する)と、「@CacheEvict」(削除する)です。 

 

キャッシュの該当キーの値を強制的に更新する

 

@CachePutアノテーションを使います。

引数として以下を指定します。

value="CacheManager生成時につけた名前"
key="#キーにしたい引数名"

@CachePut(value="names",key="#uid")
public String setUserName(final String uid,final String uname){

    if(StringUtils.isEmpty(uid) || StringUtils.isEmpty(uname)){
        return null;
    }else{
        int ret =         jooq.update(USER_INFO).set(USER_INFO.UNAME,uname).where(USER_INFO.UID.eq(uid)).execute();
        if(ret > 0){
            return userNameCachePut(uid);
        }else{
            return null;
        }
    }
}

新しい名前でDBを更新し、更新が成功したら、変更後の名前をDBからSelectして返しています。 

userNameCachePut(uid);は、こんな感じの処理です。

public String userNameCachePut(final String uid){
    Record2<String,String> result = selectUserName(uid);

    if(result == null){
        // nullを返すことに注意
            return null;
    }else{
            return result.getValue(USER_INFO.UNAME);
    }
}

@CachePutはキャッシュを有効にしたまま、値だけ更新してくれるので非常に有効です。 

実行ログの例です。

氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
氏名その一〇
Cacheable:0.002s

※ここで@CachePutアノテーション付与の処理実行
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
氏名置き換え後
Cacheable:0.002s

置き換え前がキャッシュがきいた状態で0.002秒、キャッシュの値を更新した後の結果も、0.002秒です。 

値の更新が効率に全く影響を与えていないのがわかると思います。

 

キャッシュから該当キーの値を削除する。

 

@CacheEvictアノテーションを使います。

引数として以下を指定します。

value="CacheManager生成時につけた名前"
key="#キーにしたい引数名" 

@CacheEvict(value="names",key="#uid")
public boolean deleteUserInfo(final String uid){

    int ret =     jooq.deleteFrom(USER_INFO).where(USER_INFO.UID.eq(uid)).execute();
    if(ret > 0){
        return true;
    }else{
        return false;
    }

}

 

この処理では、指定キーのレコードを削除しています。 

この@CacheEvictの場合は、戻り値は別にvoidでも良いみたいなのですが、とりあえずtrueを返してます。 

@Cacheableと違って、更新や削除の場合は、キャッシュしているキー以外の引数が必要になる場合がほとんどだと思います。 

でも、例えば、@CachePutを付与したメソッドを外部で定義して、更新処理の中で呼び出してもキャッシュは更新されません。 

だから、必ず「 @CachePut(value="names",key="#uid") 」のように、keyでキャッシュのキーを指定する書き方になります。 

なので、@Cacheableも面倒でも簡易的な書き方をしないで、同じように記述していると、あとでとてもわかりやすいです。 

絶対ではないですが、個人的にはオススメです。 

 

まとめ

 

Springのキャッシュには、他にもたくさん機能がありますが、概ね、この3つがちゃんと使えれば、あまり困りません。 

それでも、他の機能を調べる必要がある時は、例えば、こちらが参考になるかと思います。

www.baeldung.com