NHibernate下的SQL Server死锁

时间:2013-04-13 10:35:59

标签: sql-server nhibernate transactions locking isolation-level

我有一个多线程应用程序,适用于大型数据库(文件大小> 1 Gb,数据库有38个表,每个表超过500 K实体)。它使用 Castle 3.1.0.0 NHibernate 3.3.1.4000 FluentNibernate 1.3.0.733 SQL Server 2012

以下一种方式配置NHibernate:

config.SetProperty(Environment.CommandTimeout, "300");
config.SetProperty(Environment.BatchSize, "0"); 
config.SetProperty(Environment.GenerateStatistics, "true");
config.SetProperty(Environment.ReleaseConnections, "auto");
config.SetProperty(Environment.UseQueryCache, "true");
config.SetProperty(Environment.SqlExceptionConverter, typeof(MsSqlExceptionConverter).AssemblyQualifiedName);
//...
.MaxFetchDepth(1)

我使用每个线程一个会话(Castle.Windsor)和短交易。每个数据库更新,保存,删除程序由代码锁定:

public abstract class BaseEntityRepository<T, TId> : IBaseEntityRepository<T, TId> where T : BaseEntity<TId> {
    protected static readonly object Locker = new object();

    public bool Save(T item) {
        bool result = false;

        if ((item != null) && (item.IsTransient())) {
            lock (Locker) {
                using (ITransaction tr = Session.BeginTransaction()) {
                    try {                       
                        Session.Save(item);
                        if ((tr.IsActive) && (!tr.WasCommitted) && (!tr.WasRolledBack))
                            tr.Commit();  
                        result = true;
                    } catch {
                        if ((tr.IsActive) && (!tr.WasCommitted) && (!tr.WasRolledBack))
                            tr.Rollback();
                        Session.Clear();
                        throw;
                    }
                }
            }
        }
        return result;
    }

    //same for delete and update 

    public T Get(TId itemId) {
        T result = default(T);

        try {
            result = Session.Get<T>(itemId);
        } catch {
            throw;
        }
        return result;
    }

    public IList<T> Find(Expression<Func<T, bool>> predicate) {
        IList<T> result = new List<T>();
        try {
            result = Session.Query<T>().Where(predicate).ToList();
        } catch {
            throw;
        }
        return result;
    }
}

在将3个数据库(每个300-400 Mb)合并到一个大数据库(上面描述)之前,一切正常。我通过Microsoft SQL Server Management Studio导出/导入数据向导合并数据库。合并后,我设置了主键,并为 Id 列设置了标识规范。在此之后我得到了许多 SQL Server 错误(这些错误没有被复制,它们似乎不时出现):

  

事务(进程ID X)在锁资源上与另一个进程发生死锁,并被选为死锁牺牲品。重新运行该交易。

所以我看了this articlethis。我做了所有步骤,发现两个线程在同一个associatedObjectId上做同一个页面锁定。我甚至不明白它是怎么回事,因为我已经以编程方式锁定每个实体更新,所以它甚至不能成为这样的问题。这就是我将数据库更改为快照隔离的原因:

  

ALTER DATABASE MY_DB   设置ALLOW_SNAPSHOT_ISOLATION

     

ALTER DATABASE MY_DB   SET READ_COMMITTED_SNAPSHOT ON

我还将NHibernate IsolationLevel ReadCommitted 更改为快照来解决此问题。在此之后我得到了这个错误:

  

无法执行命令:UPDATE [ Foo ] SET Field1 = @ p0,Field2 = @ p1 WHERE Id = @ p2
  System.Data.SqlClient.SqlException(0x80131904):由于更新冲突而导致快照隔离事务中止。您不能使用快照隔离直接或间接访问数据库'My_DB'中的表'dbo。 Bar '来更新,删除或插入已被其他事务修改或删除的行。重试事务或更改更新/删除语句的隔离级别。

in System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
in System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning()
in System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
in System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
in System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
in System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
in System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
in System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
in NHibernate.AdoNet.AbstractBatcher.ExecuteNonQuery(IDbCommand cmd)
  

无法将数据库状态与会话同步   NHibernate.StaleObjectStateException:行已被另一个事务更新或删除(或未保存的值映射不正确):[ Foo#544353 ]

in NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
in NHibernate.Persister.Entity.AbstractEntityPersister.UpdateOrInsert(Object id, Object[] fields, Object[] oldFields, Object rowId, Boolean[] includeProperty, Int32 j, Object oldVersion, Object obj, SqlCommandInfo sql, ISessionImplementor session)
in NHibernate.Persister.Entity.AbstractEntityPersister.Update(Object id, Object[] fields, Int32[] dirtyFields, Boolean hasDirtyCollection, Object[] oldFields, Object oldVersion, Object obj, Object rowId, ISessionImplementor session)
in NHibernate.Action.EntityUpdateAction.Execute()
in NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
in NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
in NHibernate.Engine.ActionQueue.ExecuteActions()
in NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
  

NHibernate.TransactionException:事务未连接或已断开连接

in NHibernate.Transaction.AdoTransaction.CheckNotZombied()
in NHibernate.Transaction.AdoTransaction.Rollback()
in My.DBLayer.Data.Repositories.BaseEntityRepository`2.Update(T item)

我没有数据库实体的版本属性,并且我的代码中没有OptimisticLock.Version()行,所以我使用了PessimisticLock implicity。我可以添加版本 OptimisticLock ,但不要认为这会解决问题。

我试着做简单的测试:

Thread t1 = new Thread(m1);
Thread t2 = new Thread(m2);
t1.Start();
t2.Start();

private static void m1() {
    FooRepository rep1 = BootstrapContainer.Instance.Resolve<FooRepository>();
    Foo foo1 = rep1.Get(1);
    foo1.Field1 = "bbbb";
    Thread.Sleep(60*1000);
    rep1.Update(foo1);
}

private static void m2() {
    FooRepository rep2 = BootstrapContainer.Instance.Resolve<FooRepository>();
    Thread.Sleep(5*1000);
    Foo foo2 = rep2.Get(1);
    foo2.Field2 = "aaaaa";
    Thread.Sleep(5*1000);
    rep2.Update(foo2);
}

一切正常,没有任何错误。

为什么我有这些错误(我没有更改代码,只是将数据库合并为一个,并且在合并之前一切正常)?如果我使用锁定来同时阻止不同线程中的更新实体,为什么会出现这种错误。

1 个答案:

答案 0 :(得分:0)

您可以从查询并行化中获得死锁...也就是说,单个查询在将工作拆分为多个并行操作时可能会自动死锁。我曾经多次遇到这种情况。如果您使用的是hql,则可以在查询/语句中添加OPTION(MAXDOP 1)以查看是否可以解决问题。

或者,您可以将整个服务器设置为MAXDOP 1 ...这意味着您永远不会获得并行性(https://msdn.microsoft.com/en-us/library/ms189094.aspx)。如果不深入分析工作负载,我不建议这样做,但它可以帮助您查看并行性是否是死锁的来源。