【问题标题】:NHibernate HiLo generator generates duplicate Id'sNHibernate HiLo 生成器生成重复的 Id
【发布时间】:2017-02-16 05:28:57
【问题描述】:

我有一个在 nHibernate v4.0.4.4000 上运行的应用程序 - 它在三个单独的网络服务器上运行在生产环境中。对于 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 不应该防止重复吗?

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

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

【问题讨论】:

  • 高/低配置值是否在所有应用程序实例上设置为完全相同的值?
  • 是的 - 全部设置为相同的值 - 我们在所有三个实例上运行相同的代码,并且值在代码中设置。
  • 那么鉴于您提供的 SQL(来自分析器),是什么阻止了两个事务读取相同的 next_hi 值?
  • 并发插入?
  • 你能找出重复的是否是同一个应用程序插入的吗?

标签: c# sql-server-2008 nhibernate


【解决方案1】:

我发现了问题所在。事实证明,这与身份无关。

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

【讨论】:

  • 您肯定遗漏了一些东西...您在问题中发布的错误消息是由 SQL 服务器生成的,而不是由 NHibernate 生成的,因此确实尝试了重复键插入。
  • 除非我从探查器查看日志 - 那是罪魁祸首。如果我删除那个竞争条件 - 一切正常。我有测试来证明这一点。
  • 所以是 SQL Server 漏报了?
  • 据我所知 - 它是 sql server 的驱动程序 - 或 sql server it-self。可能是 sql-server 报告了多个错误,并且驱动程序以某种方式破坏了它们。我真的不知道如何区分。
【解决方案2】:

鉴于问题的评论链,恕我直言,目前我能想到两种可能的情况。

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


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

  1. var entity123 = Get(123)
  2. var entity123 = entity123.Clone() 或 ISession.Evict(entity123)。
  3. 在您调用 SaveOrUpdate(entity123) 的某处(或者更糟糕的是,您将其添加到具有级联保存规则的引用集合上)
  4. NHibernate 看到一个带有标识符的托管对象,尝试 插入它。

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

上述情况也可能发生在也复制标识符的坏/哑工厂方法中。

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

【讨论】:

  • 一些笔记。 ISession 不是线程安全的——但这里的一切都发生在一个线程中的一个事务中。这是一个原始插入,因此在有问题的对象上没有 .Get(因为我们首先知道插入后的 Id,因此无法猜测)。我们在代码库的这个特定部分中不使用 .Merge 或 .Evict ,并且该对象没有集合,也不是集合的一部分。 Id 属性只被 nHibernate 触及(它有一个受保护的设置器,所以我们不能意外设置它)
  • 该错误已在大约 3 个月前浮出水面 - 随着系统负载的增加,它的频率也在增加。而且它是间歇性的 - 因此该错误仅有时会发生并且无法在本地计算机上重现。据我从源代码中可以看出,nHibernate .Save 和 .SaveOrUpdate 进行完全相同的调用 - 所以我有点难过。
  • 顺便说一句 - id 生成只涉及每 100 个实体的数据库(根据 hilo 算法的设计) - 并且应该是线程安全的。
  • @Goblin 很好,我运行了一个带有多个并发插入的控制台实用程序,但我无法重现 PK 违规
  • 这听起来像是我自己的测试 :-) 就好像,重负载会导致一种“影子”-flush 形式,因此发送了两个 INSERT 语句而不是一个 - 但我无法弄清楚,这怎么可能。
【解决方案3】:

如果你刚刚改成:

postPreview.Index = index+1;

【讨论】:

  • 我看不出这会如何改变任何事情。这里的问题是 .Id - 而不是索引 :-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
  • 2018-03-15
  • 1970-01-01
相关资源
最近更新 更多