39

次の構造のクエリがあります。

SELECT ..... WHERE status = 'QUEUED' ORDER BY position ASC LIMIT 1 FOR UPDATE;

これは、InnoDBテーブルに対する単一テーブルのSELECTステートメントです。フィールドposition(INT NOT NULL)にはインデックスがあります。ステータスはENUMであり、インデックスも付けられます。

SELECT ... FOR UPDATEマニュアルページには、読み取ったすべての行がロックされると書かれています。この場合、1つの行だけがロックされることを正しく理解していますか?というか、テーブル全体をロックしますか?

EXPLAINどの行がクエリでロックされるかを決定することは可能ですか?はいの場合-どのように?空のテーブルに対するクエリの説明は、次のことを示しています。

1;'SIMPLE';'job';'index';<null>;'index_position';[34,...];<null>;1;'Using where'
4

4 に答える 4

32

これは素晴らしい質問です。InnoDB は行レベルのロック エンジンですが、バイナリ ログ (レプリケーション、ポイント イン タイム リカバリに使用) の安全性を確保するために、追加のロックを設定する必要があります。それを説明するために、次の(単純な)例を考えてみましょう。

session1> START TRANSACTION;
session1> DELETE FROM users WHERE is_deleted = 1; # 1 row matches (user_id 10), deleted.
session2> START TRANSACTION;
session2> UPDATE users SET is_deleted = 1 WHERE user_id = 5; # 1 row matches.
session2> COMMIT;
session1> COMMIT;

ステートメントは一度コミットされるとバイナリ ログにのみ書き込まれるため、スレーブではセッション #2 が最初に適用され、異なる結果が生成され、データの破損につながります

InnoDB が行うことは、追加のロックを設定することです。がインデックス化されている場合is_deleted、session1 がコミットされる前に、他の誰も変更したり、レコードの範囲に挿入したりis_deleted=1できなくなります。にインデックスがない場合is_deleted、 InnoDB はテーブル全体のすべての行をロックして、リプレイが同じ順序になるようにする必要があります。これは、ギャップをロックすることと考えることができます。これは、行レベルで直接ロックすることとは異なる概念です。

その場合ORDER BY position ASC、 InnoDB は、最小のキー値と「特別な」最小の可能な値の間で新しい行が変更されないようにする必要があります。..のようなことをした場合ORDER BY position DESC、誰もこの範囲に挿入できませんでした。

だからここに解決策があります:

  • ステートメントベースのバイナリロギングは最悪です。行ベースのバイナリ ロギング(MySQL 5.1 から利用可能ですが、デフォルトでは有効になっていません)にすべてが切り替わる未来を本当に楽しみにしています。

  • 行ベースのレプリケーションでは、分離レベルを read-committed に変更すると、一致する 1 つの行だけをロックする必要があります。

  • マゾヒストになりたい場合は、ステートメントベースのレプリケーションでinnodb_locks_unsafe_for_binlogを有効にすることもできます。


4 月 22 日更新: テストケースの改良版をコピーして貼り付けるには (「ギャップ内」を検索していませんでした):

session1> CREATE TABLE test (id int not null primary key auto_increment, data1 int, data2 int, INDEX(data1)) engine=innodb;
Query OK, 0 rows affected (0.00 sec)

session1> INSERT INTO test VALUES (NULL, 1, 2), (NULL, 2, 1), (5, 2, 2), (6, 3, 3), (3, 3, 4), (4, 4, 3);
Query OK, 6 rows affected (0.00 sec)
Records: 6  Duplicates: 0  Warnings: 0

session1> start transaction;
Query OK, 0 rows affected (0.00 sec)

session1> SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
+----+
| id |
+----+
|  1 |
+----+
1 row in set (0.00 sec)

session2> INSERT INTO test values (NULL, 0, 99); # blocks - 0 is in the gap between the lowest value found (1) and the "special" lowest value.

# At the same time, from information_schema:

localhost information_schema> select * from innodb_locks\G
*************************** 1. row ***************************
    lock_id: 151A1C:1735:4:2
lock_trx_id: 151A1C
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
*************************** 2. row ***************************
    lock_id: 151A1A:1735:4:2
lock_trx_id: 151A1A
  lock_mode: X
  lock_type: RECORD
 lock_table: `so5694658`.`test`
 lock_index: `data1`
 lock_space: 1735
  lock_page: 4
   lock_rec: 2
  lock_data: 1, 1
2 rows in set (0.00 sec)

# Another example:
select * from test where id < 1 for update; # blocks
于 2011-04-18T15:06:26.937 に答える
3

私はテストをしました。次の表を作成しました。

id  data1   data2
1   1   2
2   2   1
5   2   2
6   3   3
3   3   4
4   4   3

次に、トランザクションとの最初の接続を作成しました:

SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;

結果は id=1 の行でした。

次に、最初にコミットせずに別の接続から 2 番目のトランザクションを作成しました。

SELECT id FROM test WHERE data1=2 FOR UPDATE;

ブロックしませんでした。そして、最初のトランザクションで選択された行を選択しようとしたときにのみブロックされました。ORDER BY を DESC に変更して次のことを試しましたが、これも機能します。

結論: MySQL は、ORDER BY および LIMIT 句を使用するときに実際に選択された行のみをブロックします。ギャップロックの説明については、@Morgan の回答を参照してください。

MySQL のバージョンは 5.0.45 です

于 2011-04-22T17:54:05.760 に答える
1

他のデータベースとは異なり、MySQL ではクエリがインデックス位置をロックします。statusこれは事実上、現在別のトランザクションと等しい、'QUEUED'または別のトランザクションからの変更を望んでいるすべての行'QUEUED'がロックされていることを意味します。これに対して私が見つけた唯一の解決策は、 なしで行をFOR UPDATE選択し、次に ID ベースのフィルターでそれらを選択し、ロックされたら条件を再確認することです。良くはありませんが、それは仕事をします。

于 2011-04-17T16:23:36.803 に答える