如何在使用负载平衡时锁定对象

时间:2013-09-11 09:16:36

标签: c# load-balancing

背景:我正在编写一个函数,使用C#将长时间的操作放入队列中, 每个操作分为3个步骤:  
1.数据库操作(更新/删除/添加数据)
2.使用Web服务进行长时间计算
3.数据库操作(保存步骤2的计算结果)在步骤1中的同一个db表上,并检查db表的一致性,例如,步骤1中的项目是相同的(请参阅下面的更详细的示例)

为了避免脏数据或损坏,我使用锁定对象(静态单例对象)来确保作为整个事务完成3个步骤。因为当多个用户正在调用该函数来执行操作时,他们可以在他们自己的操作期间的不同步骤修改相同的db表而没有这个锁定,例如,user2正在他的step1中删除项目A,而user1正在检查A是否仍然存在于他的第3步。(附加信息:同时我正在使用Entity框架中的TransactionScope来确保每个数据库操作都是一个事务,但重复可读。)

但是,我需要把它放到一个使用负载均衡机制的云计算平台上,所以实际上我的锁对象不会生效,因为该功能将部署在不同的服务器上。

问题:如何让我的锁定对象在上述环境下工作?

1 个答案:

答案 0 :(得分:21)

这是一个棘手的问题 - 您需要分布式锁或某种共享状态。

由于您已拥有数据库,因此您可以从“静态C#锁定”更改您的实现,而是在整个“事务”中为您管理锁定。

您没有说明您使用的数据库,但如果它是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();
            }
        }
    }
}