web-dev-qa-db-ja.com

Entity Frameworkを使用して、読み取り時にテーブルをロックするにはどうすればよいですか?

Entity Framework(4.1)を使用してアクセスするSQL Server(2012)があります。データベースには、独立したプロセスが新しいURLをフィードするURLというテーブルがあります。 URLテーブルのエントリは、「新規」、「処理中」、または「処理済み」の状態になります。

別のcomputersからURLテーブルにアクセスし、ステータスが「New」のURLエントリを確認し、最初のURLエントリを取得して「In Process」としてマークする必要があります。

var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
if(newUrl != null)
{
    newUrl.StatusID = (int) URLStatus.InProcess;
    dbEntity.SaveChanges();
}
//Process the URL

クエリと更新はアトミックではないため、2つの異なるコンピューターにデータベース内の同じURLエントリを読み取らせて更新させることができます。

そのような衝突を避けるために、選択して更新するシーケンスをアトミックにする方法はありますか?

58
Gilad Gat

ロックステートメントを手動でテーブルに発行することで、これを本当に実現できました。これはcompleteテーブルロックを行うので、注意してください!私の場合、複数のプロセスを一度に触らせたくないというキューを作成するのに役立ちました。

using (Entities entities = new Entities())
using (TransactionScope scope = new TransactionScope())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Complete();
}

更新-Entity Framework 6では、特にasync/awaitコードでは、トランザクションを異なる方法で処理する必要があります。変換後、これはクラッシュしていました。

using (Entities entities = new Entities())
using (DbContextTransaction scope = entities.Database.BeginTransaction())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Commit();
}
50
jocull

アンドレの答えにコメントを追加することはできませんが、このコメントが心配です。スレッド1およびスレッド1によってトランザクションが完了しませんでした。」

繰り返し可能な読み取りは、トランザクションの終了まですべてのロックを保持することを示すだけです。トランザクションでこの分離レベルを使用して行(最大値など)を読み取ると、「共有」ロックが発行され、トランザクションが完了するまで保持されます。この共有ロックは、別のスレッドが行を更新するのを防ぎます(更新は行に排他ロックを適用しようとし、既存の共有ロックによってブロックされます)が、別のスレッドが値を読み取ることを許可します(2番目のスレッド行に別の共有ロックを設定します-これは許可されています(そのため、共有ロックと呼ばれます)。したがって、上記のステートメントを正しくするためには、「IsolationLevel.RepeatableReadは、スレッド2ができないような方法で読み取られるすべての行にロックを適用しますpdate Table A if Table Aスレッド1によって読み取られ、スレッド1はトランザクションを完了しませんでした。」

元の質問では、反復可能な読み取り分離レベルを使用する必要がありますおよび排他ロックにロックをエスカレートするは、2つのプロセスが同じ値を読み取って更新しないようにします。すべてのソリューションでは、EFをカスタムSQLにマッピングする必要があります(ロックタイプのエスカレーションはEFに組み込まれないため)。 jocull answerを使用するか、出力節を含む更新を使用して行をロックできます(更新ステートメントは常に排他ロックを取得し、2008以降では結果セットを返すことができます)。

17
Michael Baer

@jocullが提供した答えは素晴らしいです。私はこの調整を提供します:

これの代わりに:

_"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"
_

これを行う:

_"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"
_

これはより一般的です。テーブル名をパラメーターとして単純に受け取るヘルパーメソッドを作成できます。データ(別名列名)を知る必要はありません。また、パイプ(別名_TOP 1_)でレコードを実際に取得する必要はありません。

13
Timothy Khouri

UPDLOCKヒントをデータベースに渡して、特定の行をロックするだけで済みます。更新を選択すると、排他ロックも取得されるため、変更を保存できます(最初にreadlockを取得するのではなく、後で保存するときにアップグレードを試みます)。上記のジョカルによって提案されたホールドロックも良いアイデアです。

private static TestEntity GetFirstEntity(Context context) {
return context.TestEntities
          .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
          .Single();
}

楽観的な同時実行性を考慮することを強くお勧めします: https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx

0
andrew pate