悲観的ロックと楽観的ロックを体験
ここではMySQLを使う。
たとえば: cashに対して+100をするトランザクション処理のとき
排他制御しなかったとき。
code: mysql
初期値は100
mysql> select * from users;
+----+------+
| id | cash |
+----+------+
| 1 | 100 |
+----+------+
1 row in set (0.00 sec)
=====
プロンプトを分かりやすくしておく。
mysql> prompt 1 >
PROMPT set to '1 >'
=====
mysql> prompt 2 >
PROMPT set to '2 >'
=====
1 >start transaction;
Query OK, 0 rows affected (0.01 sec)
=====
1 >update users set cash=cash+100 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
ここで暗黙的に id=1 のレコードに対してロックがかかる。
=====
2 >update users set cash=cash+100 where id=1;
2でアップデートをしようとしても 1 のトランザクションをcommitしてロックを解除するまではクエリは待ちの状態になる。
=====
1 >commit;
Query OK, 0 rows affected (0.01 sec)
=====
1でcommitすると2の処理が勝手にすすむ。
2 >update users set cash=cash+100 where id=1;
Query OK, 0 rows affected (2.34 sec)
Rows matched: 1 Changed: 0 Warnings: 0
jiroshin.icon これではロストアップデートが起こってしまうね。
本来は2つのトランザクション処理でcashが300になって欲しかったのに、ロストアップデートのおかげでcashが200で終わってる。悲c。
ではどうやってロストアップデートを防げば良い??
jiroshin.icon ここで登場するのが楽観的ロックと悲観的ロック。どちらも排他制御の手法。
悲観的ロック
悲観的並行性制御(ひかんてきへいこうせいせいぎょ、pessimistic concurrency control)とは、並行性制御(ロック (情報工学))の手段の種別の一種である。悲観的ロックの概念である。他の処理と競合してはならないトランザクションにおいて、開始時に更新の抑止がされていないことを確認後(抑止されている場合は解除されるまで待機するか、エラーとして処理をあきらめる)他からの更新を抑止し(排他制御)、完了する際に抑止情報を解除する。対照的に楽観的並行性制御がある。 悲観的並行性制御 select ~ for update文(明示的ロック)を使って利用されることが一般的。
楽観的ロック
楽観的並行性制御(らっかんてきへいこうせいせいぎょ、optimistic concurrency control)とは、並行性制御(ロック)の手段の種別の一種である。楽観的ロックの概念である。他の処理と競合してはならないトランザクションにおいて、開始時には特に排他処理など行なわず、完了する際に他からの更新がされたか否かを確認し、もし他から更新されてしまっていたら自らの更新処理を破棄し、エラーとする。対照的に悲観的並行性制御がある。 楽観的並行性制御 悲観的ロックを体感
code: mysql
====
初期値は100です。
1 >select cash from users where id=1;
+------+
| cash |
+------+
| 100 |
+------+
1 row in set (0.01 sec)
======
1 >start transaction;
Query OK, 0 rows affected (0.00 sec)
======
2 >start transaction;
Query OK, 0 rows affected (0.00 sec)
======
1で select ~ for update で悲観的ロック発動。
1 >select cash from users where id=1 for update;
+------+
| cash |
+------+
| 100 |
+------+
1 row in set (0.00 sec)
======
2でも悲観的ロックを発動。1の方でレコードがロックされているため待ちの状態となる。
2 >select cash from users where id=1 for update;
=======
1でcashをアップデート。まだコミットしていないので2は待ち。
1 >update users set cash=cash+100 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
=======
1でコミット。1のロック解除。
1 >commit;
Query OK, 0 rows affected (0.01 sec)
1のロックが解除されたので自動的に2の処理がすすむ。
2 >select cash from users where id=1 for update;
+------+
| cash |
+------+
| 200 |
+------+
1 row in set (21.81 sec)
=====
最後に2でも cash+100 のupdate文を実行。
2 >update users set cash=cash+100 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
そしてcommit。
2 >commit;
Query OK, 0 rows affected (0.00 sec)
======
2つのトランザクションの結果、cashが正しく300で処理が終わっている。
1 >select cash from users where id=1;
+------+
| cash |
+------+
| 300 |
+------+
1 row in set (0.00 sec)
jiroshin.icon ロストアップデートが起きなかった。やったね!
楽観的ロックを体感
code: mysql
楽観的ロックではレコードに更新があったかどうかを判断する指標が必要になる。
そのためここではusersテーブルにversionカラムを追加する。(time_stampとかでもいい)
1 >alter table users add version integer;
Query OK, 0 rows affected (0.08 sec)
Records: 0 Duplicates: 0 Warnings: 0
1 >desc users;
+---------+---------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| cash | int(11) | NO | | NULL | |
| version | int(11) | YES | | NULL | |
+---------+---------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
======
バージョンの初期値を1にしておく。
1 >update users set version=1 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
======
cashの初期値は300です。
1 >select * from users;
+----+------+---------+
| id | cash | version |
+----+------+---------+
| 1 | 300 | 1 |
+----+------+---------+
1 row in set (0.00 sec)
======
1 >start transaction;
Query OK, 0 rows affected (0.00 sec)
======
2 >start transaction;
Query OK, 0 rows affected (0.00 sec)
======
1のトランザクションはまずレコードの情報を取得する。するとid=1, version=1のレコードのcashを+100すれば良いとわかる。
1 >select * from users where id=1;
+----+------+---------+
| id | cash | version |
+----+------+---------+
| 1 | 300 | 1 |
+----+------+---------+
1 row in set (0.00 sec)
=======
2も同じことをする。
2 >select * from users where id=1;
+----+------+---------+
| id | cash | version |
+----+------+---------+
| 1 | 300 | 1 |
+----+------+---------+
1 row in set (0.01 sec)
======
1でidとversionを指定してcashを+100する。ここでversionもインクリメントする。
1 >update users set cash=cash+100, version=2 where id=1 and version=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
======
2でも同じようにupdate文を実行する。しかし1でレコードに対して暗黙的なロックがかかっているため待ちの状態となる。
2 >update users set cash=cash+100, version=2 where id=1 and version=1;
=======
1をcommitする。
1 >commit;
Query OK, 0 rows affected (0.01 sec)
========
すると1のロックが解除されて、2のクエリが自動的にすすむ。
2 >update users set cash=cash+100, version=2 where id=1 and version=1;
Query OK, 0 rows affected (2.38 sec)
Rows matched: 0 Changed: 0 Warnings: 0
しかし、レコードが見つからずupdate文は空振り状態。
=======
2はトランザクションの途中で対象レコードに更新があったことを知ることができた。
そのためトランザクションをrollbackする。
2 >rollback;
Query OK, 0 rows affected (0.00 sec)
========
2はトランザクションをrollbackしたため、リトライする。
2 >start transaction;
Query OK, 0 rows affected (0.00 sec)
2 >select * from users;
+----+------+---------+
| id | cash | version |
+----+------+---------+
| 1 | 400 | 2 |
+----+------+---------+
1 row in set (0.01 sec)
2 >update users set cash=cash+100, version=3 where id=1 and version=2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
2 >commit;
Query OK, 0 rows affected (0.01 sec)
========
最終的にcashを確認すると、2回のトランザクション処理でcash=300から500になっている。
1 >select * from users;
+----+------+---------+
| id | cash | version |
+----+------+---------+
| 1 | 500 | 3 |
+----+------+---------+
1 row in set (0.00 sec)
jiroshin.icon ロストアップデートが発生しなかった!嬉しい!
これが楽観的ロックと悲観的ロックだ。
jiroshin.icon 楽観ロック思想の方がDBに負荷少なくて嬉しいね
ref: