Note that there are some explanatory texts on larger screens.

plurals
  1. POCode First - Retrieve and Update Record in a Transaction without Deadlocks
    text
    copied!<p>I have a EF code first context which represents a queue of jobs which a processing application can retrieve and run. These processing applications can be running on different machines but pointing at the same database. </p> <p>The context provides a method that returns a <code>QueueItem</code> if there is any work to do, or null, called <code>CollectQueueItem</code>.</p> <p>To ensure no two applications can pick up the same job, the collection takes place in a transaction with an <code>ISOLATION LEVEL</code> of <code>REPEATABLE READ</code>. This means that if there are two attempts to pick up the same job at the same time, one will be chosen as the <code>deadlock victim</code> and be rolled back. We can handle this by catching the <code>DbUpdateException</code> and return <code>null</code>.</p> <p>Here is the code for the <code>CollectQueueItem</code> method:</p> <pre><code>public QueueItem CollectQueueItem() { using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead })) { try { var queueItem = this.QueueItems.FirstOrDefault(qi =&gt; !qi.IsLocked); if (queueItem != null) { queueItem.DateCollected = DateTime.UtcNow; queueItem.IsLocked = true; this.SaveChanges(); transaction.Complete(); return queueItem; } } catch (DbUpdateException) //we might have been the deadlock victim. No matter. { } return null; } } </code></pre> <p>I ran a test in LinqPad to check that this is working as expected. Here is the test below:</p> <pre><code>var ids = Enumerable.Range(0, 8).AsParallel().SelectMany(i =&gt; Enumerable.Range(0, 100).Select(j =&gt; { using (var context = new QueueContext()) { var queueItem = context.CollectQueueItem(); return queueItem == null ? -1 : queueItem.OperationId; } }) ); var sw = Stopwatch.StartNew(); var results = ids.GroupBy(i =&gt; i).ToDictionary(g =&gt; g.Key, g =&gt; g.Count()); sw.Stop(); Console.WriteLine("Elapsed time: {0}", sw.Elapsed); Console.WriteLine("Deadlocked: {0}", results.Where(r =&gt; r.Key == -1).Select(r =&gt; r.Value).SingleOrDefault()); Console.WriteLine("Duplicates: {0}", results.Count(r =&gt; r.Key &gt; -1 &amp;&amp; r.Value &gt; 1)); //IsolationLevel = IsolationLevel.RepeatableRead: //Elapsed time: 00:00:26.9198440 //Deadlocked: 634 //Duplicates: 0 //IsolationLevel = IsolationLevel.ReadUncommitted: //Elapsed time: 00:00:00.8457558 //Deadlocked: 0 //Duplicates: 234 </code></pre> <p>I ran the test a few times. Without the <code>REPEATABLE READ</code> isolation level, the same job is retrieved by different theads (seen in the 234 duplicates). With <code>REPEATABLE READ</code>, jobs are only retrieved once but performance suffers and there are 634 deadlocked transactions.</p> <p>My question is: is there a way to get this behaviour in EF without the risk of deadlocks or conflicts? I know in real life there will be less contention as the processors won't be continually hitting the database, but nonetheless, is there a way to do this safely without having to handle the DbUpdateException? Can I get performance closer to that of the version without the <code>REPEATABLE READ</code> isolation level? Or are Deadlocks not that bad in fact and I can safely ignore the exception and let the processor retry after a few millis and accept that the performance will be OK if the not all the transactions are happening at the same time?</p> <p>Thanks in advance!</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload