NHibernate HiLo生成器生成重复Id's

本文关键字:Id HiLo NHibernate | 更新日期: 2023-09-27 18:11:56

我有一个运行在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被设置为兼容级别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不应该防止复制吗?

EDIT:重复的id不是只发生在一个表上,而是发生在频繁插入和删除的表上。上面的代码是嫌疑中最简单的,而且非常简单——它只需要.Get()一个父节点来检查它是否在那里,然后在新实体上创建并调用.Save()以及审计跟踪行(它使用nHibernate中的PostInsert事件监听器)。

EDIT2: id -上述类型的映射(跨所有实体使用):

    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
    }
}

NHibernate HiLo生成器生成重复Id's

我找到了问题所在。事实证明,它与Id无关。

作为insert语句的一部分,我们更新了一个控制数字序列的二级表。如果二级表遇到快照隔离错误,那么问题就会发生——因为所有事情都是在nHibernate的SQLCommandSets内部处理的——错误会在链上弹出一个错误的描述。

考虑到对这个问题的评论链,我认为目前有两种可能的情况。

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


这个问题的另一个角度假设一个已经插入的对象被加载,然后从会话中分离出来,但是(通过设计或意外)在一个(相同/不同的)会话上重新连接,然后立即尝试插入该对象。这个可以发生,假设的场景可以是

  1. var entity123 = Get(123)
  2. var entity123 = entity123. clone()或session . evict (entity123).
  3. 在某个地方你调用SaveOrUpdate(entity123)(或者更糟糕的跟踪你添加它与级联保存规则的引用集合)
  4. NHibernate看到一个有标识符的非托管对象,尝试插入它。

在一些早期版本的NHibernate中,我确实在非身份插入中看到了这种行为。

上面的情况也可能发生在同样复制标识符的坏/愚蠢工厂方法上。

要跟踪此检查是否插入SQL参数(对于log4net,将是一个NHibernate)。带有调试的SQL条目(尽管我认为NHibernate分析器也会暴露这一点)匹配现有行的列值。如果它们完全匹配,那么可能会发生上述情况。如果它们部分匹配,也许您对实体进行部分复制,它也会错误地复制ID。

如果改成:

postPreview.Index = index+1;