LoginSignup
29
4

More than 1 year has passed since last update.

Spring BootでElasticache for Memcachedのキャッシュ操作をしてみた

Last updated at Posted at 2022-12-06

この記事は アイスタイル Advent Calendar 2022 7日目の記事です。

こんにちは。アイスタイルにて会員基盤の運用保守や機能改修を担当しているyukawacです。

Spring BootElasticache for Memcachedのキャッシュ操作をする機会があったので、
キャッシュ操作するまでに実施したことやコード例を書きます。

⚠️注意⚠️
※本記事ではElasticache for Memcachedへ疎通するためのネットワーク周りの設定については記載しません。
※ローカル環境にて、JavaとDockerの実行環境は構築済みとします。
※ElastiCache for Memcachedクラスターは作成済みとします。

一連の流れ

  1. ローカル環境でmemcachedを立ち上げる(dockerコンテナ)
  2. Spring Bootでローカル環境のmemcachedのキャッシュ操作をするコードを作成する
  3. Elasticache for Memcachedへ接続できるように設定を書き換える

筆者のローカル環境

  • OS : Windows10
  • IDE : IntelliJ IDEA 2021.3.1
  • Docker環境 : Rancher Desktop 1.6.2
  • OpenJDK : Amazon Corretto 17

それでは、進めます。

1. ローカル環境でmemcachedを立ち上げる(dockerコンテナ)

いきなりElastiCache for Memcachedへ接続はせず、
ローカル環境構築も兼ねて、まずはローカル環境でmemcachedを立ち上げ、接続を試みました。
memcached公式イメージを利用します。
ローカル環境でdockerコマンドを実行し、memcachedを立ち上げます。

# 初回起動
docker run --name demo-memcached --env TZ=Asia/Tokyo -d -p 11211:11211 memcached:latest

telnetコマンドにて接続確認できます。

telnet localhost 11211

※ちなみに、停止と2回目以降の起動コマンドは下記です。

# 停止
docker stop demo-memcached
# 2回目以降の起動
docker start demo-memcached

2. Spring Bootでローカル環境のmemcachedのキャッシュ操作をするコードを作成する

2-1. Spring Bootプロジェクトを作成する

Spring InitializrでSpring Bootプロジェクトを生成します。
Spring Initializr

主要なものは

  • Spring Boot:2.7.6 (作成時の最新2系)
  • Java:17
  • Spring Web (動作確認デモ用に追加)
  • Lombok (動作確認デモ用に追加)

を選択しました。

「GENERATE」をクリックしてdemo.zipをダウンロードし、展開します。
展開したプロジェクトはIntelliJ IDEAで開きます。

2-2. Memcachedクライアントライブラリの依存関係をbuild.gradleに記述しダウンロードする

JavaのMemcachedクライアントはAmazon ElastiCache Cluster Clientを利用します。
その他のクライアントライブラリでもキャッシュ操作することはできますが、
AWSのElastiCache for Memcachedの機能

等を利用するためにはAWSのライブラリの利用が必要となります。

build.gradleファイルのdependenciesに下記を追加し、ダウンロードします。

implementation 'com.amazonaws:elasticache-java-cluster-client:1.2.0'

2-3. Cacheインターフェースを実装する

キャッシュ操作のためのCacheインターフェースを実装します。
今回は最低限、@Cacheableを利用したキャッシュの登録、取得が操作できるような実装例を記載します。

Cacheインターフェースの実装例
Memcached.java
package com.example.demo;

import java.util.concurrent.Callable;
import lombok.extern.slf4j.Slf4j;
import net.spy.memcached.MemcachedClient;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;

@Slf4j
public class Memcached implements Cache {

  private final String name;
  private final MemcachedClient memcachedClient;

