【问题标题】:Using SQL Server application locks to solve locking requirements使用 SQL Server 应用程序锁解决锁定要求
【发布时间】:2014-06-21 12:29:33
【问题描述】:

我有一个基于 Dynamics CRM 2011 的大型应用程序,它在各个地方都有代码,必须根据某些条件查询记录,如果不存在则创建它,否则更新它。

我所说的这种事情的一个例子类似于这样:

stk_balance record = context.stk_balanceSet.FirstOrDefault(x => x.stk_key == id);
if(record == null)
{    
    record = new stk_balance();
    record.Id = Guid.NewGuid();
    record.stk_value = 100;

    context.AddObject(record);
}
else
{
    record.stk_value += 100;

    context.UpdateObject(record);
}

context.SaveChanges();

就 CRM 2011 实施而言(尽管与此问题不严格相关),代码可以从同步或异步插件触发。问题是代码不是线程安全的,在检查记录是否存在和创建记录之间,另一个线程可能会进入并首先执行相同的操作,从而导致重复记录。

由于系统架构的原因,普通锁定方法不可靠,使用多个线程的各种服务可能都使用相同的代码,并且这些多个服务也在多台机器之间进行负载平衡。

在试图找到解决这个问题的方法时,它不会增加大量额外的复杂性,也不会影响不存在单点故障或可能发生瓶颈的单点的想法,我遇到了使用 SQL Server 应用程序锁的想法。

我想出了以下课程:

public class SQLLock : IDisposable
{
    //Lock constants
    private const string _lockMode = "Exclusive";
    private const string _lockOwner = "Transaction";
    private const string _lockDbPrincipal = "public";

    //Variable for storing the connection passed to the constructor 
    private SqlConnection _connection;

    //Variable for storing the name of the Application Lock created in SQL
    private string _lockName;

    //Variable for storing the timeout value of the lock
    private int _lockTimeout;

    //Variable for storing the SQL Transaction containing the lock
    private SqlTransaction _transaction;

    //Variable for storing if the lock was created ok
    private bool _lockCreated = false;

    public SQLLock (string lockName, int lockTimeout = 180000)
    {
        _connection = Connection.GetMasterDbConnection();
        _lockName = lockName; 
        _lockTimeout = lockTimeout;

        //Create the Application Lock
        CreateLock();
    }

    public void Dispose()
    {
        //Release the Application Lock if it was created
        if (_lockCreated)
        {
            ReleaseLock();
        }

        _connection.Close();
        _connection.Dispose();
    }

    private void CreateLock()
    {
        _transaction = _connection.BeginTransaction();

        using (SqlCommand createCmd = _connection.CreateCommand())
        {
            createCmd.Transaction = _transaction;
            createCmd.CommandType = System.Data.CommandType.Text;

            StringBuilder sbCreateCommand = new StringBuilder();
            sbCreateCommand.AppendLine("DECLARE @res INT");
            sbCreateCommand.AppendLine("EXEC @res = sp_getapplock");
            sbCreateCommand.Append("@Resource = '").Append(_lockName).AppendLine("',");
            sbCreateCommand.Append("@LockMode = '").Append(_lockMode).AppendLine("',");
            sbCreateCommand.Append("@LockOwner = '").Append(_lockOwner).AppendLine("',");
            sbCreateCommand.Append("@LockTimeout = ").Append(_lockTimeout).AppendLine(",");
            sbCreateCommand.Append("@DbPrincipal = '").Append(_lockDbPrincipal).AppendLine("'");
            sbCreateCommand.AppendLine("IF @res NOT IN (0, 1)");
            sbCreateCommand.AppendLine("BEGIN");
            sbCreateCommand.AppendLine("RAISERROR ( 'Unable to acquire Lock', 16, 1 )");
            sbCreateCommand.AppendLine("END");

            createCmd.CommandText = sbCreateCommand.ToString();

            try
            {
                createCmd.ExecuteNonQuery();
                _lockCreated = true;
            }
            catch (Exception ex)
            {
                _transaction.Rollback();
                throw new Exception(string.Format("Unable to get SQL Application Lock on '{0}'", _lockName), ex);
            }
        }
    }

