web-dev-qa-db-ja.com

データベースの同時更新に対処するにはどうすればよいですか?

SQLデータベースの同時更新を処理する一般的な方法は何ですか?

次のような単純なSQLスキーマ(制約とデフォルトは表示されていません。)を考えます。

create table credits (
  int id,
  int creds,
  int user_id
);

その目的は、ユーザーのある種のクレジットを保存することです。 Stackoverflowの評判のようなもの。

そのテーブルへの同時更新に対処するにはどうすればよいですか?いくつかのオプション:

  • update credits set creds= 150 where userid = 1;

    この場合、アプリケーションは現在の値を取得し、新しい値(150)を計算して、更新を実行しました。他の誰かが同時に同じことをすると、どれが災いを招くでしょう。現在の値の取得とトランザクションの更新をラップすることでそれが解決されると思います。 Begin; select creds from credits where userid=1; do application logic to calculate new value, update credits set credits = 160 where userid = 1; end;この場合、新しいクレジットが0未満かどうかを確認し、負のクレジットが意味をなさない場合は0に切り捨てます。

  • update credits set creds = creds - 150 where userid=1;

    このケースでは、DBが一貫性の問題を処理するため、同時更新を心配する必要はありませんが、credsが負になりやすく、一部のアプリケーションでは意味をなさない可能性があるという欠点があります。

簡単に言えば、上で概説した(非常に単純な)問題を処理するために受け入れられている方法は何ですか。dbがエラーをスローした場合はどうなりますか?

29
Leeeroy

トランザクションを使用:

BEGIN WORK;
SELECT creds FROM credits WHERE userid = 1;
-- do your work
UPDATE credits SET creds = 150 WHERE userid = 1;
COMMIT;

いくつかの重要な注意事項:

  • すべてのデータベースタイプがトランザクションをサポートしているわけではありません。特に、mysqlの古いデフォルトのデータベースエンジン(バージョン5.5.5より前のデフォルト)であるMyISAMにはありません。 mysqlを使用している場合は、InnoDB(新しいデフォルト)を使用します。
  • トランザクションは、制御不能な理由により中止される場合があります。これが発生した場合、アプリケーションは、BEGIN WORKから最初からやり直す準備をする必要があります。
  • 分離レベルをSERIALIZABLEに設定する必要があります。そうしないと、最初の選択で他のトランザクションがまだコミットしていないデータを読み取ることができます(トランザクションはプログラミング言語のmutexとは異なります)。一部のデータベースでは、SERIALIZABLEトランザクションが同時に進行している場合にエラーがスローされ、トランザクションを再起動する必要があります。
  • 一部のDBMSはSELECT .. FOR UPDATEを提供し、トランザクションが終了するまで、selectによって取得された行をロックします。

トランザクションをSQLストアドプロシージャと組み合わせると、後の部分の処理が容易になります。アプリケーションは、トランザクションで単一のストアドプロシージャを呼び出し、トランザクションが中止された場合はそれを再度呼び出します。

26
bdonlan

MySQL InnoDBテーブルの場合、これは実際に設定した分離レベルに依存します。

デフォルトのレベル3(REPEATABLE READ)を使用している場合、トランザクション中であっても、後続の書き込みに影響を与える行をロックする必要があります。あなたの例ではあなたがする必要があります:

SELECT FOR UPDATE creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

レベル4(シリアライズ可能)を使用している場合は、単純なSELECTに続いて更新を行うだけで十分です。 InnoDBのレベル4は、読み取るすべての行を読み取りロックすることによって実装されます。

SELECT creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

ただし、この特定の例では、計算(クレジットの追加)はSQLで実行できるほど単純なので、簡単です。

UPDATE credits set creds = creds - 150 where userid=1;

sELECT FOR UPDATEの後にUPDATEが続くのと同じです。

16
Savant Degrees

トランザクション内でコードをラップすることは、定義する分離レベルに関係なく、場合によっては十分ではありません(たとえば、本番環境で2つの異なるサーバーにコードをデプロイした場合のイメージング)。

次の手順と2つの同時実行スレッドがあるとします。

1) open a transaction
2) fetch the data (SELECT creds FROM credits WHERE userid = 1;)
3) do your work (credits + amount)
4) update the data (UPDATE credits SET creds = ? WHERE userid = 1;)
5) commit

そして、このタイムライン:

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) and fetches 100
Time =  4; ThreadB executes (2) and fetches 100
Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadB executes (3) and adds 100 + 50
Time =  7; ThreadA executes (4) and updates creds to 150
Time =  8; ThreadB tries to executes (4) but in the best scenario the transaction
          (depending of isolation level) won't allow it and you get an error

このトランザクションにより、credsの値を誤った値で上書きすることができなくなりますが、エラーに失敗したくないので十分ではありません。

代わりに、失敗することのない低速のプロセスを好み、データをフェッチした瞬間に「データベース行ロック」の問題を解決し(ステップ2)、他のスレッドがそれを完了するまで同じ行を読み取れないようにしました。

SQL Serverで実行する方法はいくつかありますが、これはその1つです。

SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1;

この改善で前のタイムラインを再現すると、次のようになります。

