セッション外部化で同時アクセス発生した際の挙動を動かしながら理解する




# わかったこと

同時アクセスが発生した場合、後にリクエストが終了したシーケンスのセッション情報で上書きされる。
理由はおそらく、リクエスト開始時にRedisからデータを取得してSessionオブジェクトを作成し、リクエスト終了時にセッション内容をcommitするため。

そして大きなポイントは２つ。
・Sessionオブジェクトは同時アクセスした同一セッションの処理同士で共有されない。
・Sessionオブジェクトの操作有無に関わらず、リクエスト終了時にセッション内容がcommitされる。

# 検証内容

今回はJavaアプリを使って検証をしています。

## 検証前提

### フレームワーク・ライブラリ

SpringBoot、SpringSessionDataRedisを利用しています。
```groovy:gradle.build
plugins {
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    ...（省略）
}

sourceCompatibility = '18'
...（省略）

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.session:spring-session-data-redis'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    ...（省略）
}
```

セッションはRedisに格納、その他の設定はデフォルト（なのでリクエスト終了時にセッション内容コミット）。
```properties:application.properties
spring.redis.host=localhost
spring.redis.port=6379
spring.session.store-type=redis
```

### アプリ仕様

下記のように４APIを用意。
```
@RestController
public class Controller {

    private static final Logger logger = LoggerFactory.getLogger(Controller.class);

    @PostMapping("/create-user-info")
    public String createUserInfo(HttpSession session){
        logger.info("[create-user-info] セッションにユーザー情報を入れます");
        UserInfo userInfo = getUserInfoMock();
        session.setAttribute("USER_INFO", userInfo);
        logger.info("[create-user-info] セッションにユーザー情報を入れました（id=99,name=taro,role=ADMIN）");
        return "ok";
    }

    @PostMapping("/change-role")
    public String changeRole(HttpSession session) throws Exception{
        logger.info("[change-role] ユーザー情報のロールを変更します");
        UserInfo userInfo = (UserInfo) session.getAttribute("USER_INFO");
        userInfo.setRole("CLIENT");
        session.setAttribute("USER_INFO", userInfo);
        logger.info("[change-role] ユーザー情報のロールを変更しました（role=CLIENT）");

        logger.info("[change-role] 待機を開始します");
        Thread.sleep(10 * 1000);
        logger.info("[change-role] 待機が終了しました");
        return "ok";
    }

    @PostMapping("/change-name")
    public String changeName(HttpSession session) throws Exception{
        logger.info("[change-name] ユーザー情報の名前を変更します");
        UserInfo userInfo = (UserInfo) session.getAttribute("USER_INFO");
        userInfo.setName("new taro");
        session.setAttribute("USER_INFO", userInfo);
        logger.info("[change-name] ユーザー情報のロールを変更しました（name=new taro）");
        logger.info("[change-name] 待機を開始します");
        Thread.sleep(10 * 1000);
        logger.info("[change-name] 待機が終了しました");
        return "ok";
    }

    @GetMapping("/user-info")
    public UserInfo getUserInfo(HttpSession session) throws Exception{
        logger.info("[user-info] 待機を開始します");
        Thread.sleep(10 * 1000);
        logger.info("[user-info] 待機が終了しました");
        return (UserInfo) session.getAttribute("USER_INFO");
    }

    private UserInfo getUserInfoMock(){
        return new UserInfo(99L, "taro", "ADMIN");
    }
}
```

API仕様は下記

 - 情報初期化(create-user-info)
    - id=99,name=taro,role=ADMIN
 - 名前変更(change-name)
   - nameをnew taroに変更
 - ロール変更(change-role)
   - roleをCLIENTに変更
 - 情報確認(user-info)
   - セッション情報を参照し返却

### 利用コマンド

