SpringBoot+インメモリデータグリッド入門

インメモリデータグリッドについて勉強してみたついでに、実際にアプリに組み込むとどうなるのか気になったので入門してみた。

# インメモリデータグリッドとは
- データを複数サーバで分散管理する仕組み。
- 全サーバが重複してデータを持つこと(レプリケーション方式)もできれば、あるグループのサーバにとってのみ必要なデータを、そのグループ内で重複して管理する(パーティション方式)といった柔軟なデータの信頼性確保が可能。
- DBのようにディスクI/Oが発生しないため、高速にデータのCRUD操作、P2Pのデータ同期が行える。
- 信頼性、高速性を備えたアーキテクチャ。らしい。

下記の記事を参考に学習
- [概要を掴むのにおすすめ](http://www.itmedia.co.jp/im/articles/1209/19/news139_2.html)
- [具体的なアーキテクチャの種類](https://www.slideshare.net/MasakiYamakawa/ss-72385833)



# SpringBoot + Apach GEODEでアプリを作ってみよう

 - 色々と記事をザッピングしたところで実際に作ってみたくなったので`SpringDataGeode`というプロジェクトがSpringのgitリポジトリにあるので、それを使ってSpringBootでインメモリデータグリッドを体感してみようと思います。
 - ユーザ登録・検索を行うサーバアプリケーションを想定して作成します。

   - [spring-data-geodeプロジェクト](https://github.com/spring-projects/spring-data-geode)

## Apache GEODEのclient/serverモデルでアプリを作る
- [Spring Initializer](https://start.spring.io)でclient,serverのアプリの雛形を作成する。
  - Web,lombokのみ選択。
  - 今回は`gradle`でプロジェクトを作成。

### client側アプリを作成

- build.gradleの依存関係に`spring-data-geode`を追加。

```Groovy:build.gradle
dependencies {
	implementation('org.springframework.boot:spring-boot-starter-web'){
      // log4jのライブラリがspring-data-geodeの依存するlog4jのライブラリと競合するため、除外
	  exclude group: 'org.springframework.boot', module:'spring-boot-starter-logging'
	}
    // ドメインモデルを扱うプロジェクトを依存に追加
	compile project(':geodeCommon')
	compileOnly('org.projectlombok:lombok')
	compile(group: 'org.springframework.data', name: 'spring-data-geode', version: '2.1.3.RELEASE')
}
```

- 起動クラスのコード

```java:GeodeClientApplication.java
package spring.geode.client.geodeClient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.config.annotation.EnablePdx;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;

import spring.geode.client.geodeClient.repository.UserRepository;
import spring.geode.geodeCommon.model.User;

@SpringBootApplication
@ClientCacheApplication(name = "SpringGeodeClientApplication") //①
@EnableGemfireRepositories(basePackageClasses = UserRepository.class) //②
@EnableEntityDefinedRegions(basePackageClasses = User.class) //③
@EnablePdx //④
public class GeodeClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(GeodeClientApplication.class, args);
	}

}

```

- アノテーションの説明
  - ①: `Apache GEODE`におけるclientのアプリケーションとして起動する設定
  - ②: 指定したクラスを`Apache GEODE`のデータアクセサとして機能させる設定
  - ③: 指定したRegion(RDBで言うところのテーブル)を自動的に作成する設定
  - ④: `Apache GEODE`の扱うデータのシリアライズ/デシリアライズに関する設定(必須ではなさそう)
 
- Controllerクラス

```java:UserController.java
package spring.geode.client.geodeClient.controller;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import spring.geode.client.geodeClient.service.UserService;
import spring.geode.geodeCommon.model.User;
import spring.geode.geodeCommon.model.UserRequest;

@RestController
@RequiredArgsConstructor
public class UserController {
	private final UserService userService;
    //nameでのユーザ検索API
	@RequestMapping(path = "/find/user/{name}", method = RequestMethod.GET)
	public User findById(@PathVariable String name) {
		return userService.findByName(name);
	}
    //ユーザ全件検索API
	@RequestMapping("/findAll")
    public List<User> findAll() {
        return userService.findAll();
    }
    //新規ユーザ登録API
	@RequestMapping(path = "/register/user", method = RequestMethod.POST)
	public String register(@RequestBody UserRequest request) {
		return userService.register(request).getName();
	}
}
```

- serviceクラス

```java:UserService.java
package spring.geode.server.geodeServer.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import spring.geode.geodeCommon.model.User;
import spring.geode.geodeCommon.model.UserRequest;
import spring.geode.server.geodeServer.repository.UserRepository;

@RequiredArgsConstructor
@Service
public class UserService {
	private final UserRepository rep; 
	public User findByName(String name) {
		User user=rep.findByName(name).get(0);
		return user;
	}
	
	public User register(UserRequest request) {
		User commited = rep.save(new User(request));
		return commited;
	}
	
	public List<User> findAll(){
		List<User> users=new ArrayList<>();
		rep.findAll().forEach(user -> users.add(user));;
		return users;
	}
}
```

Repositoryクラス

```java:UserRepository.java
package spring.geode.server.geodeServer.repository;

import java.util.List;

import org.springframework.data.gemfire.repository.GemfireRepository;

import spring.geode.geodeCommon.model.User;

public interface UserRepository extends GemfireRepository<User, Integer> {
	List<User> findByName(String name);
}
```

- 設定ファイル

```properties:application.properties
spring.data.gemfire.pool.locators=localhost[40404]
server.port=9000
```
clientアプリケーションが接続する先のlocatorのIP,portを設定。
今回はserverアプリケーションの設定でlocatorを起動するため、`localhost`を指定。

`Apache GEODE`におけるclient,server,locatorの関係は以下の記事に記載。
[Apache GEODE の概要](https://dev.classmethod.jp/server-side/geode-apache-geode-handson/)

ここまででclientアプリケーションは実装完了。
扱うデータオブジェクト(Userクラス)はclient,serverプロジェクトで共通のクラスを使う必要があるため、Commonプロジェクトに集約して後ほど作成する。

### server側アプリを作成
起動クラス以外は、clientアプリケーションのコードをそのままserverアプリケーションのプロジェクトに持ち込めば良い。

- 起動クラス

```java:GeodeServerApplication.java
package spring.geode.server.geodeServer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.config.annotation.CacheServerApplication;
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
import org.springframework.data.gemfire.config.annotation.EnableLocator;
import org.springframework.data.gemfire.config.annotation.EnableManager;
import org.springframework.data.gemfire.config.annotation.EnablePdx;
import org.springframework.data.gemfire.repository.config.EnableGemfireRepositories;

import spring.geode.geodeCommon.model.User;
import spring.geode.server.geodeServer.repository.UserRepository;

@SpringBootApplication
@CacheServerApplication(locators = "localhost[40404]") //①
@EnableGemfireRepositories(basePackageClasses = UserRepository.class)
@EnableEntityDefinedRegions(basePackageClasses = User.class)
@EnablePdx
public class GeodeServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(GeodeServerApplication.class, args);
	}

	@Configuration
	@EnableLocator(port = 40404)　//②
	@EnableManager(start = true) //③
	static class LocatorManagerConfiguration {
	}

}
```

- アノテーションの説明
  - ①: `Apache GEODE`におけるserverのアプリケーションとして起動する設定。接続してくるlocatorは`localhost`の40404ポートであることを設定。
  - ②: locatorを40404ポートで起動する設定
  - ③: client/serverのアプリケーションの監視を行うサービスを起動する設定

ここまででserverアプリケーションは実装完了。

### データモデルの作成

- Webアプリにおけるclientからのリクエストモデル(`Apache GEODE`におけるclientとは異なる)

```java:UserRequest.java
package spring.geode.geodeCommon.model;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequest implements Serializable{
	private static final long serialVersionUID = 1L;
	
	private String name;
	private int age;
}
```

- `Apache GEODE`に永続化するドメインモデル

```java:User.java
package spring.geode.geodeCommon.model;

import java.io.Serializable;
import java.util.UUID;

import org.springframework.data.gemfire.mapping.annotation.Region;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Region("Users") //①
public class User implements Serializable {
	private static final long serialVersionUID = 1L;

	private Integer id;
	private String name;
	private int age;
	
	public User(UserRequest request) {
		this.name=request.getName();
		this.age=request.getAge();
		this.id=UUID.randomUUID().hashCode();
	}
}
```

- アノテーションの説明
  - ①: このモデルが紐付くRegionを設定する。

- データモデルを実装したプロジェクトはclient,serverアプリケーションのプロジェクトに依存されるため、client,serverアプリケーションの`settings.gradle`に以下の内容を記載

```Groovy:settings.gradle
// geodeCommonは自分の作成したプロジェクト名に読み換える
include ':geodeCommon'
project(':geodeCommon').projectDir = new File('../geodeCommon')
```

ここまでで、データモデルの実装は完了

## 起動してみる
clientアプリケーションは起動時にlocatorへ接続するため、先にserverアプリケーションを起動する必要がある。

- serverアプリケーション起動(組み込みTomacatはポート番号`9090`で起動)
<img width="1440" alt="スクリーンショット 2019-01-27 2.01.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/301386/7c24314e-49d3-49df-a4f4-f61e08c4cb19.png">


- clientアプリケーション起動(組み込みTomcatはポート番号`9000`で起動)
<img width="1440" alt="スクリーンショット 2019-01-27 2.02.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/301386/abd44736-b3e2-d1eb-4a0f-739255077dfa.png">

正常に両方のアプリケーションが起動できれば、client,locator,serverの接続はできているはずです。


## ユーザをclientアプリケーションに登録してみる

```bash
curl -H "Content-Type: application/json" -X POST -d '{"name":"John","age":23}' http://localhost:9000/register/user/;

curl -H "Content-Type: application/json" -X POST -d '{"name":"Bob","age":10}' http://localhost:9000/register/user/;
```

## serverアプリケーションからユーザを検索してみる

```bash
curl -i http://localhost:9090/findAll
```

検索結果で、clientアプリケーションに登録したユーザが検索できればOK

```
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Jan 2019 17:10:37 GMT

[{"id":-1174841827,"name":"Bob","age":10},{"id":-516984913,"name":"John","age":23}]
```

念のため、name指定での検索も行う

```bash
curl -i http://localhost:9090/find/user/John;

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Jan 2019 17:12:33 GMT

{"id":-516984913,"name":"John","age":23}
```

client,serverでデータが同期されていることを確認できました。
以下の公式ドキュメントを参考に実装してみました。
https://geode.apache.org/docs/

今後はpeerモデルでのアプリケーション作成や、AWSでのアプリ構成、非同期永続化など作り込んでいってみようと思います。