Time =  0; creds = 100
Time =  1; ThreadA executes (1) and creates Txn1
Time =  2; ThreadB executes (1) and creates Txn2
Time =  3; ThreadA executes (2) with lock and fetches 100
Time =  4; ThreadB tries executes (2) but the row is locked and 
                   it's has to wait...

Time =  5; ThreadA executes (3) and adds 100 + 50
Time =  6; ThreadA executes (4) and updates creds to 150
Time =  7; ThreadA executes (5) and commits the Txn1

Time =  8; ThreadB was waiting up to this point and now is able to execute (2) 
                   with lock and fetches 150
Time =  9; ThreadB executes (3) and adds 150 + 50
Time = 10; ThreadB executes (4) and updates creds to 200
Time = 11; ThreadB executes (5) and commits the Txn2
11
Abel ANEIROS

新しいtimestamp列を使用した楽観的ロックは、この同時実行の問題を解決できます。

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date
3
Ovidiu Lupas

最初のシナリオでは、where-clauseに別の条件を追加して、同時ユーザーが行った変更を上書きしないようにすることができます。例えば。

update credits set creds= 150 where userid = 1 AND creds = 0;
2
Ola Herrdahl

ランク付けの値への加算または減算が定期的なLIFOいくつかのジョブによる処理のためにキューに入れられるキューイングメカニズムを設定できます。ランクの「バランス」に関するリアルタイム情報が未処理のキューエントリが調整されるまでバランスが計算されないため、これは適合しないことを要求しましたが、即時の調整を必要としないものである場合は機能する可能性があります。

これは、少なくとも外側を見ると、古いPanzer Generalシリーズのようなゲームが個々の動きをどのように処理するかを反映しているようです。一人のプレイヤーの番が上がり、彼らは自分の動きを宣言します。各移動は順番に処理されます。各移動はキューに入れられるため、競合は発生しません。

1
Darth Continent

テーブルは以下のように変更でき、楽観的ロックを処理する新しいフィールドバージョンを導入します。これは、データベースレベルでロックを使用してテーブルクレジット(int id、int creds、int user_id、int version)を作成するよりも、コスト効率が高く効率的な方法です。

creds、user_id、user_id = 1のクレジットからバージョンを選択します。

これがcreds = 100とversion = 1を返すと仮定します

更新クレジットセットcreds = creds * 10、version = version + 1 where user_id = 1およびversion = 1;

常にこれにより、最新のバージョン番号を持っている人は誰でもこのレコードを更新できるだけであり、ダーティーな書き込みは許可されません。

1
amar

ユーザーの現在のクレジットフィールドを要求された量だけ減らすであり、それが正常に減少した場合、他の操作を実行すると問題がある理論的には、減額操作の多くの並列リクエストたとえば、ユーザーが残高に1クレジットを持ち、5つの並列1クレジット請求リクエストがある場合、リクエストが正確に同時に送信され、最終的に-4クレジットになると、5つのアイテムを購入できます。ユーザーのバランス。

これを回避するには、現在のクレジット値を要求された量で減らす必要があります(この例では1クレジット)および現在の値から要求された量を引いた値がゼロ以上かどうかも確認します

UPDATEクレジットSET creds = creds-1 WHERE creds-1> =およびuserid = 1

これは、ユーザーがシステムを実行する場合、ユーザーが少数のクレジットの下で多くのものを購入しないことを保証します。

このクエリの後、現在のユーザークレジットが基準を満たし、行が更新されたかどうかを示すROW_COUNT()を実行する必要があります。

UPDATE credits SET creds = creds-1 WHERE creds-1>=0 and userid = 1
IF (ROW_COUNT()>0) THEN 
   --IF WE ARE HERE MEANS USER HAD SURELY ENOUGH CREDITS TO PURCHASE THINGS    
END IF;

PHP内の同様のことは次のように行うことができます:

mysqli_query ("UPDATE credits SET creds = creds-$amount WHERE creds-$amount>=0 and userid = $user");
if (mysqli_affected_rows())
{
   \\do good things here
}

ここではSELECT ... FOR UPDATEもTRANSACTIONも使用していませんが、このコードをトランザクション内に配置する場合は、トランザクションレベルが常に行からの最新データを提供していることを確認してください(すでにコミットされている他のトランザクションを含む)。 ROW_COUNT()= 0の場合、ROLLBACKを使用することもできます

行ロックなしのWHERE credit- $ amount> = 0の欠点は次のとおりです。

更新後、ユーザーがクレジット残高に十分な量を持っていることが確実にわかりますユーザーが多くのリクエストでクレジットをハックしようとした場合でも、請求前のクレジットが何であったか(更新)、何であったかはわかりませんチャージ後のクレジット(更新)。

注意:

最新の行データを提供しないトランザクションレベル内でこの戦略を使用しないでください。

更新前後の価値を知りたい場合は、この方法を使用しないでください。

ゼロを下回ることなくクレジットが正常に請求されたという事実に依存してみてください。

1
BIOHAZARD

レコードと共に最終更新のタイムスタンプを保存する場合、値を読み取るときにタイムスタンプも読み取ります。レコードを更新するときは、タイムスタンプが一致することを確認してください。誰かがあなたの後ろに来てあなたの前に更新した場合、タイムスタンプは一致しません。

0
Mark Sherretta