乍一看,我有一个非常简单的问题。我希望能够获得带前缀的唯一键值。我有一个包含'Prefix'和'Next_Value'列的表。
所以你认为你只是启动一个事务,从这个表中获取下一个值,递增表中的下一个值并提交,将前缀连接到值,并保证一系列独特的字母数字键。
然而,在负载下,各种服务器通过ADO.NET点击这个存储过程,我发现它会不时地将相同的密钥返回给不同的客户端。当密钥用作主键时,这当然会导致错误!
我天真地假设BEGIN TRAN ... COMMIT TRAN确保了范围内数据访问的原子性。在调查这个问题时,我发现了事务隔离级别,并添加了SERIALIZABLE作为最严格的限制 - 没有任何乐趣。
Create proc [dbo].[sp_get_key]
@prefix nvarchar(3)
as
set tran isolation level SERIALIZABLE
declare @result nvarchar(32)
BEGIN TRY
begin tran
if (select count(*) from key_generation_table where prefix = @prefix) = 0 begin
insert into key_generation_table (prefix, next_value) values (@prefix,1)
end
declare @next_value int
select @next_value = next_value
from key_generation_table
where prefix = @prefix
update key_generation_table
set next_value = next_value + 1
where prefix = @prefix
declare @string_next_value nvarchar(32)
select @string_next_value = convert(nvarchar(32),@next_value)
commit tran
select @result = @prefix + substring('000000000000000000000000000000',1,10-len(@string_next_value)) + @string_next_value
select @result
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRAN
DECLARE @ErrorMessage NVARCHAR(400);
DECLARE @ErrorNumber INT;
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
DECLARE @ErrorLine INT;
SELECT @ErrorMessage = N'{' + convert(nvarchar(32),ERROR_NUMBER()) + N'} ' + N'%d, Line %d, Text: ' + ERROR_MESSAGE();
SELECT @ErrorNumber = ERROR_NUMBER();
SELECT @ErrorSeverity = ERROR_SEVERITY();
SELECT @ErrorState = ERROR_STATE();
SELECT @ErrorLine = ERROR_LINE();
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END CATCH
这是密钥生成表...
CREATE TABLE [dbo].[Key_Generation_Table](
[prefix] [nvarchar](3) NOT NULL,
[next_value] [int] NULL,
CONSTRAINT [PK__Key_Generation_T__236943A5] PRIMARY KEY CLUSTERED
(
[prefix] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
答案 0 :(得分:1)
你的if块上有一些竞争条件。两个请求同时进入一个新的前缀,两个都可以通过if块。您应该更改此值以始终插入到您的表中,但在插入的where子句中执行检查以确保它不存在。另外我建议使用Exists而不是count(*)= 0。有了Exists,一旦sql找到一行,它就会停止查看。
同样的事情可能发生在你的选择中,你可以让两个线程都选择相同的值,然后一个被阻塞等待更新,但是当它返回时它将返回旧的id。
修改逻辑以首先更新行,然后获取更新它的值
update key_generation_table
set next_value = next_value + 1
where prefix = @prefix
select @next_value = next_value -1
from key_generation_table
where prefix = @prefix
我还会考虑使用ouput语句而不是第二次选择。
我可能会改变这个以使用输出,因为yoru在SQL2005上:
declare @keyTable as table (next_value int)
UPDATE key_generation_Table
set next_value=next_value+1
OUTPUT DELETED.next_value into @keyTable(next_value)
WHERE prefix=@prefix
/* Update the following to use your formating */
select next_value from @keyTable
答案 1 :(得分:1)
尝试使用UPDLOCK提示。
select @next_value = next_value
from key_generation_table WITH(UPDLOCK)
where prefix = @prefix
理想情况下,key_generation_table仅用于此特定存储过程。否则UPDLOCK会增加死锁的可能性。
答案 2 :(得分:0)
在盒子外面思考,你能不能在一个带有AUTO_INCREMENT id的表中添加一行,然后使用id?这保证在负载下是唯一的。然后你可以删除该行(以免表格无休止地增长)。
要回答有关正在发生的事情的问题,transactions aren't critical regions。
<强> SERIALIZABLE 强>
最具限制性的隔离级别。使用时,幻像值不会发生。它会阻止其他用户在完成事务之前将行更新或插入数据集。
此机制旨在防止的问题与您遇到的问题不同。
如果您想采用上面概述的方法,您应该获得关键区域的独占锁定。
答案 3 :(得分:0)
Serializable保存锁,但允许读取。因此,如果非常快速地并行调用proc,则加载中的select / update可能会产生相同的结果。 我想......
如果你这样做,使用有效的语法,你可以结合2。 tablock确保整个表被锁定。这与serializable是不同的,即并发性.tablock是粒度。 此外,您假设密钥在那里..如果需要,添加缺少的前缀。
update
key_generation_table WITH (TABLOCK)
set
@next_value = next_value, next_value = next_value + 1
where
prefix = @prefix
if @@ROWCOUNT = 0
begin
set @next_value = 1
insert into key_generation_table (prefix, next_value) values (@prefix, 1)
end
select @string_next_value = convert(nvarchar(32),@next_value)