75

I have a SQL Server (2012) which I access using Entity Framework (4.1). In the database I have a table called URL into which an independent process feeds new URLs. An entry in the URL table can be in state "New", "In Process" or "Processed".

I need to access the URL table from different computers, check for URL entries with status "New", take the first one and mark it as "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

Since the query and update are not atomic, I can have two different computers read and update the same URL entry in the database.

Is there a way to make the select-then-update sequence atomic to avoid such clashes?

Andre Pena
  • 56,650
  • 48
  • 196
  • 243
Gilad Gat
  • 1,468
  • 2
  • 14
  • 19

5 Answers5

61

I was only able to really accomplish this by manually issuing a lock statement to a table. This does a complete table lock, so be careful with it! In my case it was useful for creating a queue that I didn't want multiple processes touching at once.

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();
}

Update - In Entity Framework 6, especially with async / await code, you need to handle the transactions differently. This was crashing for us after some conversions.

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();
}
jocull
  • 20,008
  • 22
  • 105
  • 149
  • 4
    This is a really good solution. Works much better than the other solutions here using an IsolationLevel (with them I had issues with multiple access) for my case where I must ensure that only one thread can access a table at a time. – Jürgen Bayer Sep 18 '14 at 19:37
  • even with this solution, when both threads A & B execute at the same time, both of them are locked out. Any solution for this scenario? – StackTrace Jun 09 '15 at 08:46
  • @SQL.NETWarrior Are you saying there is a deadlock that occurs? This is meant to be a mutex, so one thread should lock out the other, but then release when it's done. I haven't experienced a deadlock yet, but be careful not to open another context within this when you're querying. I've created deadlocks on accident by not locking in the exact same way each time. It's best if you operate on only one table at a time. – jocull Jun 09 '15 at 12:32
  • This works when your only way is to go through these SELECTs with TABLOCKXs. It does not block SELECTs without hints that you potentially can use in other places in your application or in SQL Mng Studio or elsewhere. – Andrzej Martyna Oct 26 '17 at 18:10
  • Your async issues were possibly related to the `TransactionScope` default options which don't flow the `SynchronizationContext`. Solution: `new TransactionScope(scopeOption, transactionOption, TransactionScopeAsyncFlowOption.Enabled);` – LucidObscurity Feb 15 '19 at 22:50
  • Since your logic only needs to process a fixed set of rows per thread, you could improve concurrency significantly by using hints `XLOCK` and `READPAST` instead of `TABLOCKX`. – Taudris Nov 20 '19 at 17:33
20

The answer that @jocull provided is great. I offer this tweak:

Instead of this:

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

Do this:

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

This is more generic. You can make a helper method that simply takes the table name as a parameter. No need to know of the data (aka any column names), and there is no need to actually retrieve a record down the pipe (aka TOP 1)

Timothy Khouri
  • 31,315
  • 21
  • 88
  • 128
  • why you omit `holdlock`?! without it, the exclusive lock released after the statement finished! – ABS Aug 16 '17 at 06:14
  • 1
    Hi Ali. The lock is kept alive for the entire EF transaction. We use this in production fairly often at my work. You do need to be in a transaction. – Timothy Khouri Aug 17 '17 at 12:08
  • That's cool, I didn't know you could select things that way. I wonder if there's a way to parameterize the table name to safely make this a generic method? – jocull Feb 26 '18 at 18:25
19

I cannot add a comment to Andre's answer, but I am concerned about this comment "The IsolationLevel.RepeatableRead will apply a lock to all rows that are read in such a way that a Thread 2 cannot read from Table A if Table A has been read by Thread 1 and Thread 1 did not complete the Transaction."

The repeatable read only says that you will hold all locks until the end of a transaction. When you use this isolation level in a transaction and read a row (say the max value) a "Shared" lock is issued and will be held until the transaction completes. This shared lock will prevent another thread from updating the row (the update would try to apply an Exclusive lock on the row and that would be blocked by the existing shared lock), but it will allow another thread to read the value (the second thread will put another shared lock on the row - which is allowed (that is why they are called shared locks)). So to make the above statement correct, it would need to say "The IsolationLevel.RepeatableRead will apply a lock to all rows that are read in such a way that a Thread 2 cannot update Table A if Table A has been read by Thread 1 and Thread 1 did not complete the Transaction."

For the original question, you would need to use a repeatable read isolation level AND escalate the lock to an Exclusive lock to prevent two processes from reading and updating the same value. All the solutions would involve mapping EF to custom SQL (as escalating the lock type is not built into EF),. You could use jocull answer or you could use an update with an output clause to lock the rows (update statements always get Exclusive locks and in 2008 or above can return a result set).

Anders
  • 544
  • 2
  • 8
Michael Baer
  • 251
  • 3
  • 3
1

You could try passing a UPDLOCK hint to the database and just lock specific rows.. So that what it selects to update it also aquires an exclusive lock on so it can save its changes (rather than just acquiring a readlock in the beginning, that it later tries to upgrade later when saving). The Holdlock suggested by jocull above is also a good idea too.

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

I strongly advise considering optimistic concurrency: https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx

andrew pate
  • 3,833
  • 36
  • 28
0

Wanted to share my combination of the above answers:

public class Repository<TEntity, TKey>
    : IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
    protected readonly DbContext DbContext;

    ...

    private sealed class Transaction<TTransactionEntity> : IDisposable
    {
        private readonly IDbContextTransaction dbTransaction;

        public Transaction(DbContext context)
        {
            var tableName = context.Model
                .FindEntityType(typeof(TTransactionEntity))
                .GetTableName();

            this.dbTransaction = context.Database
                .BeginTransaction(IsolationLevel.RepeatableRead);

            context.Database
                .ExecuteSqlRaw($"SELECT TOP 0 NULL FROM {tableName} WITH (TABLOCKX)");
        }

        public void Dispose()
        {
            this.dbTransaction.Commit();
            this.dbTransaction.Dispose();
        }
    }

    public IDisposable LockingTransaction()
        => new Transaction<TEntity>(this.DbContext);
}

usage:

using (this.unitOfWork.MyRepository.LockingTransaction())
{
    ...
}
Chris
  • 527
  • 3
  • 15