14

Background: I'm writing a function putting long lasting operations in a queue, using C#, and each operation is kind of divided into 3 steps:
1. database operation (update/delete/add data)
2. long time calculation using web service
3. database operation (save the calculation result of step 2) on the same db table in step 1, and check the consistency of the db table, e.g., the items are the same in step 1 (Pls see below for a more detailed example)

In order to avoid dirty data or corruptions, I use a lock object (a static singleton object) to ensure the 3 steps to be done as a whole transaction. Because when multiple users are calling the function to do operations, they may modify the same db table at different steps during their own operations without this lock, e.g., user2 is deleting item A in his step1, while user1 is checking if A still exists in his step 3. (additional info: Meanwhile I'm using TransactionScope from Entity framework to ensure each database operation as a transaction, but as repeat readable.)

However, I need to put this to a cloud computing platform which uses load balancing mechanism, so actually my lock object won't take effect, because the function will be deployed on different servers.

Question: what can I do to make my lock object working under above circumstance?

jay2014
  • 151
  • 1
  • 5
  • Not very clear. Wen each thread/task has its own connection you wouldn't need a lock. – H H Sep 11 '13 at 09:22
  • thanks for the comment, I've updated my description, hope I made this more clear this time – jay2014 Sep 11 '13 at 10:01
  • Still not very clear, what is "the consistency of the db table" ? It's very hard and very inefficient to coordinate actions from the cloud like this so the answer might be in adapting the overall design. – H H Sep 11 '13 at 10:08
  • thanks for your patience, I've updated ag, maybe I do need to change the design, but really want to see if there is any easier sln. – jay2014 Sep 11 '13 at 10:18

1 Answers1

25

This is a tricky problem - you need a distributed lock, or some sort of shared state.

Since you already have the database, you could change your implementation from a "static C# lock" and instead the database to manage your lock for you over the whole "transaction".

You don't say what database you are using, but if it's SQL Server, then you can use an application lock to achieve this. This lets you explicitly "lock" an object, and all other clients will wait until that object is unlocked. Check out:

http://technet.microsoft.com/en-us/library/ms189823.aspx

I've coded up an example implementation below. Start two instances to test it out.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Transactions;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var locker = new SqlApplicationLock("MyAceApplication",
                "Server=xxx;Database=scratch;User Id=xx;Password=xxx;");

            Console.WriteLine("Aquiring the lock");
            using (locker.TakeLock(TimeSpan.FromMinutes(2)))
            {
                Console.WriteLine("Lock Aquired, doing work which no one else can do. Press any key to release the lock.");
                Console.ReadKey();
            }
            Console.WriteLine("Lock Released"); 
        }

        class SqlApplicationLock : IDisposable
        {
            private readonly String _uniqueId;
            private readonly SqlConnection _sqlConnection;
            private Boolean _isLockTaken = false;

            public SqlApplicationLock(
                String uniqueId,                 
                String connectionString)
            {
                _uniqueId = uniqueId;
                _sqlConnection = new SqlConnection(connectionString);
                _sqlConnection.Open();
            }

            public IDisposable TakeLock(TimeSpan takeLockTimeout)
            {
                using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Suppress))
                {
                    SqlCommand sqlCommand = new SqlCommand("sp_getapplock", _sqlConnection);
                    sqlCommand.CommandType = CommandType.StoredProcedure;
                    sqlCommand.CommandTimeout = (int)takeLockTimeout.TotalSeconds;

                    sqlCommand.Parameters.AddWithValue("Resource", _uniqueId);
                    sqlCommand.Parameters.AddWithValue("LockOwner", "Session");
                    sqlCommand.Parameters.AddWithValue("LockMode", "Exclusive");
                    sqlCommand.Parameters.AddWithValue("LockTimeout", (Int32)takeLockTimeout.TotalMilliseconds);

                    SqlParameter returnValue = sqlCommand.Parameters.Add("ReturnValue", SqlDbType.Int);
                    returnValue.Direction = ParameterDirection.ReturnValue;
                    sqlCommand.ExecuteNonQuery();

                    if ((int)returnValue.Value < 0)
                    {
                        throw new Exception(String.Format("sp_getapplock failed with errorCode '{0}'",
                            returnValue.Value));
                    }

                    _isLockTaken = true;

                    transactionScope.Complete();
                }

                return this;
            }

            public void ReleaseLock()
            {
                using (TransactionScope transactionScope = new TransactionScope(TransactionScopeOption.Suppress))
                {
                    SqlCommand sqlCommand = new SqlCommand("sp_releaseapplock", _sqlConnection);
                    sqlCommand.CommandType = CommandType.StoredProcedure;

                    sqlCommand.Parameters.AddWithValue("Resource", _uniqueId);
                    sqlCommand.Parameters.AddWithValue("LockOwner", "Session");

                    sqlCommand.ExecuteNonQuery();
                    _isLockTaken = false;
                    transactionScope.Complete();
                }
            }

            public void Dispose()
            {
                if (_isLockTaken)
                {
                    ReleaseLock();
                }
                _sqlConnection.Close();
            }
        }
    }
}
Daniel James Bryars
  • 4,429
  • 3
  • 39
  • 57
  • It is SQL Server, but I'm already using Transactions for more granular operations, as I stated, so I wonder if using another whole transaction could introduce deadlocks – jay2014 Sep 11 '13 at 10:22
  • That's another route. I'm not suggesting you change the transaction code at all. I'm suggesting to explicitly take a lock out in SQL Server, and explicitly unlock it using an Application Lock. To keep things the same as you have them you wouldn't take this application lock out using a transaction. Do it separately. I'm suggesting replacing the "C# lock" with a "SQL Server Application Lock" as a drop in replacement. – Daniel James Bryars Sep 11 '13 at 10:35
  • To understand if my suggestion will work you will need to do some reading up and playing with Application locks in SQL Server. – Daniel James Bryars Sep 11 '13 at 10:36
  • One major caution here. Locking the DB can bring your user experience to a dead halt. This will be even more pronounced when you move to a cloud environment and begin to scale. I heartily recommend you redesign your DB and application. If you cannot achieve this level of concurrency in a DB, you may need to consider alternatives such as the approach taken with LMAX (http://martinfowler.com/articles/lmax.html). – Tyler Jensen Sep 16 '13 at 04:01
  • @TylerJenson, yes there are different ways to address the poster's underlying design; this answer answers the specific question. Might be worth adding your comment to the question. – Daniel James Bryars Jan 23 '16 at 06:18
  • Why do you suppress the ambient transaction in your solution? is to avoid having the underlying transaction joined and then causing your connection to be closed? – Ibrahim Najjar Aug 16 '17 at 08:05
  • what would need to be changed to make this work for MySQL? – big_water Jan 04 '18 at 19:11
  • @big_water You'd need to use the equivalent feature in MySQL. I haven't tried it, but a quick google search leads me to: https://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock – Daniel James Bryars Jan 05 '18 at 16:39
  • Isn't this approach ruined by connection pooling? ADO.NET manages connections *for you* and reuses existing connections all the time. And when it does that - it calls `sp_reset_connection` which effectively releases any locks – Alex from Jitbit Feb 27 '21 at 23:00