web-dev-qa-db-ja.com

MySQL:トランザクションとテーブルのロック

データベースの整合性を確保し、SELECTとUPDATEの同期を維持し、他の接続が干渉しないようにするために、トランザクションとテーブルのロックとを少し混同しています。する必要がある:

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) {
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...
}

他のクエリが干渉し、同じSELECT(その接続が行の更新を完了する前に「古い値」を読み取る)が実行されないようにする必要があります。

デフォルトでLOCK TABLES tableに設定して、一度に1つの接続のみがこれを実行し、完了したらロックを解除できることを知っていますが、それはやり過ぎのようです。トランザクションでそれをラップすることは同じことをしますか(他の接続がまだ処理している間、他の接続が同じプロセスを試行しないことを保証します)?または、SELECT ... FOR UPDATEまたはSELECT ... LOCK IN SHARE MODEの方が良いでしょうか?

98
Ryan

テーブルをロックすると、他のDBユーザーがロックした行/テーブルに影響を与えなくなります。しかし、ロック自体は、ロジックが一貫した状態で出力されることを保証しません。

銀行システムについて考えてください。オンラインで請求書を支払うと、取引の影響を受けるアカウントが少なくとも2つあります。アカウントは、お金の受け取り元です。そして、送金先の受取人の口座。そして、銀行の口座には、取引で請求されたすべてのサービス料を喜んで入金します。 (最近誰もが知っているように)銀行は非常に愚かだと考えると、システムは次のように機能するとしましょう。

_$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) {
    charge_huge_overdraft_fees();
}
$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance
_

現在、ロックやトランザクションがないため、このシステムはさまざまな競合状態に対して脆弱です。最大の原因は、アカウントまたは受信者のアカウントで並行して行われる複数の支払いです。コードで残高が取得され、huge_overdraft_fees()などを実行している間、他の支払いが同じタイプのコードを並行して実行する可能性は完全にあります。彼らはあなたの残高を取得し(例えば100ドル)、彼らの取引を行い(あなたが支払っている20ドルと彼らがあなたにねじ込んでいる30ドルを取り出します)、そして両方のコードパスは2つの異なるバランスを持っています:80ドルと70ドル。どちらが最後に終了するかに応じて、アカウント内のこれらの2つの残高のいずれかになりますが、50ドルではなく(100ドル-20ドル-30ドル)になります。この場合、「あなたの都合の良い銀行エラー」。

では、ロックを使用するとしましょう。請求書の支払い(20ドル)が最初にパイプに当たるため、勝ち、アカウントレコードをロックします。これで、排他的に使用できるようになり、残高から20ドルを差し引き、新しい残高を安心して書き戻すことができます。アカウントは予想どおり80ドルになります。しかし...ええと...あなたは受信者のアカウントを更新しようとしますが、それはロックされ、コードが許可するよりも長くロックされ、トランザクションをタイムアウトします...私たちは愚かな銀行を扱っているので、適切なエラーが発生する代わりに処理すると、コードはexit()をプルするだけで、$ 20は電子の塊になります。今、あなたは20ドルを出していますが、あなたはまだ20ドルをレシーバーに借りています、そしてあなたの電話は取り戻されます。

だから...取引を入力してください。取引を開始し、アカウントに20ドルを引き落とし、レシーバに20ドルを入金しようとすると、何かが再び爆発します。ただし、今回は、コードはexit()の代わりにrollbackを実行するだけで、$ 20が魔法のようにアカウントに追加されます。

最終的に、これは次のように要約されます。

ロックは、他の誰かがあなたが扱っているデータベースレコードを妨害するのを防ぎます。トランザクションは、「後の」エラーが、実行した「以前の」ことを妨げるのを防ぎます。どちらも単独で物事が最終的にうまくいくことを保証することはできません。しかし、一緒に、彼らは行います。

明日のレッスン:デッドロックの喜び。

156
Marc B

SELECT ... FOR UPDATE または SELECT ... LOCK IN SHARE MODEあなたが言ったように、トランザクション内では、通常、SELECTがトランザクションにあるかどうかに関係なく、テーブルをロックしません。どちらを選択するかは、トランザクションの進行中に他のトランザクションがその行を読み取れるようにするかどうかによって異なります。

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOTは、他のトランザクションが引き続きその行を変更する可能性があるため、トリックを行いません。これは、以下のリンクの上部に記載されています。

他のセッションが同じテーブルを同時に更新する場合[...]、データベースに存在しない状態のテーブルが表示される場合があります。

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

14
Alison R.

IF NOT EXISTS ...を試行してからINSERTを実行すると、複数のスレッドが同じテーブルを更新しているときに競合状態を引き起こしたときに、同様の問題が発生しました。

私はここで問題の解決策を見つけました: 標準SQLでINSERT IF NOT EXISTSクエリを書く方法

これはあなたの質問に直接答えるものではありませんが、単一のステートメントとしてチェックと挿入を実行するのと同じ原理が非常に便利です。それを変更して更新を実行できるはずです。

6
Tony

トランザクションの概念とロックは異なります。ただし、トランザクションはロックを使用して、ACIDの原則に従うことを支援しました。読み取り/書き込み中に他のユーザーが同じ時点で読み取り/書き込みを行えないようにテーブルを使用する場合は、ロックする必要があります。データの整合性と一貫性を確認する場合は、トランザクションを使用することをお勧めします。ロックを伴うトランザクションの分離レベルの混合概念だと思います。トランザクションの分離レベルを検索してください。SERIALIZEは必要なレベルである必要があります。

4
tczhaodachuan

あなたはロックとトランザクションと混同しています。それらはRMDBの2つの異なるものです。ロックは、トランザクションがデータ分離に焦点を合わせている間、同時操作を防ぎます。 this 明確化のための素晴らしい記事といくつかの優雅な解決策をチェックしてください。

2
David

私は使用します

START TRANSACTION WITH CONSISTENT SNAPSHOT;

そもそも

COMMIT;

で終わる。

間に行うことは、データベースの他のユーザーから隔離されますストレージエンジンがトランザクションをサポートしている場合(これはInnoDBです)。

1