Table of Contents [expand]
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2022年03月24日(木)
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 は、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
新しいタスクを追加すると、タスクリストのキャッシングの実装以降に追加したすべてのタスクがリストに表示されます。
