Как заблокировать объект при использовании балансировки нагрузки

Фон. Я пишу функцию, ставящую длительные операции в очередь, используя С#, и каждая операция разделена на 3 этапа:  
1. операция базы данных (обновление/удаление/добавление данных)
2. длительный расчет с использованием веб-сервиса
3. Операция базы данных (сохранение результата вычисления на шаге 2) в той же таблице db на шаге 1 и проверка согласованности таблицы db, например, на этапе 1 элементы одинаковы (см. Ниже более подробный пример )

Во избежание грязных данных или сбоев я использую объект блокировки (статический одноэлементный объект), чтобы обеспечить 3 шага, которые нужно выполнить как целую транзакцию. Поскольку, когда несколько пользователей вызывают функцию для выполнения операций, они могут изменять одну и ту же таблицу db на разных этапах во время своих собственных операций без этой блокировки, например, user2 удаляет элемент A в своем шаге1, тогда как user1 проверяет, существует ли A еще в его шаг 3. (дополнительная информация: Между тем я использую TransactionScope из фреймворка Entity, чтобы гарантировать, что каждая операция базы данных является транзакцией, но как повторяемая для чтения.)

Однако мне нужно поместить это на платформу облачных вычислений, которая использует механизм балансировки нагрузки, так что фактически мой объект блокировки не вступит в силу, поскольку функция будет развернута на разных серверах.

Вопрос:, что я могу сделать, чтобы сделать объект блокировки работающим при вышеуказанных обстоятельствах?

Ответ 1

Это сложная проблема - вам нужна распределенная блокировка или какое-то общее состояние.

Поскольку у вас уже есть база данных, вы можете изменить свою реализацию из "статической блокировки С#", а вместо нее - для управления вашей блокировкой для всей транзакции.

Вы не говорите, какую базу данных вы используете, но если это SQL Server, то для этого вы можете использовать блокировку приложения. Это позволяет явно "заблокировать" объект, и все остальные клиенты будут ждать, пока этот объект не будет разблокирован. Выезд:

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

Я закодировал пример реализации ниже. Начните два экземпляра, чтобы проверить его.

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