This article was contributed by The MemCachier Add-on
MemCachier manages and scales clusters of memcache servers so you can focus on your app. Tell us how much memory you need and get started for free instantly. Add capacity later as you need it.
follow @MemCachier on Twitter
Memcache を使用した Spring Boot アプリケーションのスケーリング
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2022年03月24日(木)
Memcache は、Web アプリとモバイルアプリバックエンドのパフォーマンスとスケーラビリティを改善する技術です。ページの読み込みが遅すぎる場合や、アプリにスケーラビリティの問題がある場合は、Memcache の使用を検討してください。小規模なサイトであっても、Memcache の導入によってページの読み込みを高速化し、将来の変化にアプリを対応させることができます。
このガイドでは、単純な Spring Boot 2 アプリケーション (ベースは Spring Framework 5 を作成して Heroku にデプロイし、Memcache を追加してパフォーマンスのボトルネックを軽減する方法を示します。
ソースコードを表示したり、次の Heroku Button を使用してそれをデプロイしたりできます。
前提条件
このガイドの手順を完了する前に、以下のすべての条件を満たしていることを確認してください。
- Java の知識がある (Spring Boot の知識もあれば理想的です)
- Heroku ユーザーアカウント (無料ですぐにサインアップ)
- Maven と Heroku CLI がコンピュータにインストールされている
Heroku への Spring Boot アプリケーションのデプロイ
Spring Boot アプリケーションを簡単に作成するために、Spring Boot CLI をインストールすることをお勧めします。 CLI をインストールしない場合、Spring Initializer から Spring Boot スケルトンを設定およびダウンロードすることもできます。
spring という名前の Ruby on Rails アプリケーションもあります。このアプリケーションが
インストールされているか、このバイナリを shim する Ruby バージョンマネージャー rbenv
が
インストールされている場合は、spring cli のエイリアスを作成します (例:
alias springboot='/opt/spring-boot-cli/bin/spring'
)。
CLI を使用すると、スケルトンを簡単に作成できます。
$ spring init --d=web,data-jpa,thymeleaf -g com.memcachier -a tutorial -n TaskList memcached_tutorial
$ cd memcached_tutorial
作成されるスケルトンは、データベースをサポートし (data-jpa
)、thymeleaf
テンプレート言語を使用する Web アプリです。JSP、groovy、freemaker、mustache など、その他のテンプレート言語も Spring Boot ではサポートされています。
Heroku アプリの作成
3 つの簡単な手順で、Spring Boot スケルトンから Heroku アプリを作成できます。
- アプリの起動方法を Heroku に指示するために、
Procfile
を追加する必要があります。
$ echo 'web: java -Dserver.port=$PORT $JAVA_OPTS -jar target/*.jar' > Procfile
- Git リポジトリを初期化してスケルトンをコミットします。
$ git init
$ git add .
$ git commit -m 'Spring Boot skeleton for Heroku'
- Heroku アプリを作成します。
$ heroku create
このコマンドにより、実際の Heroku アプリケーションが作成されるのに加えて、対応するリモートがローカルの Git リポジトリに追加されます。
タスクリスト機能の追加
ユーザーがタスクを表示、追加、削除できるタスクリストをアプリに追加しましょう。そのためには、次の手順に従う必要があります。
- データベースをセットアップする
Task
エンティティと保存先のテーブルを作成する- ビューとコントローラーロジックを作成する
PostgreSQL データベースのセットアップ
Spring Boot でデータベースを設定する前に、データベースを作成する必要があります。Heroku では、次のようにして、無料の開発用データベースをアプリに追加できます。
$ heroku addons:create heroku-postgresql:hobby-dev
アプリ用の PostgreSQL データベースが作成され、その URL を含む DATABASE_URL
環境設定が追加されます。
Spring Boot では、変数 SPRING_DATASOURCE_URL
を設定する必要があります。この
変数には DATABASE_URL
と同じ URL が含まれますが、
postgres
ではなく jdbc:postgresql
で始まる点が異なります。この変数の値は実行時に
Heroku によって自動的に設定されるため、心配無用です。
このデータベースを使用するには、いくつかのパッケージをインストールする必要があります。次の依存関係を pom.xml
に追加します。
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>3.6.1</version>
</dependency>
最初の依存関係は PostgreSQL ドライバーです。
Java SE の新しいバージョンには JAXB API が含まれなくなったため、2 番目の依存関係でこの API を追加します。詳細は、StackOverflow のこのスレッドを参照してください。
3 番目の依存関係は、liquibase データベース移行を作成および実行するためのものです。
ここで、src/main/resources/application.properties
でデータベースを設定できます。
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.maxActive=10
spring.datasource.maxIdle=5
spring.datasource.minIdle=2
spring.datasource.initialSize=5
spring.datasource.removeAbandoned=true
# Supress exception regarding missing PostgreSQL CLOB feature at Spring startup.
# See http://vkuzel.blogspot.ch/2016/03/spring-boot-jpa-hibernate-atomikos.html
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect
PostgreSQL データベースを使用する準備ができました。次のようにして変更内容を保存します。
$ git commit -am 'Database setup'
Heroku 上の Java からリレーショナルデータベースに接続する方法の詳細は、このガイドを参照してください。
Task エンティティとデータベーステーブルの作成
タスクの作成と保存のために、3 つのものを作成する必要があります。Task
エンティティ、タスクの保存および取得方法を Spring Boot に指示するリポジトリ、実際のテーブルをデータベースに作成する移行です。
Task
エンティティをsrc/main/java/com/memcachier/tutorial/Task.java
に追加します。
package com.memcachier.tutorial;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.validator.constraints.NotEmpty;
@Entity
public class Task {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@NotEmpty
private String name;
protected Task() {}
public Task(String name) {
this.name = name;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return String.format("Task[id=%d, name='%s']", this.id, this.name);
}
}
- リポジトリを
src/main/java/com/memcachier/tutorial/TaskRepository.java
に作成します。
package com.memcachier.tutorial;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface TaskRepository extends CrudRepository<Task, Long> {}
データにアクセスするために基本の CRUD 関数以外のものが必要な場合、代わりに PagingAndSortingRepository
または JpaRepository
を拡張することもできます。詳細は、StackOverflow のこのスレッドを
参照してください。
- liquibase 移行を
src/main/resources/db/changelog/db.changelog-master.yaml
に作成します。
databaseChangeLog:
- changeSet:
id: 1
author: memcachier
changes:
- createTable:
tableName: task
columns:
- column:
name: id
type: int
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: name
type: varchar(255)
constraints:
nullable: false
db
および changelog
フォルダーを作成する必要があります。
この移行は、アプリケーションが起動すると自動的に実行されます。
ここまでの変更内容を保存しましょう。
$ git add .
$ git commit -m 'Task table setup'
タスクリストアプリケーションの作成
実際のアプリケーションを構成するのは、フロントエンドに表示されるビューと、バックエンドの機能を実装するコントローラーです。
- コントローラーを
src/main/java/com/memcachier/tutorial/TaskController.java
に作成します。
package com.memcachier.tutorial;
import javax.validation.Valid;
import java.lang.Iterable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("/")
public class TaskController {
private TaskRepository taskRepo;
@Autowired
public TaskController(TaskRepository repo) {
this.taskRepo = repo;
}
@RequestMapping(method = RequestMethod.GET)
public String showAllTasks(ModelMap model) {
Iterable<Task> tasks = this.taskRepo.findAll();
model.addAttribute("tasks", tasks);
model.addAttribute("newTask", new Task());
return "task";
}
@RequestMapping(method = RequestMethod.POST)
public String newTask(ModelMap model,
@ModelAttribute("newTask") @Valid Task task,
BindingResult result) {
if (!result.hasErrors()) {
this.taskRepo.save(task);
}
return showAllTasks(model);
}
@RequestMapping(method = RequestMethod.DELETE)
public String deleteTask(ModelMap model, @RequestParam("taskId") Long id) {
this.taskRepo.deleteById(id);
return showAllTasks(model);
}
}
このコントローラーには、すべてのタスクを GET
して task
ビューを描画する機能、後からデータベースに保存される新しいタスクを POST
する機能、既存のタスクを DELETE
する機能のすべてが含まれています。
- ビューを
src/main/resources/templates/task.html
に作成します。
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>MemCachier Spring Boot Tutorial</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- Fonts -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css"
rel='stylesheet' type='text/css' />
<!-- Bootstrap CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
rel="stylesheet" />
</head>
<body>
<div class="container">
<!-- New Task Card -->
<div class="card">
<div class="card-body">
<h5 class="card-title">New Task</h5>
<form th:object="${newTask}" method="POST">
<div class="form-group">
<input type="text" class="form-control"
placeholder="Task Name" th:field="*{name}" />
</div>
<button type="submit" class="btn btn-default">
<i class="fa fa-plus"></i> Add Task
</button>
</form>
</div>
</div>
<!-- Current Tasks -->
<div th:if="${not #lists.isEmpty(tasks)}">
<div class="card">
<div class="card-body">
<h5 class="card-title">Current Tasks</h5>
<table class="table table-striped">
<tr th:each="task : ${tasks}">
<!-- Task Name -->
<td th:text="${task.name}" class="table-text"></td>
<!-- Delete Button -->
<td>
<form th:object="${deleteTask}" th:method="DELETE">
<input type="hidden" name="taskId" th:value="${task.id}">
<button type="submit" class="btn btn-danger">
<i class="fa fa-trash"></i> Delete
</button>
</form>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
<!-- Bootstrap related JavaScript -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
</html>
ビューは基本的に、2 つのカードで構成されます。1 つのカードには、新しいタスクを作成するためのフォームが含まれます。もう 1 つのカードには、既存のタスクが入ったテーブルと、対応するタスクを削除するための削除ボタンが含まれます。
これまでの作業を確認し、タスクリストを Heroku にデプロイしましょう。
$ git add .
$ git commit -m 'Add task list view and controller'
$ git push heroku master
$ heroku open
タスクをいくつか追加して、アプリケーションをテストします。タスクリストが Heroku 上で動作するようになりました。ここまで完了したら、Memcache を使用してタスクリストのパフォーマンスを向上させる方法を学ぶことができます。
キャッシングを Spring Boot に追加する
Memcache はインメモリの分散キャッシュです。そのプライマリ API は、SET(key, value)
と GET(key)
の 2 つの操作で構成されます。
Memcache は、複数のサーバーに分散していますが、操作は一定の時間に実行されるハッシュマップ (または辞書) のようなものです。
Memcache の最も一般的な用途は、コストの高いデータベースクエリや HTML レンダリングをキャッシュし、これらの高コスト操作を繰り返す必要をなくすことです。
Memcache のセットアップ
Spring Boot で Memcache を使用するには、まず実際の Memcache キャッシュをプロビジョニングする必要があります。これは、MemCachier アドオンから無料で簡単に入手できます。
$ heroku addons:create memcachier:dev
次に、適切な依存関係を設定する必要があります。simple-spring-memcached
と
XMemcached
を使用して、Spring Boot 内で Memcache を使用します。simple-spring-memcached
と SpyMemcached
を使用することもできます。後者の方法については、MemCachier のドキュメントを参照してください。
simple-spring-memcached
を使用するには、次の内容を pom.xml
に追加します。
<dependency>
<groupId>com.google.code.simple-spring-memcached</groupId>
<artifactId>xmemcached-provider</artifactId>
<version>4.0.0</version>
</dependency>
<!-- Force XMemcached to version 2.4.3 simple-spring-memcached uses 2.4.0 -->
<dependency>
<groupId>com.googlecode.xmemcached</groupId>
<artifactId>xmemcached</artifactId>
<version>2.4.3</version>
</dependency>
src/main/java/com/memcachier/tutorial/MemCachierConfig.java
で、Spring 用に Memcache を設定できるようになりました。
package com.memcachier.tutorial;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.ssm.CacheFactory;
import com.google.code.ssm.config.AbstractSSMConfiguration;
import com.google.code.ssm.config.DefaultAddressProvider;
import com.google.code.ssm.providers.xmemcached.XMemcachedConfiguration;
import com.google.code.ssm.providers.xmemcached.MemcacheClientFactoryImpl;
import net.rubyeye.xmemcached.auth.AuthInfo;
import net.rubyeye.xmemcached.utils.AddrUtil;
@Configuration
public class MemCachierConfig extends AbstractSSMConfiguration {
@Bean
@Override
public CacheFactory defaultMemcachedClient() {
String serverString = System.getenv("MEMCACHIER_SERVERS").replace(",", " ");
List<InetSocketAddress> servers = AddrUtil.getAddresses(serverString);
AuthInfo authInfo = AuthInfo.plain(System.getenv("MEMCACHIER_USERNAME"),
System.getenv("MEMCACHIER_PASSWORD"));
Map<InetSocketAddress, AuthInfo> authInfoMap =
new HashMap<InetSocketAddress, AuthInfo>();
for(InetSocketAddress server : servers) {
authInfoMap.put(server, authInfo);
}
final XMemcachedConfiguration conf = new XMemcachedConfiguration();
conf.setUseBinaryProtocol(true);
conf.setAuthInfoMap(authInfoMap);
final CacheFactory cf = new CacheFactory();
cf.setCacheClientFactory(new MemcacheClientFactoryImpl());
cf.setAddressProvider(new DefaultAddressProvider(serverString));
cf.setConfiguration(conf);
return cf;
}
}
これによって simple-spring-memcached
が設定され、そのキャッシングアノテーションを使用できるようになります。simple-spring-memcached
で有効化できる組み込みのキャッシングアノテーションは Spring からも提供されています。しかし、このチュートリアルでは、アノテーションで提供された simple-spring-memcached
を使用します。こちらの方が全体的に柔軟性が高く、Memcached を使用したキャッシュに適しているからです。ただし、Spring のアノテーションを使用しても、このチュートリアルの動作に問題はありません。Spring の組み込みキャッシングアノテーションを使用する場合は、MemCachier のドキュメントを参照してください。
コストの高いデータベースクエリをキャッシュする
Memcache を使用して、コストの高いデータベースクエリをキャッシュすることはよくあります。この単純な例にはコストの高いクエリはありませんが、学習のために、すべてのタスクをデータベースから取得するのはコストの高い操作であると仮定します。
Task のクエリをキャッシュするために、キャッシングを実装するメソッドで TaskRepository
を拡張します。Spring Boot でリポジトリを拡張するには、3 つの手順が必要です。
TaskRepository
に追加するメソッドを備えたインターフェースをsrc/main/java/com/memcachier/tutorial/CachedTaskRepository.java
に構築します。
package com.memcachier.tutorial;
import java.lang.Iterable;
public interface CachedTaskRepository {
public Iterable<Task> findAllCached();
}
- このインターフェースの実装を
src/main/java/com/memcachier/tutorial/TaskRepositoryImpl.java
に作成します。
package com.memcachier.tutorial;
import java.lang.Iterable;
import org.springframework.beans.factory.annotation.Autowired;
import com.google.code.ssm.api.ReadThroughAssignCache;
public class TaskRepositoryImpl implements CachedTaskRepository {
@Autowired
TaskRepository taskRepository;
@ReadThroughAssignCache(namespace="Taskrepo", assignedKey="all")
public Iterable<Task> findAllCached() {
return this.taskRepository.findAll();
}
}
実装のファイル名で従う必要がある命名規則は
<REPOSITORY-NAME>Impl.java
です。
TaskRepository
の CRUD インターフェースの残り部分には、@Autowired
参照を追加するだけでアクセスできます。
ここで、@ReadThroughAssignCache
アノテーションを介してキャッシングが発生します。すべての @ReadThrough*Cache
アノテーションでは、次の処理を行います。
- 値がキャッシュにあるかどうかチェックし、ある場合はその値を返します。
- キャッシュにない場合は、関数を実行してその値を返し、その値をキャッシュにも保存します。
このアノテーションの Assign
バージョンでは、アノテーションで宣言された割り当て済みのキーを使用します。これらのアノテーションについての詳細は、Simple Spring Memcached のドキュメントを参照してください。
- 必ず、この実装を
TaskRepository
に統合してください。 これは単に、TaskRepository
インターフェースにもCachedTaskRepository
インターフェースを拡張させることによって行います。
// ...
public interface TaskRepository extends CrudRepository<Task, Long>, CachedTaskRepository {}
キャッシングアノテーションに関する注意事項: Spring ではキャッシングアノテーションの処理に プロキシを使用します。このため、コントローラーの内部にプライベートメソッドを作成して キャッシュアノテーションを追加しても、メソッドがキャッシュされることは期待できません。簡単に 言うと、キャッシュされるメソッドは、そのインターフェースを介してアクセスされるコンポーネントの 一部でなければなりません。詳細は、 StackOverflow のこのスレッド と、そこで言及されているリファレンスをご覧ください。
すべてのタスクをキャッシュするためのメソッドは用意できましたが、それらのメソッドを機能させるためには、src/main/java/com/memcachier/tutorial/Task.java
で Task のデータ型のシリアル化を可能にする必要があります。
// ...
import java.io.Serializable;
public class Task implements Serializable {
// ...
}
最後に、src/main/java/com/memcachier/tutorial/TaskController.java
のコントローラーで、キャッシュされたタスクを取得できるようになりました。
// ...
public String showAllTasks(ModelMap model) {
Iterable<Task> tasks = this.taskRepo.findAllCached();
// ...
}
// ...
この新しい機能をデプロイしてテストしてみましょう。
$ git add .
$ git commit -m 'Add caching with MemCachier'
$ git push heroku master
$ heroku open
キャッシュ内で何が起きているかを見るために、MemCachier のダッシュボードを開きます。
$ heroku addons:open memcachier
タスクリストを最初に読み込むと、get miss と set コマンドが増加しているはずです。それ以降は、タスクリストを再読み込みするたびに get hit が増加するはずです (ダッシュボードの統計を更新してください)。
キャッシュは機能していますが、まだ大きな問題があります。新しいタスクを追加して結果を確認します。新しいタスクが現在のタスクリストに反映されていません。新しいタスクがデータベースに作成されましたが、アプリで表示されているのはキャッシュから取得した古いタスクリストです。
古いデータのクリア
データをキャッシュするだけでなく、古くなったデータを無効化することも重要です。この例では、新しいタスクが追加されるか既存のタスクが削除されるたびに、キャッシュされたタスクリストは古くなります。この 2 つの操作のどちらかが実行されるたびに、キャッシュを確実に無効化する必要があります。
キャッシュをクリアするラッパーを、TaskRepository
の save メソッドと delete メソッドに追加することができます。
これは、次の 2 つの手順で行います。
src/main/java/com/memcachier/tutorial/CachedTaskRepository.java
で、CachedTaskRepository
インターフェースにラッパーメソッドを宣言します。
// ...
public interface CachedTaskRepository {
public Iterable<Task> findAllCached();
public Task saveAndClearCache(Task t);
public void deleteByIdAndClearCache(Long id);
}
src/main/java/com/memcachier/tutorial/TaskRepositoryImpl.java
でラッパーメソッドを実装します。
// ...
import com.google.code.ssm.api.InvalidateAssignCache;
public class TaskRepositoryImpl implements CachedTaskRepository {
// ...
@InvalidateAssignCache(namespace="Taskrepo", assignedKey="all")
public Task saveAndClearCache(Task t){
return this.taskRepository.save(t);
}
@InvalidateAssignCache(namespace="Taskrepo", assignedKey="all")
public void deleteByIdAndClearCache(Long id){
this.taskRepository.deleteById(id);
}
}
ここでは、古くなったデータが @InvalidateAssignCache
アノテーションを介して無効化されます。
@ReadThroughAssignCache
と同様に、これは、アノテーションで宣言された割り当て済みのキーに対して機能します。
タスクの追加または削除のリクエストが発生するたびに、コントローラーでこれらのラッパー関数を使用してキャッシュをクリアできるようになりました。そのためには、save
と
deleteById
(src/main/java/com/memcachier/tutorial/TaskController.java
内) を
saveAndClearCache
と deleteByIdAndClearCache
に置き換えます。次のようになります。
// ...
@RequestMapping(method = RequestMethod.POST)
public String newTask(ModelMap model,
@ModelAttribute("newTask") @Valid Task task,
BindingResult result) {
if (!result.hasErrors()) {
this.taskRepo.saveAndClearCache(task);
}
return showAllTasks(model);
}
@RequestMapping(method = RequestMethod.DELETE)
public String deleteTask(ModelMap model, @RequestParam("taskId") Long id) {
this.taskRepo.deleteByIdAndClearCache(id);
return showAllTasks(model);
}
修正されたタスクリストをデプロイします。
$ git commit -am 'Clear stale data from cache'
$ git push heroku master
$ heroku open
新しいタスクを追加すると、タスクリストのキャッシングの実装以降に追加したすべてのタスクがリストに表示されます。