我有一个使用事件编号的应用程序(以及其他类型的数字)。这些数字存储在名为“Number_Setup”的表中,该表包含计数器的当前值。
当应用程序生成新事件时,它会在number_setup表中获取所需的数字计数器行(计数器可以每天,每周等重置并存储为int)。然后它会递增计数器并使用新值更新行。
该应用程序是多用户(任何时候大约有100个用户,以及运行和获取100个事件记录并为每个事件请求事件编号的sql作业)。事件表中有一些重复的事件编号,它们不应重复。
存储过程用于检索下一个计数器。
SELECT @Counter = counter, @ShareId=share_id, @Id=id FROM Number_Setup WHERE LinkTo_ID=@LinkToId AND Counter_Type='I' IF isnull(@ShareId,0) > 0 BEGIN -- use parent counter SELECT @Counter = counter, @ID=id FROM Number_Setup WHERE Id=@ShareID END SELECT @NewCounter = @Counter + 1 UPDATE Number_Setup SET Counter = @NewCounter WHERE id=@Id
我现在用一个事务包围了那个块,但是我不完全确定它会100%解决这个问题,因为我认为还有共享锁,所以无论如何都可以读取计数器。
也许我可以在更新声明中检查计数器是否未更新
UPDATE Number_Setup SET Counter = @NewCounter WHERE Counter = @Counter IF @@ERROR = 0 AND @@ROWCOUNT > 0 COMMIT TRANSACTION ELSE ROLLBACK TRANSACTION
我确信这是财务应用程序中发票号码的常见问题 我也不能将逻辑放在代码中并在该级别使用锁定。 我也锁定了HOLDLOCK,但我不确定它的应用程序。它应该放在两个SELECT语句上吗?
如何确保不会创建重复项?
答案 0 :(得分:4)
诀窍是进行计数器更新并读取单个原子操作:
UPDATE Number_Setup SET Counter = Counter+1
OUTPUT INSERTED.Counter
WHERE id=@Id;
虽然这不会将新计数器分配给@NewCounter,而是将其作为结果集返回给客户端。如果必须分配它,请使用中间表变量输出新计数器INTO:
declare @NewCounter int;
declare @tabCounter table (NewCounter int);
UPDATE Number_Setup SET Counter = Counter+1
OUTPUT INSERTED.Counter INTO @tabCounter (NewCounter)
WHERE id=@Id
SELECT @NewCounter = NewCounter FROM @tabCounter;
这解决了使Counter增加原子的问题。您的过程中仍然有其他竞争条件,因为LinkTo_Id和share_id仍然可以在第一次选择后更新,因此您可以递增错误的链接到项目的计数器,但这不能仅从此代码示例中解决,因为它还取决于关于实际更新shared_id和/或LinkTo_Id的代码。
顺便说一句,你应该用一致的案例进入你的领域。如果 一致地命名,则必须使用T-SQL代码中的完全匹配大小写。您的脚本现在运行正常只是因为您有一个不区分大小写的排序规则服务器,如果您在区分大小写的排序规则服务器上进行部署并且您的脚本与字段/表名称的确切情况不匹配,则错误将跟随错误。答案 1 :(得分:0)
您是否尝试过使用GUID而不是自动增量作为唯一标识符?
答案 2 :(得分:0)
如果您有能力修改您的工作以获得多个记录,我会改变想法,以便您的计数器是一个标识列。然后,当您获得下一条记录时,您只需执行插入操作即可获得该表的@@标识。这将确保您获得最大的数字。您还需要执行dbccReseed来重置计数器,而不是仅在需要重置标识时更新表。唯一的问题是,您必须在sql作业中执行100次左右的插入才能获得一组身份。这可能是过多的开销,但使用标识列是获得唯一数字的一种保证方式。
答案 3 :(得分:0)
我可能会遗漏一些东西,但您似乎正在尝试重新发明已被大多数数据库解决的技术。
而不是从Number_Setup表中的“Counter”列读取和更新,为什么不直接使用自动增量主键作为计数器呢?您永远不会有主键的重复值。