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;