背景:我正在编写一个函数,使用C#将长时间的操作放入队列中,
每个操作分为3个步骤:
1.数据库操作(更新/删除/添加数据)
2.使用Web服务进行长时间计算
3.数据库操作(保存步骤2的计算结果)在步骤1中的同一个db表上,并检查db表的一致性,例如,步骤1中的项目是相同的(请参阅下面的更详细的示例)
为了避免脏数据或损坏,我使用锁定对象(静态单例对象)来确保作为整个事务完成3个步骤。因为当多个用户正在调用该函数来执行操作时,他们可以在他们自己的操作期间的不同步骤修改相同的db表而没有这个锁定,例如,user2正在他的step1中删除项目A,而user1正在检查A是否仍然存在于他的第3步。(附加信息:同时我正在使用Entity框架中的TransactionScope来确保每个数据库操作都是一个事务,但重复可读。)
但是,我需要把它放到一个使用负载均衡机制的云计算平台上,所以实际上我的锁对象不会生效,因为该功能将部署在不同的服务器上。
问题:如何让我的锁定对象在上述环境下工作?
答案 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();
}
}
}
}