NHibernate HiLo生成器生成重复的Id

时间:2016-10-07 09:03:11

标签: c# sql-server-2008 nhibernate

我有一个在nHibernate v4.0.4.4000上运行的应用程序 - 它在三个单独的Web服务器上运行。对于ID生成,我使用默认的HiLo实现(跨表的唯一ID)。

有时,在使用以下堆栈跟踪保存新实体时,它会生成重复的ID:

at NHibernate.AdoNet.SqlClientBatchingBatcher.DoExecuteBatch(IDbCommand ps)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchWithTiming(IDbCommand ps)
at NHibernate.AdoNet.AbstractBatcher.ExecuteBatch()
at NHibernate.AdoNet.AbstractBatcher.PrepareCommand(CommandType type, SqlString sql, SqlType[] parameterTypes)
at NHibernate.AdoNet.AbstractBatcher.PrepareBatchCommand(CommandType type, SqlString sql, SqlType[] parameterTypes)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Boolean[] notNull, Int32 j, SqlCommandInfo sql, Object obj, ISessionImplementor session)
at NHibernate.Persister.Entity.AbstractEntityPersister.Insert(Object id, Object[] fields, Object obj, ISessionImplementor session)
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
at NHibernate.Impl.SessionImpl.Flush()
at Xena.Database.Main.Listeners.Strategies.CreateEntityAuditTrailStrategy.Execute(Object criteria) in K:\Projects\Xena\WorkDir\src\Xena.Database.Main\Listeners\Strategies\CreateEntityAuditTrailStrategy.cs:line 41
at Xena.Domain.Rules.Strategies.StrategyExtensions.Execute[TCriteria](IEnumerable`1 strategies, TCriteria criteria) in K:\Projects\Xena\WorkDir\src\Xena.Domain\Rules\Strategies\RelayStrategy.cs:line 55
at NHibernate.Action.EntityInsertAction.PostInsert()
at NHibernate.Action.EntityInsertAction.Execute()
at NHibernate.Engine.ActionQueue.Execute(IExecutable executable)
at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
at NHibernate.Engine.ActionQueue.ExecuteActions()
at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
at NHibernate.Event.Default.DefaultAutoFlushEventListener.OnAutoFlush(AutoFlushEvent event)
at NHibernate.Impl.SessionImpl.AutoFlushIfRequired(ISet`1 querySpaces)
at NHibernate.Impl.SessionImpl.List(CriteriaImpl criteria, IList results)
at NHibernate.Impl.CriteriaImpl.List(IList results)
at NHibernate.Impl.CriteriaImpl.UniqueResult[T]()
at Xena.Web.EntityUpdaters.LedgerPostPreviewUpdater.TryCreateNewFiscalEntity(ISession session, FiscalSetup fiscalSetup, LedgerPostPreview& entity, IEnumerable`1& errors) in K:\Projects\Xena\WorkDir\src\Xena.Web\EntityUpdaters\LedgerPostPreviewUpdater.cs:line 52
at Xena.Web.SecurityContext.<>c__DisplayClass8_0`1.<TrySaveUpdate>b__0(ISession session, TEntity& entity, IEnumerable`1& errors) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 235
at Xena.Web.SecurityContext.<>c__DisplayClass41_0`1.<TrySave>b__0(ITransaction tx) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 815
at Xena.Web.SecurityContext.TryWrapInTransaction[T](Func`2 action, T& result, IEnumerable`1& errors, Boolean alwaysCommit) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 804
at Xena.Web.SecurityContext.TrySave[TEntity](IEntityUpdater`1 entityUpdater, EntityCreate`1 create) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 812
at Xena.Web.SecurityContext.TrySaveUpdate[TEntity](IFiscalEntityUpdater`1 entityUpdater) in K:\Projects\Xena\WorkDir\src\Xena.Web\SecurityContext.cs:line 236
at Xena.Web.Api.XenaFiscalApiController.WrapSave[TEntity,TDto](IFiscalEntityUpdater`1 updater, Func`2 get, Action`2 postGet) in K:\Projects\Xena\WorkDir\src\Xena.Web\Api\Abstract\XenaFiscalApiController.cs:line 35
at Xena.Web.Api.ApiLedgerPostPreviewController.Post(LedgerPostPreviewDto ledgerPostPreview) in K:\Projects\Xena\WorkDir\src\Xena.Web\Api\ApiLedgerPostPreviewController.cs:line 79
at lambda_method(Closure , Object , Object[] )
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass10.<GetExecutor>b__9(Object instance, Object[] methodParameters)
at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(HttpControllerContext controllerContext, IDictionary`2 arguments, CancellationToken cancellationToken)
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Controllers.ApiControllerActionInvoker.<InvokeActionAsyncCore>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Controllers.ActionFilterResult.<ExecuteAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.Filters.AuthorizationFilterAttribute.<ExecuteAuthorizationFilterAsyncCore>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()

以下消息:

Message=Violation of PRIMARY KEY constraint 'PK_LedgerPostPreview'. Cannot insert duplicate key in object 'dbo.LedgerPostPreview'. The duplicate key value is (94873244).
The statement has been terminated.

SessionFactory设置为使用SnapshotIsolation,DB设置为compability level 2008(100)

据我所知,hilo值的更新是在与“正常”事务分开的事务中运行的(我尝试过导致异常 - hilo值没有回滚(这是有意义的))

根据NHibernate探查器,针对hilo值运行的服务器的SQL是:

Reading high value: 
select next_hi
from   hibernate_unique_key with (updlock, rowlock)
Updating high value: 
update hibernate_unique_key
set    next_hi = 5978 /* @p0 */
where  next_hi = 5977 /* @p1 - next_hi */

我错过了什么? HiLo不应该防止重复吗?

编辑:重复的ID不仅发生在一个表上,而是发生在具有非常频繁的插入和删除的表中。上面的代码是嫌疑人中最简单的代码,非常简单 - 只有.Get()父母检查它,然后在新实体上创建并调用.Save()以及审计跟踪行(在nHibernate中使用PostInsert eventlistener。

EDIT2:上述类型的Id-Mapping(用于所有实体):

    public static void MapId<TMapping, TType>(this TMapping mapping)
        where TMapping : ClassMapping<TType>
        where TType : class,IHasId
    {
        mapping.Id(m => m.Id, m => m.Generator(Generators.HighLow, g => g.Params(new { max_lo = 100 })));
    }

奇怪的部分是(由于@Dexions评论)当我检查审计跟踪和表时 - 没有任何东西被保留。用于保持的代码如下:

using (var tx = Session.BeginTransaction())
{
    try
    {
        var voucherPreview = Session.Get<VoucherPreview>(voucherPreviewId); //Parent
        var postPreview = //Factory create with the voucherPreview;
        var index = Session.QueryOver<LedgerPostPreview>()
            .Where(lpp => lpp.VoucherPreview == voucherPreview)
            .SelectList(l => l.SelectMax(lpp => lpp.Index))
            .SingleOrDefault<int>() + 1
        postPreview.Index = index;
        // Set a few other properties and check validity
        Session.SaveOrUpdate(postPreview);
    }
    catch(Exception ex)
    {
        //Errorhandling leading to the above stacktrace
    }
}

3 个答案:

答案 0 :(得分:1)

我弄明白了这个问题。事实证明,它与Id无关。

作为insert语句的一部分,我们更新控制数字系列的辅助表。如果该辅助表遇到快照隔离故障,则会出现问题 - 因为所有内容都是在nHibernate内部的SQLCommandSets中处理的 - 错误会在链中出现错误的描述。

答案 1 :(得分:0)

鉴于该问题的评论链,恕我直言,目前我可以想到两种可能的案例。

您可能会错误处理nhibernate会话,并且当在给定实例上发生实际ID生成时,您会遇到隐藏的竞争条件(因为数据库上的ID排序是事务隔离的)。这假定相同的应用程序实例已成功插入{ID = 123},然后尝试插入{ID = 123}的其他对象。您可以将插入跟踪回应用程序实例,以验证插入的重复是否发生在同一实例上。我不太确定这个场景在整个NHibernate管道链上是否合理,但ISession不是线程安全的(这是一个众所周知的事实)。你说这已经运行了4年了(虽然你没有提到bug是否已经存在了很长时间),所以也许最近的提交引入了这种行为(collection.AsParallel()就足以触发它了我相信)?

问题的另一个角度假定已插入的对象已加载然后从ISession中分离,但在(相同/不同)ISession上重新附加(通过设计或意外),然后立即尝试插入物体。这个可以发生,假设情景可能是

  1. var entity123 =获取(123)
  2. var entity123 = entity123.Clone()或ISession.Evict(entity123)。
  3. 您调用SaveOrUpdate(entity123)的某个地方(或者更糟糕的是跟踪您将其添加到带有级联保存规则的引用集合中)
  4. NHibernate看到一个带有标识符的托管对象,试图 插入它。
  5. 在NHibernate的某些早期版本中,我确实看到了非标识插入的这种行为。

    上述情况也可能发生在一个坏/哑工厂方法,也会复制标识符。

    要跟踪此检查,是否插入SQL参数(对于带有debug的NHibernate.SQL条目的log4net,尽管我认为NHibernate探查器也会公开它)与现有行的列值匹配。如果它们完全匹配,那么可能会发生类似上述的事情。如果它们部分匹配,也许您可​​以制作实体的部分副本,并且它也会模糊地复制ID。

答案 2 :(得分:0)

如果您刚刚更改为:

postPreview.Index = index+1;