  public Memcached(String name, MemcachedClient memcachedClient) {
    this.name = name;
    this.memcachedClient = memcachedClient;
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public Object getNativeCache() {
    return this.memcachedClient;
  }

  @Override
  public ValueWrapper get(Object key) {
    Object value = this.memcachedClient.get(key.toString());
    if (value == null) {
      log.debug("cache miss for key: " + key); // 動作確認用DEBUGログ
      return null;
    }
    log.debug("cache hit for key: " + key); // 動作確認用DEBUGログ
    return new SimpleValueWrapper(value);
  }

  @Override
  public <T> T get(Object key, Class<T> type) { return null; } // 実装省略

  @Override
  public <T> T get(Object key, Callable<T> valueLoader) { return null; } // 実装省略

  @Override
  public void put(Object key, Object value) {
    if (value == null) {
      return;
    }
    this.memcachedClient.set(key.toString(), 24 * 60 * 60 /* expiration 1 day */, value);
    log.debug("cache put for key: " + key); // 動作確認用DEBUGログ
  }

  @Override
  public void evict(Object key) { } // 実装省略

  @Override
  public void clear() { } // 実装省略

}

2-4. Memcached接続設定クラスを作成する

CachingConfigurerSupportを継承したMemcached接続設定クラスを作成します。

CachingConfigurerSupportを継承したMemcached接続設定クラス実装例
CachingConfiguration.java
package com.example.demo;

import java.io.*;
import java.util.*;
import lombok.extern.slf4j.*;
import net.spy.memcached.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.cache.*;
import org.springframework.cache.annotation.*;
import org.springframework.cache.interceptor.*;
import org.springframework.cache.support.*;
import org.springframework.context.annotation.*;

@EnableCaching
@Configuration
@Slf4j
public class CachingConfiguration extends CachingConfigurerSupport {

  @Value("${cache.hostname}")
  private String hostname;

  @Value("${cache.port}")
  private String port;

