我有一个多线程应用程序,它适用于大型数据库(文件大小 > 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)与另一个进程在锁资源上死锁,并已被选为死锁牺牲品。重新运行事务。
于是我看了这篇文章,这个。我完成了所有步骤,发现两个线程在同一个关联对象 ID 上执行相同的页面锁定。我什至不明白它是怎么回事,因为我以编程方式锁定了每个实体更新,所以它甚至不可能是这样的问题。这就是我将数据库更改为快照隔离的原因:
ALTER DATABASE MY_DB SET ALLOW_SNAPSHOT_ISOLATION ON
ALTER DATABASE MY_DB SET READ_COMMITTED_SNAPSHOT ON
我还将 NHibernate IsolationLevel从ReadCommitted更改为Snapshot以解决此问题。在此之后我得到了这个错误:
无法执行命令:UPDATE [ Foo ] SET Field1 = @p0, Field2= @p1 WHERE Id = @p2
System.Data.SqlClient.SqlException (0x80131904):快照隔离事务由于更新冲突而中止。您不能使用快照隔离来访问表 'dbo. Bar ' 直接或间接在数据库 'My_DB' 中更新、删除或插入已被另一个事务修改或删除的行。重试事务或更改更新/删除语句的隔离级别。
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)
我没有DB 实体的Version属性,并且我的代码中没有 OptimisticLock.Version() 行,所以我隐式使用 PessimisticLock。我可以添加Version和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);
}
一切正常,没有任何错误。
为什么我有所有这些错误(我没有更改代码,只是将数据库合并为一个,并且在合并之前一切正常)?为什么即使我使用锁定来防止同时更新不同线程中的实体也会出现此错误。