REST API における楽観ロックの実装方法

# 概要

あるリソースの状態を変更することは、あらゆるシステムで実装される動作です。
このときに、整合性の担保を考慮すると、なんらかのロックの仕組みが必要となってきます。

ここでは、[Zalando](https://restful-api-guidelines-ja.netlify.com/#optimistic-locking)を参考にして、RESTful APIの更新をするにおいて、楽観ロックによって整合性の担保をする場合に、どのような考え方があるかを見ていきます。
その後、実際に楽観ロックを実装した場合どのようになるのか、と、Zalandoで明確に述べられていない部分の考察をしていきます。

# 楽観ロックの実装方法について

[ZalandoのRESTful APIに関するガイド](https://restful-api-guidelines-ja.netlify.com/#optimistic-locking)では、楽観ロックの実現方法について、以下の4つが紹介されています。

* If-MatchヘッダとETagヘッダ
* 結果エンティティにおけるETags
* バージョン番号
* Last-Modified / If-Unmodified-Since

API経由で一覧情報を取得し、その中の1件を更新する、というケースでこれら4つがどのように楽観ロックを実現するか見ていきます。

## If-MatchヘッダとETagヘッダ

更新の流れ、楽観ロックの流れは以下の図のとおりです。
`Etagヘッダを付けて返却`部分でclient側に返却された値と、`If-Matchヘッダを付けて更新`で`If-Match`に付与されている値の一致・不一致をみて、API serverが更新するか否かを判断しています。

![If-Match-and-Etag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/a5fb4b53-953b-c4e0-f7f0-5644976bd166.png)

## 結果エンティティにおけるETags

更新の流れ、楽観ロックの流れは以下の図のとおりです。
`一覧返却`で、該当のエンドポイントに紐づくリソースを全取得します。そのときに、各リソースの値をseedにして、何らかのアルゴリズムでetagを作成し、レスポンスに含めます。
API serverでは、`If-Matchヘッダを付けて更新`での`If-Match`に付与されている値と、更新要求があったリソースの再計算されたetag値が同じか否かで更新可否を判断します。

![結果エンティティにおけるETags.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/549066bb-8dc0-566f-7080-f8ff3e8ae191.png)


## バージョン番号

更新の流れ、楽観ロックの流れは以下の図のとおりです。
clientは`バージョン番号を含んだ一覧を返却`で、該当のエンドポイントに紐づくリソースをバージョン番号を含んだ形で全取得します。
API serverでは、`更新`で送られてきたバージョン番号と、更新要求があったリソースのバージョン番号が同じか否かで更新可否を判断します。
更新がなされた場合は該当のリソースのバージョン番号をインクリメントします。


![バージョン番号.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/349c716d-4d2e-67ae-4dc8-8957bedb706d.png)

## Last-Modified / If-Unmodified-Since
更新の流れ、楽観ロックの流れは以下の図のとおりです。
clientは`Last-Modifiedヘッダを付けて一覧返却`で、該当のエンドポイントに紐づくリソースを全取得します。そのときに、`Last-Modified`ヘッダにてリソースの最終更新日時を付与しておきます。
API serverでは、`If-Unmodified-Since`(=`Last-Modified`の値)で送られてきた日時と、更新要求があったリソースの最終更新日時を比較して、更新可否を判断します。(リソースの最終更新日時の方が新しい場合は更新不可と判断します)


![Last-Modified_If-Unmodified-Since.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/8a44cb1f-8745-06fa-6b0d-2ff794d7800d.png)

# 各手法のPros/Cons

* Zalandoによると、各手法のメリット・デメリットは以下のように整理され、推奨する方法としては、**結果エンティティにおけるETags**と**Last-Modified / If-Unmodified-Since**であるようです。

| 楽観ロック実現方法 | メリット | デメリット | 
| :---              | :---    | :---      |
| If-MatchヘッダとETagヘッダ | ・RESTfulな解決手段 | ・リクエスト数が多くなる |
| 結果エンティティにおけるETags | ・パーフェクトな楽観ロックである | ・HTTPヘッダに付与すべき情報が、業務オブジェクトに入り込んでしまう |
| バージョン番号 | ・パーフェクトな楽観ロックである | ・HTTPヘッダに付与すべき情報が、業務オブジェクトに入り込んでしまう |
| Last-Modified / If-Unmodified-Since | ・昔から使用されているので実績がある<br>・業務オブジェクトに干渉しない<br>・実装が容易<br>・更新リクエスト以外の追加リクエストが不要 | ・API側が複数インスタンスから構成されている場合、厳密な時刻同期が必要となる |

# `結果エンティティにおけるETags`による楽観ロックの具体例

以下ソースで`optimistic/etag`というエンドポイントで一覧取得と更新について実装しています。
https://github.com/nannany/optimistic-lock-demo
ETag生成には以下のようにsha256で行っています。

```java

package nannany.optimistic.demo.util;

import com.google.common.hash.Hashing;

import static java.nio.charset.StandardCharsets.UTF_8;

public final class DemoUtils {

    @SuppressWarnings("UnstableApiUsage")
    public static String getHash(String origin) {
        return Hashing.sha256().hashString(origin, UTF_8).toString();
    }
}
```

以下のgifように、ETag値が一致すれば更新成功し、不一致なら409が返されます。

![etag2019-09-23 17-43.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/0abeb687-c31a-769f-0bc2-f5cbb507fea3.gif)


# `Last-Modified / If-Unmodified-Since`による楽観ロックの具体例

以下ソースで`optimistic/lastModify`というエンドポイントで一覧取得と更新について実装しています。
https://github.com/nannany/optimistic-lock-demo
最終更新日時には一覧取得を実施した日時を入れ、それ以降にリソースに対して更新が入っていないかで、楽観ロックを行っています。

以下のgifように、更新対象リソースの最終更新日時が`If-Unmodified-Since`ヘッダで受け取った日時以前であれば更新成功し、そうでない場合は412が返されます。

![lastModify2019-09-23 17-55.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/441085/5d445df0-79a0-61ba-fd16-0b764ffefe9c.gif)

# 考察
楽観ロックの各手法のうち、Zalandoには明記されていない部分について考察を加えてみます。

## `結果エンティティにおけるETags`と`バージョン番号`での違いについて
Zalandoにおいては、上記2手法はどちらも、
メリット： パーフェクトな楽観ロックである
デメリット： HTTPヘッダに付与すべき情報が、業務オブジェクトに入り込んでしまう
とあり、明確にメリットデメリットの差異が示されていません。（パーフェクトな楽観ロックである、については詳細な説明がないのでいまいちよく分からないですが）

この2手法のメリット・デメリットの差異は以下の2点にあると考えています。

* API server側で楽観ロックを検知するための要素を保持せねばならないか
* 更新可否を判断する値の類推難易度

### API server側で楽観ロックを検知するための要素を保持せねばならないか

2手法とも、clientに返却される一覧情報に、業務オブジェクト外の要素（ETagとversion）が含まれてしまいます。
しかし、API server側において、ETagの値は業務オブジェクトの何らかの値をseed値として生成できる、つまり、ETagの値を保持しておく必要がないのに対し、versionの値についてはAPI server側で保持しておく必要があります。
一方、ETagの値をAPI server側に保持しないと決めた場合には、一覧情報取得の際に都度都度ETagの計算をしなければならないので、性能的に大丈夫なのかは考えておく必要がありそうです。

永続化情報をいかにシンプルにするか、性能をどうするか、を考える際には、このあたりのことがメリット・デメリットになるのではないでしょうか。

### 更新可否を判断する値の類推難易度

バージョン番号は基本的には更新のたびに1ずつインクリメントされていく値であり、更新することが可能なバージョン番号の類推は比較的容易いです。
一方で、ETagの値を類推するにあたっては、ETag生成に用いられるアルゴリズム、ETagの生成時に使用されるseedが何か、更新時のseedの値、が必要となり、バージョン番号の類推に比べて難しいと考えられます。

セキュリティに関することを考える際には、このあたりのことがメリット・デメリットになるのではないでしょうか。

## `Last-Modified / If-Unmodified-Since`のセキュリティ的な面について

`Last-Modified / If-Unmodified-Since`では、client側から`If-Unmodified-Since`ヘッダの値を書き換えて、だいぶ先の未来を指定すれば(9999年12月31日とか)、リソースの値が`Last-Modified`以降に更新されていようと、その上から更新することが可能になってしまいます。

よって、セキュリティ的な面を見ると、`Last-Modified / If-Unmodified-Since`は強くないと考えています。

# まとめ

Zalandoに載っている楽観ロック手法について見ていきましたが、結局は自分が構築しているシステムで求められることに合わせて取るべき手法を考える他ないと思いました。Zalandoでも4種類手法を紹介しているということは、あらゆるシステムでベストな楽観ロック実装手法が編み出されているわけではないということなのでしょう。

ただ、0からRESTful APIの楽観ロック実装方法を考えていくのではなく、Zalando等の記述を参考にし、紹介されているメリット・デメリットを自システムに当てはめて調査しながら実装を勧めていくのが現実的な解なのではないでしょうか。

# 参考

[Zalando](https://restful-api-guidelines-ja.netlify.com/#optimistic-locking)
[理系学生日記](https://kiririmode.hatenablog.jp/entry/20180917/1537148448)
[RFC7232](https://tools.ietf.org/html/rfc7232)