ロストアップデートと呼ばれるものがあるらしい
15年ほどPostgreSQLを運用していて、最近恥ずかしながら初めて「ロストアップデート」という言葉を目にしました。
へぇーMySQLはREPEATABLE READでこんな事が起こるのかぁー。大変だなあ。と他のサイトで情報を調べながらの帰り道。調べれば調べるほど嫌な予感がしてくる。
早速帰宅後試してみました。手元にあったDocker imageから
実験準備
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.02 sec)
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.20 |
+-----------+
mysql> CREATE TABLE Posts (
-> id INT UNSIGNED NOT NULL PRIMARY KEY,
-> b INT NOT NULL
-> ) ENGINE INNODB;
mysql> INSERT INTO Posts VALUES(1, 100);
と設定しました。
実験開始
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 100 |
+----+-----+
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update Posts set b = b+1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 101 |
+----+-----+
あれ? ああ、なるほど、トランザクションAがコミットする前に一度selectしておく必要があるのか。もう一度。
mysql> update Posts set b = 100 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 100 |
+----+-----+
mysql> update Posts set b = b+1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 100 |
+----+-----+
うんうん。
mysql> update Posts set b = b+1 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 102 |
+----+-----+
1 row in set (0.01 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from Posts where id = 1;
+----+-----+
| id | b |
+----+-----+
| 1 | 102 |
+----+-----+
1 row in set (0.01 sec)
あれれー? 嫌な予感が当たったー。
やっぱりロストアップデートなんて起こらないんだけどー??
これってもしかしてさー、、、Active Recordとかのこういったことをロストアップデートって言ってるの?
irb(main):010:0> post = Post.find(1)
Post Load (3.3ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
irb(main):014:0> post.b += 1
=> 101
irb(main):015:0> post.save
(1.8ms) BEGIN
Post Update (1.9ms) UPDATE "posts" SET "updated_at" = $1, "b" = $2 WHERE "posts"."id" = $3 [["updated_at", "2019-03-26 16:18:35.708693"], ["b", 101], ["id", 1]]
(3.5ms) COMMIT
=> true
これってDBの設計やトランザクション隔離レベルではなくて、find
からsave
の間隔があればあるほどリスク高くなるんじゃ...別トランザクションのコミットが失われたってニュアンスはなんだかなあ...もやっと。
それをSELECT FOR UPDATEで防止するってのもなんだかなあ...もやっと。
DBのレイヤでの解決は、update table set column = column + 1
で良いと思ったのですが、、、
確かにActive Recordでcolumn = column + 1
ってSQLの書き方は調べても分からなかったですが、、、
が、aplicationやlibraryの都合で必要のないロックを取りましょう!ってのは個人的にはよくないと思います。こうゆうのは優秀なruby界隈の人がシュババっと実装してくれそうですし。
ロックについてはこの増田の真似をしないように。
https://anond.hatelabo.jp/20190324181740