  @Bean
  MemcachedClient memcachedClient() {
    String memcachedAddress = hostname + ":" + port;
    /* ※1 */
    ClientMode clientMode = hostname.contains("cache.amazonaws.com") ? ClientMode.Dynamic : ClientMode.Static;
    /* ※2 */
    ConnectionFactory connectionFactory = new ConnectionFactoryBuilder(new KetamaConnectionFactory())
        .setClientMode(clientMode)
        .build();

    try {
      return new MemcachedClient(connectionFactory, AddrUtil.getAddresses(memcachedAddress));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  @Bean
  public CacheManager cacheManager() {
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    // 複数のCacheインスタンスをセットできますが、今回は1つのみセットします。
    cacheManager.setCaches(Collections.singletonList(new Memcached("cache", memcachedClient())));
    cacheManager.afterPropertiesSet();
    return cacheManager;
  }

  @Override
  public CacheErrorHandler errorHandler() {
    return new SimpleCacheErrorHandler();
  }

}
ローカル用接続のapplication.yml設定例
application.yml
cache:
  hostname: localhost
  port: 11211

logging:
  level:
    com.example.demo: DEBUG # ログ出力用
※補足
  1. クライアントモードを決定しています。
ClientMode clientMode = hostname.contains("cache.amazonaws.com") ? ClientMode.Dynamic : ClientMode.Static;

オートディスカバリ機能を利用するためにはClientMode.Dynamicを利用する必要があります。
Elasticache for Memcachedクラスターへ接続するときのみClientMode.Dynamicを利用し、 
オートディスカバリ機能がないmemcachedへ接続する場合はClientMode.Staticを利用するように分岐させています。
2. 効率的な負荷分散のための ElastiCache クライアントの設定をするため、KetamaConnectionFactoryインスタンスを指定しています。

new ConnectionFactoryBuilder(new KetamaConnectionFactory())

2-5. キャッシュ操作のための、動作確認デモ用のエンドポイントを用意する

動作確認デモ用にエンドポイントを用意します。

コントローラクラス
DemoController.java
package com.example.demo;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class DemoController {

  final DemoService demoService;

  @GetMapping("/demo")
  public ResponseEntity<String> demo() {
    String value = demoService.getValue("keyName");
    return new ResponseEntity<>(value, HttpStatus.OK);
  }

}
サービスクラス
DemoService.java
package com.example.demo;

import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class DemoService  {

  /**
   * @Cacheableでキャッシュ機能の有効化をしています。
   * キャッシュがなければ、return文の"Value!!"値が返却され、keyNameをキーとしてキャッシュ登録されます。
   * キャッシュがあれば、キャッシュに登録されている"Value!!"値が返却されます。
   */
  @Cacheable(cacheNames = "cache", key = "#keyName")
  public String getValue(final String keyName) {
    return "Value!!";
  }

}

以上のクラスや設定ファイルを用意したら、Spring Bootアプリケーションを起動します。

http://localhost:8080/demo

へGETリクエストすると、ローカル環境にてキャッシュ操作の動作確認ができます。
数回リクエストすると動作確認用DEBUGログにより、キャッシュ登録・取得操作の様子がうかがえます。

2022-12-05 14:17:19.783 DEBUG 25796 --- [nio-8080-exec-7] com.example.demo.Memcached               : cache miss for key: keyName
2022-12-05 14:17:19.788 DEBUG 25796 --- [nio-8080-exec-7] com.example.demo.Memcached               : cache put for key: keyName
2022-12-05 14:17:25.890 DEBUG 25796 --- [nio-8080-exec-8] com.example.demo.Memcached               : cache hit for key: keyName
2022-12-05 14:17:26.624 DEBUG 25796 --- [nio-8080-exec-9] com.example.demo.Memcached               : cache hit for key: keyName

3. Elasticache for Memcachedへ接続できるように設定を書き換える

ElastiCache for Memcachedクラスターの接続設定をapplication.ymlに記載します。
AWSマネジメントコンソールから作成済のクラスターの詳細画面を開き、「設定エンドポイント」に記載されているホスト名とポートを設定します。

ElastiCache for Memcachedクラスターの設定エンドポイントを記載したapplication.yml例
application.yml
# 設定エンドポイントに「demo-memcached.xxxxx.cfg.apne1.cache.amazonaws.com:11211」と記載されている場合
cache:
  hostname: demo-memcached.xxxxx.cfg.apne1.cache.amazonaws.com
  port: 11211

※ホスト名は必ず「.cfg」が入っているものを指定すること(重要

設定変更後、Spring BootアプリケーションをElastiCache for Memcachedクラスターに接続できるネットワーク上(AWS上であればEC2やFargate等)に置いて起動すると、キャッシュ操作ができます。

以上、Spring BootでElasticache for Memcachedのキャッシュ操作をするまでの一連の流れでした。
memcachedサーバーに接続できているアプリケーションは、接続をElasticache for Memcachedクラスターに向けるよう設定変更すれば接続可能ということがわかりました。

(おまけ)オートディスカバリ機能の動作確認

ElastiCache for MemcachedクラスターへClientMode.Dynamicモードで接続するとオートディスカバリ機能が利用できます。
クラスターへ定期的に問い合わせをし、ノード数の増減が有るか無いかを確認し、
クラスターの状態を把握し接続設定を更新するようになります。
ElastiCache for Memcachedクラスターのノード増減によってconfigurationが変更される様子はログで確認できます。(WARNレベルで出力されます。)

クラスターのノード数を1から4に増やしたとき
ノード増加時のログ例
Level:WARN	Logger:n.s.m.ConfigurationPoller:90	 Message:Change in configuration - Existing configuration: Version:1
  CacheNode List:
    demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
New configuration:Version:2
  CacheNode List:
    demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0003.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0004.apne1.cache.amazonaws.com:11211

configurationのversionが1から2に上がり、0001~0004ノードが認識されるようになりました。

クラスターのノード数を4から2に減らしたとき
ノード減少時のログ例
Level:WARN	Logger:n.s.m.ConfigurationPoller:90	Message:Change in configuration - Existing configuration: Version:2
  CacheNode List:
    demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0003.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0004.apne1.cache.amazonaws.com:11211
New configuration:Version:3
  CacheNode List:
    demo-memcached.xxxxxx.0001.apne1.cache.amazonaws.com:11211
    demo-memcached.xxxxxx.0002.apne1.cache.amazonaws.com:11211

configurationのversionが2から3に上がり、0003と0004のノードは認識されなくなりました。

自動検出されるため、ノードを追加してもアプリケーション側の修正は必要ありません。便利です。

参考文献

最後までお読みいただきありがとうございました。

29
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
4