    private void ReleaseLock()
    {
        using (SqlCommand releaseCmd = _connection.CreateCommand())
        {
            releaseCmd.Transaction = _transaction;
            releaseCmd.CommandType = System.Data.CommandType.StoredProcedure;
            releaseCmd.CommandText = "sp_releaseapplock";

            releaseCmd.Parameters.AddWithValue("@Resource", _lockName);
            releaseCmd.Parameters.AddWithValue("@LockOwner", _lockOwner);
            releaseCmd.Parameters.AddWithValue("@DbPrincipal", _lockDbPrincipal);

            try
            {
                releaseCmd.ExecuteNonQuery();
            }
            catch {}
        } 

        _transaction.Commit();
    }
}

我将在我的代码中使用它来创建一个 SQL Server 应用程序锁,使用我正在查询的唯一键作为锁名称,如下所示

using (var sqlLock = new SQLLock(id))
{
    //Code to check for and create or update record here
}

现在这种方法似乎可行,但我绝不是任何类型的 SQL Server 专家,并且对将它放在生产代码附近的任何地方持谨慎态度。

我的问题真的有 3 个部分

1.由于我没有考虑过,这是一个非常糟糕的主意吗?

SQL Server 应用程序锁是否完全不适合此目的?

您一次可以拥有的应用程序锁(具有不同名称)的最大数量是多少?

如果创建了可能大量的锁,是否有性能考虑? 一般方法还有什么问题?

2.上面的解决方案真的实现了吗?

如果 SQL Server 应用程序锁可以像这样使用,我是否真的正确使用了它们?

有没有更好的方法使用 SQL Server 来实现相同的结果?

在上面的代码中,我连接到主数据库并在那里创建锁。这可能会导致其他问题吗?我应该在不同的数据库中创建锁吗?

3.是否可以使用不使用 SQL Server 应用程序锁的完全替代方法?

我无法使用存储过程来创建和更新记录(在 CRM 2011 中不受支持)。

我不想添加单点故障。

【问题讨论】:

    标签: c# sql-server sql-server-2008-r2 dynamics-crm-2011


    【解决方案1】:

    您可以更轻松地做到这一点。

    //make sure your plugin runs within a transaction, this is the case for stage 20 and 40
    //you can check this with IExecutionContext.IsInTransaction
    //works not with offline plugins but works within CRM Online (Cloud) and its fully supported
    //also works on transaction rollback
    
    var lockUpdateEntity = new dummy_lock_entity(); //simple technical entity with as many rows as different lock barriers you need
    lockUpdateEntity.Id = Guid.parse("well known guid"); //well known guid for this barrier
    lockUpdateEntity.dummy_field=Guid.NewGuid(); //just update/change a field to create a lock, no matter of its content
    
    //--------------- this is untested by me, i use the next one
    context.UpdateObject(lockUpdateEntity);
    context.SaveChanges(); 
    //---------------
    
    //OR
    
    //--------------- i use this one, but you need a reference to your OrganizationService
    OrganizationService.Update(lockUpdateEntity);
    //---------------
    
    //threads wait here if they have no lock for dummy_lock_entity with "well known guid"
    
    stk_balance record = context.stk_balanceSet.FirstOrDefault(x => x.stk_key == id);
    if(record == null)
    {    
        record = new stk_balance();
        //record.Id = Guid.NewGuid(); //not needed
        record.stk_value = 100;
    
        context.AddObject(record);
    }
    else
    {
        record.stk_value += 100;
    
        context.UpdateObject(record);
    }
    
    context.SaveChanges(); 
    
    //let the pipeline flow and the transaction complete ...
    

    更多背景信息请参考http://www.crmsoftwareblog.com/2012/01/implementing-robust-microsoft-dynamics-crm-2011-auto-numbering-using-transactions/

    【讨论】:

    • 感谢您的回复,我们实际上在代码的其他部分中使用了该方法。与我见过的其他地方相比,您的代码得到了更好的解释和评论!不幸的是,我们不能在我们产品的特定区域使用它,因为代码也可以从插件外部调用。通过在虚拟实体中创建记录并将我需要的参数传递给它来触发它需要大量的重新架构,这将是有问题的
    • 所以当你更新 dummy_lock_entity 时,在你的事务(即插件)完成之前,其他线程不能更新同一个实体吗?
    猜你喜欢
    • 2019-07-11
    • 2023-03-12
    • 2012-05-22
    • 2019-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多