【Spring Boot】Resilience4jのRateLimiterでリクエストパラメータごとの流量制御

# 流量制御とは

アプリケーションで一度に実行できる処理の最大値を設定し、同時にリクエストされる回数を制限することです。サーバの負荷が一定以上になるのを防ぐことで、安定的にアプリケーションを稼働させることができます。APIを外部に公開する場合など、リクエストの回数がものすごく多くなる懸念があるときに使用すると有効でしょう。

Javaでは`Resilience4j`というライブラリに`RateLimiter`という仕組みがあるので、それを用いて実現することができます。

- [Resilience4j公式ドキュメント](https://resilience4j.readme.io/)


# リクエストパラメータごとに流量制御を適応する

通常流量制御はコントローラーに設定したURLごとに適応することが一般的でしょう。このような使い方に関しては、以下のサイトでわかりやすく解説されていました。

https://qiita.com/d-yosh/items/31fad6b4f6a9bb518e6d

本記事ではURLのエンドポイントに加え、リクエストパラメータごとでも制御する回数を分ける方法を紹介します。

具体的にはリクエストパラメータに`userId`という値を設定し、GETでの`/sample`へのアクセスを`userId`ごとに一定時間にリクエストできる回数を制限しています。

# 実装

それでは具体的な実装に入っていきます。流量制御で用いる値は主に以下の3つです。

- `limitForPeriod`：一定時間にリクエストできる回数
- `limitRefreshPeriod`：制限をリフレッシュする時間
- `timeoutDuration`：制限されたときの再度実行するまでの待機時間

`limitRefreshPeriod`の時間の中で`limitForPeriod`回までメソッドの呼び出しができ、上限に達したら制限する。
呼び出し時に制限されていた場合`timeoutDuration`で指定した時間待機し、再度呼び出しを行う。このときも制限されている場合は処理が落ちる。

このような振る舞いをします。

## ディレクトリ構成

今回作成するサンプルは以下のような構成になっています。

```
sample-project
    ├── ︙
    ├── build.gradle
    └── src
        ├── ︙
        └── main
            ├── java
            │   └── com
            │       └── example
            │           ├── SampleProjectApplication.java
            │           ├── config
            │           │   └── resilience4j
            │           │       └── SampleRateLimiterConfig.java
            │           ├── infrastructure
            │           │   └── resilience4j
            │           │       └── SampleRateLimiterResistry.java
            │           └── controller
            │                   └── SampleController.java
            └── resources
                ├── application.yml
                └── ︙
```
（使用するファイルのみ記述しています。）
以下で作成するファイルのパスはこちらを参照してください。

## 依存関係（Gradle）
`/build.gradle`の`dependencies`に以下のように記述します。
```build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'io.github.resilience4j:resilience4j-ratelimiter:2.0.1'
	compileOnly 'org.projectlombok:lombok:1.18.24'
	annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
```
これでspringの機能、Resilience4jのRateLimiter、lombokをしようできるようになります。
（lombokはGetterやSetterを簡単に記述できるので使用しています。）

【注意】
Resilience4jのバージョンの`2.0.*`ではJavaのバージョンが17以降でないとbuildに失敗します。Javaの8や11を使っている場合は、Resilience4jのバージョンを`1.7.1`などにすると利用できます。

## 値を設定をする

上で説明した3つの値を設定していきます。以下の2つのファイル作成します。

- `application.yml`：データを定義するためのYAML形式のファイル
- `SampleRateLimiterConfig.java`：`application.yml`の値を読み取るためのファイル

`application.yml`を以下のように記述します。

```application.yml
resilience4j:
  ratelimiter:
    limitRefreshPeriod: 1000
    limitForPeriod: 10
    timeoutDuration: 5000
```

次に`SampleRateLimiterConfig.java`で上で設定した値を読み取れるようにします。

```SampleRateLimiterConfig.java
package com.example.config.resilience4j;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "resilience4j.ratelimiter")
public class SampleRateLimiterConfig {
	int limitRefreshPeriod;
	int limitForPeriod;
	int timeoutDuration;
}
```

`application.yml`に設定した値を読み込む方法は[こちらの記事](https://tech.excite.co.jp/entry/2022/09/30/200658)が参考になりました。

## 流量制御の管理クラス

設定した値で流量制御を行えるようにしたインスタンスの管理などを行う`SampleRateLimiterResistry.java`を作成します。

```SampleRateLimiterResistry.java
package com.example.infrastructure.resilience4j;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Component;

import com.example.demo.config.resilience4j.SampleRateLimiterConfig;

import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;

/**
 * 流量制御を管理するクラス.
 */
@Component
public class SampleRateLimiterResistry {

    // 流量制御の値の設定
	private final RateLimiterConfig config;
    // リクエストパラメータのuserIdごとにRegistryを管理するためのmap
	private Map<Integer, RateLimiterRegistry> registryMap = new HashMap<>();
	
	/**
	 * コンストラクタ.
	 * 
	 * @param sampleRateLimiterConfig application.ymlの値を読み込んでいるクラス
	 */
	public SampleRateLimiterResistry(SampleRateLimiterConfig sampleRateLimiterConfig) {
		// 流量制御で用いる各種値を設定
		this.config = RateLimiterConfig.custom()
				 .limitRefreshPeriod(Duration.ofMillis(sampleRateLimiterConfig.getLimitRefreshPeriod()))
				  .limitForPeriod(sampleRateLimiterConfig.getLimitForPeriod())
				  .timeoutDuration(Duration.ofMillis(sampleRateLimiterConfig.getTimeoutDuration()))
				  .build();
	}
	
	/**
	 * ユーザIDごとのRateLimiterRegistryを返す.
	 * 
	 * @param userId ユーザID
	 * @return ユーザIDごとのRateLimiterRegistry
	 */
	public RateLimiterRegistry getRegistry(Integer userId) {
		RateLimiterRegistry result = registryMap.get(userId);
		if(result == null) {
			// コンストラクタで作成した設定値を適応した流量制御を行うRegistryを作成
			result = RateLimiterRegistry.of(config);
			registryMap.put(userId, result);
		}
		return result;
	}
}
```

これで1秒間に10回のリクエストまで許容し、制限された場合は5秒待機するような設定となります。

## リクエストの受け取りと制限

`SampleController.java`にて流量制御を適応します。DDDの観点から言うと`infrastructure`配下の物をコントローラーで使用するのは良くないと思いますが、今回はサンプルということで許容させてください。

```SampleController.java
package com.example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.infrastructure.resilience4j.SampleRateLimiterResistry;

import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.vavr.control.Try;

@RestController
@RequestMapping("/api/sample")
public class SampleController {
	
	private SampleRateLimiterResistry sampleRateLimiterResistry;
	
	TaskController(SampleRateLimiterResistry sampleRateLimiterResistry) {
		this.sampleRateLimiterResistry = sampleRateLimiterResistry;
	}

	/**
	 * 流量制御用サンプルエンドポイント.
	 * 
	 * @param userId ユーザID
	 */
	@GetMapping("/ratelimiter")
	public String rateLimiter(@RequestParam("userId") Integer userId) {
		RateLimiterRegistry registry = sampleRateLimiterResistry.getRegistry(userId);
		Try.ofSupplier(RateLimiter.decorateSupplier(registry.rateLimiter("default"), () -> {
			System.out.println("処理の実行");
			return null;
		})).get();
		return userId.toString();
	}
}
```

コントローラーの処理自体は受け取った`userId`をそのまま返しているだけです。
これでリクエストパラメータの`userId`ごとに流量制御を行うことができます。

なお、`registry.rateLimiter`の第2引数のラムダ式の部分を`SampleService::sampleMethod`のようにすれば、`SampleSevice.java`の`sampelMethod`という名前のメソッドを呼び出すことができます。

`localhost`でサーバを起動すれば次のように`curl`コマンドで呼び出すことができます。

```zsh
$curl 'localhost:8080/api/sample/ratelimiter?userId=1'
```

連続でリクエストを送ると処理を待機したり失敗することがわかると思います。
`application.yml`に設定した数値を変えて、動作を確認してみてください。