Gontanda.rb
# `SELECT FOR UPDATE = 悲観ロック` という理解が誤りであると気づいた話
## 楽観ロック、悲観ロックとは
### 楽観ロック
ビジネストランザクションの競合が少ないという前提に基づき、ビジネストランザクション`終了時` に排他制御を行う
### 悲観ロック
ビジネストランザクションの`実行前`に、排他制御を行う。
### 楽観ロックで困る例
ユーザーの問い合わせを蓄積しているサービスがあるとする。
オペレータは、上から順番にユーザー問い合わせを行うとする。
オペレータが、一つの問い合わせを捌くのにかかる時間は、10minとする。
楽観ロックの場合、業務終了時に競合を検知するため、オペレータのコストが無駄になる。
## デモの前提
```bash
$ mysql --version
mysql Ver 8.0.26 for macos10.15 on x86_64 (Homebrew)
```
```
SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
```
## UPDATE方式
### デモ
#### MySQL
```sql
SELECT * FROM performance_schema.data_locks\G; # 行ロックを確認する
```
```sql
START TRANSACTION;
SELECT @p_id := `id` FROM people limit 1;
SELECT @p_version := `lock_version` FROM people limit 1;
# タイミングずらす
UPDATE `people` SET name = 'a', lock_version = (@p_version + 1) WHERE id = @p_id AND lock_version = @p_version;
commit;
START TRANSACTION;
SELECT @p_id := `id` FROM people limit 1;
SELECT @p_version := `lock_version` FROM people limit 1;
SELECT id, name, lock_version from people where id = @p_id\G;
# タイミングずらす
UPDATE `people` SET name = 'b', lock_version = (@p_version + 1) WHERE id = @p_id AND lock_version = @p_version;
# `Rows matched: 0 Changed: 0 Warnings: 0` を確認する
commit;
```
#### Railsでの実現方法
```ruby
reset_person = Person.first
reset_person.name = "imaharu"
reset_person.save
ActiveRecord::Base.transaction do
person = Person.first
person.name = "a"
sleep 3
person.save
end
ActiveRecord::Base.transaction do
person = Person.first
person.name = "b"
person.save
end
```
Railsの実装は、以下。(多分)
https://github.com/rails/rails/blob/83217025a171593547d1268651b446d3533e2019/activerecord/lib/active_record/locking/optimistic.rb#L100-L108
## SELECT FOR UPDATE方式
### デモ
```sql=
START TRANSACTION;
SELECT @p_id := `id` FROM people limit 1;
SELECT @p_version := `lock_version` FROM people limit 1;
select * from people where id = @p_id and lock_version = @p_version for update;
# Empty set が返るので、それ以降の処理をキャンセルする
UPDATE `people` SET name = 'b', lock_version = (@p_version + 1);
commit;
```
## UPDATEとSELECT FOR UPDATEの違い (もっとあるかも)
UPDATEの前に、CPUなどのリソースを利用しまくるロジックがある時に、SELECT FOR UPDATEだとリソース効率が良くなる。
## なぜ、`SELECT FOR UPDATE = 悲観ロック` となってしまったのか
ビジネストランザクションを意識できていなかったため。
楽観ロックと悲観ロックは、ビジネストランザクションの開始前、終了時どちらで排他制御を入れるかというパターン。
SELECT FOR UPDATEは、How。
## 雑談タイム?に向けて
間違ってるとか、あればガンガン意見ほしいです
------
# デモ
トランザクション分離レベル
```
SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
```
別ターミナルで動かす
```sql
START TRANSACTION;
SELECT @p_id := `id` FROM people limit 1;
SELECT @p_version := `lock_version` FROM people limit 1;
SELECT id, name, lock_version from people where id = @p_id\G;
# タイミングずらす
UPDATE `people` SET name = 'hoge', lock_version = (@p_version + 1) WHERE id = @p_id AND lock_version = @p_version;
SELECT id, name, lock_version from people where id = @p_id;
commit;
```
```sql
START TRANSACTION;
SELECT @p_id := `id` FROM people limit 1;
SELECT @p_version := `lock_version` FROM people limit 1;
SELECT id, name, lock_version from people where id = @p_id\G;
# タイミングずらす
UPDATE `people` SET name = 'fuga', lock_version = (@p_version + 1) WHERE id = @p_id AND lock_version = @p_version;
SELECT id, name, lock_version from people where id = @p_id\G;
COMMIT;
```
SELECT * FROM performance_schema.data_locks\G;
ref: https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
ref: https://dev.mysql.com/doc/refman/5.6/ja/user-variables.html
ref: https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0145
あとで
ref: https://stackoverflow.com/questions/22646226/how-are-locking-mechanisms-pessimistic-optimistic-related-to-database-transact
Railsは、オブジェクトがDirtyである場合、update処理を行う
楽観ロックについて
select * from people where id = 1 and lock_version = 10 for update;
UPDATE `people` SET name = 'a', lock_version = 11 WHERE id = 1 and lock_version = 10;
UPDATE `people` SET name = 'b', lock_version = 11 WHERE id = 1 and lock_version = 10;