14

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

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

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

问题:我该怎么做才能使我的锁对象在上述情况下工作?

4

1 回答 1

24

这是一个棘手的问题——你需要一个分布式锁,或者某种共享状态。

由于您已经拥有数据库,因此您可以将您的实现从“静态 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();
            }
        }
    }
}
于 2013-09-11T10:17:34.130 回答