NHibernate锁数据库表,以避免插入"重复"

时间:2017-04-28 15:33:04

标签: c# multithreading nhibernate locking

我认为这是一个常见的问题,但我还没有找到任何解决方案,也许我并没有在谷歌中正确地搜索问题。总之,我有一个在表中插入多行的进程(在同一事务中的许多其他内容中),但该进程在多个线程和多个服务器中执行。

TABLE: COVERAGES
COLUMNS: COV_Id, COV_Description

描述是唯一的,但不是数据库(遗留)中的约束,我想避免插入重复的描述。我已经隔离了搜索并插入了一个独立的事务中,我想在选择之前锁定表格并在" Save"之后解放它。如果它不存在。

我会喜欢(高级别):

{
    this.Lock(Coverage); // Lock table Coverages to avoid select out of this transaction
    Coverage coverage = session.QueryOver<Coverage>().Where(g => g.Description == description).Take(1).SingleOrDefault();
    if (coverage == null)
    {
        this.Save(new Coverage { Description = description });
    }
    return coverage;
};

我不能使用C#的锁定指令,因为这个过程是在多个服务器上执行的,我不能使用NHibernate的Lock指令,因为当我没有结果时,我想要阻止

我使用NHibernate 3.3 for SqlServer和Oracle。

2 个答案:

答案 0 :(得分:2)

我终于在数据库上实现了一个信号量来解决问题。正如我在上面与Frédéric的“讨论”中提到的那样,我需要将线程锁定在select以避免重复插入,Serializable隔离级别,锁定INSERT并在SQL Server上的并发调用中调用insert时抛出死锁异常。通过Oracle的其他方式抛出错误08177. 00000 - “无法序列化此事务的访问”,或者等待另一个事务的结束插入稍后重复的值(请参阅下面的示例sql)。

所以解决方案是这样的:

public Coverage CreateCoverageSessionIsolated(string description, out bool isNew)
{
    Coverage coverage = null;
    bool _isNew = false;
    this.ExecuteOnNewSession((session) =>
    {
        this.semphoresDao.LockSemaphore(session, "SMF_COVERAGES");
        coverage = session.QueryOver<Coverage>()
            .Where(g => g.Description == description)
            .Take(1) 
            .SingleOrDefault();
        _isNew = coverage == null;
        if (coverage == null)
        {
            coverage = new Coverage { Description = description };
            this.Save(coverage);
        }
    });
    isNew = _isNew;
    return coverage;
}

我对实际代码进行了一些调整,以便更好地进行扩展。

  • ExecuteOnNewSession,启动一个新的隔离的ReadCommitted事务。因此它不会干扰打开的事务,以避免不受控制的锁和死锁超时,并减少风险时间。
  • LockSempahore:执行选择查询,锁定特定行。

我已经尝试过,并且在SQL Server和Oracle上运行良好。

修改 要检查Serializable事务的解决方案是否适合我,我会在两个并行执行的并发事务上使用该简单的SQL代码:

BEGIN TRAN; -- ONLY FOR SQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT COV_ID FROM COVERAGES WHERE COV_DESCRIPTION = 'testcov';
INSERT INTO COVERAGES (COV_DESCRIPTION) VALUES ('testcov');
COMMIT;

答案 1 :(得分:1)

您可以使用IsolationLevel.Serializable锁定您的桌子。

using (var t = session.BeginTransaction(IsolationLevel.Serializable))
{
    var coverage = session.QueryOver<Coverage>()
        .Where(g => g.Description == description)
        .Take(1).SingleOrDefault();
    if (coverage == null)
    {
        coverage = new Coverage { Description = description };
        session.Save(coverage);
    }
    t.Commit();
    return coverage;
}

为了限制锁争用,这要求您的表在Description上有一个索引,并且该索引实际上是由读取查询使用的。否则,它将锁定整个表,而不是仅锁定“附近”Description值。阅读更多here。对于比.Net Framework 1更完整的官方文档,请阅读herehere

如果两个或多个进程(或线程)尝试执行并发冲突的插入 1 ,则它们都会遇到死锁。所有这些除了一个将被作为死锁受害者回滚。剩下的一个将继续。

插入时发生死锁,而不是选择。所有进程都会挂在插件上,所有进程都会结束回滚,除外。这样可以确保不会插入重复项。

这意味着完整的处理代码更加精细。

while (true)
{
    using (var session = sessFactory.OpenSession())
    {
        try
        {
            using (var t = session.BeginTransaction(IsolationLevel.Serializable))
            {
                var coverage = session.QueryOver<Coverage>()
                    .Where(g => g.Description == description)
                    .Take(1).SingleOrDefault();
                if (coverage == null)
                {
                    coverage = new Coverage { Description = description };
                    session.Save(coverage);
                }
                t.Commit();
                // Breaks the loop by the way.
                return coverage;
            }
        }
        catch (GenericADOException ex)
        {
            // SQL-Server specific code for identifying deadlocks
            var sqlEx = ex.InnerException as SqlException;
            if (sqlEx == null || sqlEx.Number != 1205)
                throw;
            // Deadlock, just try again by letting the loop go on (eventually
            // log it).
        }
    }
}

注意:
1.根据DB锁定的范围发生冲突,不仅仅是要插入的实际值。强烈建议使用足够的指数来缩小此范围。没有一个可能导致整个表被锁定,导致同时插入不同值的能力非常差。