```shell:curl
# 情報初期化
curl -XPOST http://localhost:8080/create-user-info -H 'Cookie: SESSION=NWQ1NmQzZGYtMmFhZi00OTVmLTljNTQtNTUxMzk0M2YxZTNl' -v

# 名前変更
curl -XPOST http://localhost:8080/change-role -H 'Cookie: SESSION=NWQ1NmQzZGYtMmFhZi00OTVmLTljNTQtNTUxMzk0M2YxZTNl' -v

# ロール変更
curl -XPOST http://localhost:8080/change-name -H 'Cookie: SESSION=NWQ1NmQzZGYtMmFhZi00OTVmLTljNTQtNTUxMzk0M2YxZTNl' -v

# 情報確認
curl http://localhost:8080/user-info -H 'Cookie: SESSION=NWQ1NmQzZGYtMmFhZi00OTVmLTljNTQtNTUxMzk0M2YxZTNl' -v
```

## 検証内容
### 並列リクエスト（あるリクエストの処理中に別のリクエストが開始して終了する）
下記のような処理パターン。ある処理中（changeName）に別のリクエスト（changeRole）が開始して終了している。
```plantuml
@startuml
concise changeName
concise changeRole

@changeName
0 is request
2 is {hidden}
3 is "getAttribute"
5 is {hidden}
6 is "changeName"
8 is {hidden}
9 is "setAttribute"
11 is {hidden}
25 is reseponse
27 is {hidden}

@changeRole
0 is {hidden}
11 is "request"
13 is {hidden}
14 is "getAttribute"
16 is {hidden}
17 is "changeName"
19 is {hidden}
20 is "setAttribute"
22 is {hidden}
23 is reseponse
25 is {hidden}
```

この時、下記のように名前だけ変更される。最後にリクエストが終了したchangeNameの更新が反映され、同時実行されたchangeRoleの変更は破棄されている。
| 項目  | Before  | After |
|:-----------:|:------------:|:------------:|
| id       | 99        | 99         |
| name     | taro      | new taro       |
| role       | ADMIN        | ADMIN         |

この結果から後にリクエストが終了したデータで上書きされ、同時アクセスによりデータ整合性が損なわれるケースがあることが分かる。

### 並列リクエスト（あるリクエストの処理中に別のリクエストが開始、開始順に終了する）

```plantuml
@startuml
concise changeName
concise changeRole

@changeName
0 is request
2 is {hidden}
3 is "getAttribute"
5 is {hidden}
6 is "changeName"
8 is {hidden}
9 is "setAttribute"
11 is {hidden}
22 is reseponse
24 is {hidden}

@changeRole
0 is {hidden}
11 is "request"
13 is {hidden}
14 is "getAttribute"
16 is {hidden}
17 is "changeName"
19 is {hidden}
20 is "setAttribute"
22 is {hidden}
24 is reseponse
26 is {hidden}
```

この時、下記のように名前だけ変更される。最後にリクエストが終了したchangeRoleの更新が反映され、最初のchangeNameの変更は破棄されている。
| 項目  | Before  | After |
|:-----------:|:------------:|:------------:|
| id       | 99        | 99         |
| name     | taro      | taro       |
| role       | ADMIN        | CLIENT         |


この結果からchangeRoleでgetAttributeしたデータは、changeNameの変更を反映していないことが分かる。

### 並列リクエスト（あるリクエストの処理中に別のリクエストが開始、開始順に終了する、後続処理はセッション操作をしない）
```plantuml
@startuml
concise changeName
concise showInfo

@changeName
0 is request
2 is {hidden}
3 is "getAttribute"
5 is {hidden}
6 is "changeName"
8 is {hidden}
9 is "setAttribute"
11 is {hidden}
22 is reseponse
24 is {hidden}

@showInfo
0 is {hidden}
11 is "request"
13 is {hidden}
14 is "getAttribute"
16 is {hidden}
19 is {hidden}
20 is "setAttribute"
22 is {hidden}
24 is reseponse
26 is {hidden}
```

この時、下記のように何も変更されない。最後のSessionオブジェクト操作を伴わないshowInfoの取得情報が反映され、changeNameの変更は破棄されている。
| 項目  | Before  | After |
|:-----------:|:------------:|:------------:|
| id       | 99        | 99         |
| name     | taro      | taro       |
| role       | ADMIN        | ADMIN         |

この結果から、Sessionオブジェクトの操作有無に関わらず、セッション情報はcommitされて上書きされることが分かる。