9

我必须在遗留数据库的表中插入一些记录,并且由于其他古老系统使用它,因此更改表不是解决方案。

问题是目标表有一个 int 主键但没有标识规范。所以我必须找到下一个可用的 ID 并使用它:

select @id=ISNULL(max(recid)+1,1) from subscriber

但是,当我这样做时,我想防止其他应用程序插入表中,这样我们就不会遇到任何问题。我试过这个:

begin transaction
    declare @id as int
    select @id=ISNULL(max(recid)+1,1) from subscriber WITH (HOLDLOCK, TABLOCK)
    select @id
    WAITFOR DELAY '00:00:01'
    insert into subscriber (recid) values (@id)
commit transaction
select * from subscriber

在 SQL Management Studio 的两个不同窗口中,一个事务总是作为死锁牺牲品被杀死。

我也先尝试SET TRANSACTION ISOLATION LEVEL SERIALIZABLE了相同的结果...

关于如何确保我获得下一个 id 并使用它而不冒其他人(或我!)被冲洗掉的风险,有什么好的建议吗?

很抱歉之前没有提到这一点,但这是一个 SQL 2000 服务器,所以我不能使用 FOR UPDATE 和 OUTPUT 之类的东西

更新:这是对我有用的解决方案:

BEGIN TRANSACTION
    DECLARE @id int

    SELECT  @id=recid
    FROM    identities WITH (UPDLOCK, ROWLOCK)
    WHERE table_name = 'subscriber'

    waitfor delay '00:00:06'

    INSERT INTO subscriber (recid) values (@id)

    UPDATE identities SET recid=recid+1 
    WHERE table_name = 'subscriber'

COMMIT transaction

select * from subscriber

WAITFOR 是为了让我可以有多个连接并多次启动查询以引发并发。

感谢 Quassnoi 的回答以及所有其他做出贡献的人!惊人的!

4

4 回答 4

10

Create another table:

t_identity (id INT NOT NULL PRIMARY KEY CHECK (id = 1), value INT NOT NULL)

with a single row, lock this row, and increment value by one each time you need an IDENTITY.

To lock, increment, and return the new value in a single statement, use:

UPDATE  t_identity
SET     value = value + 1
OUTPUT  INSERTED.value

If you don't want to update, just lock, then issue:

SELECT  value
FROM    t_identity WITH (UPDLOCK, ROWLOCK)

This will lock the table until the end of the transaction.

If you always first lock t_identity before messing with ancient_table, you will never get a deadlock.

于 2009-04-15T11:38:24.403 回答
4

添加另一个带有标识列的表,并使用这个新表和列来选择/生成旧表的标识值。

Update: Depending on the frequency of INSERTS (and the number of existing rows e) you could seed your new IDENTITY values at e+x where x is sufficiently large. Thhis would avoid conflict with the legacy inserts. A sad solution, an imperfect one for sure, but something to think about?

于 2009-04-15T11:34:31.123 回答
3

EDIT this is basically the method purposed by @Quassnoi, I just implement it in a loop so you can run it against multiple windows at the same time to see it works great.

set up:

create user's existing table:

create table Subscriber
(
recid  int not null primary key
)

create new table to keep track of the missing identity, you could add an extra column to keep track of a table if this is needed for multiple tables, but I hve not done that in this example:

CREATE TABLE SubscriberIDs
(
SubscriberID int
)
insert into SubscriberIDs values (0) --row must exist first

create test script, put this into multiple windows and run them at the same time:

declare @idtable table --will hold next ID to use
(
id int
)
declare @x  int
declare @y  int
set @x=0
while @x<5000 --set up loop
begin
    set @x=@x+1
    begin transaction
    --get the next ID to use, lock out other users
    UPDATE SubscriberIDs
        SET SubscriberID= SubscriberID+ 1
        OUTPUT  INSERTED.SubscriberID
        INTO @idtable
    --capture the next id from temp table variable
    select @y=id from @idtable
    --print @y
    --use the next id in the actual table
    insert into subscriber values (@y)

    commit
    --print @x
    waitfor delay '00:00:00.005'
end --while

---------------------------------------------------------------
EDIT here is my original attempt, which will eventually get some deadlocks when run in a loop and in multiple windows at the same time. The above method always works. I tried all combinations of transactions, with(holdlock), and set transaction isolation level serializable, etc. but could not get it to run as well as the above method.

set up:

create table subscriber
(
recid  int not null primary key
)

used to capture the id:

declare @idtable table
(
id int
)

the insert:

insert into subscriber
    OUTPUT INSERTED.recid
        recid
    INTO @idtable
    SELECT ISNULL(MAX(recid),0)+1 FROM subscriber

list the new id:

select * from @idtable

list all the ids:

select * from subscriber
于 2009-04-15T17:43:41.733 回答
0

You shouldn't get a deadlock here as the second should just wait for the first to complete. Your issue is that you are creating a transaction, then adding another lock while in that transaction.

Also, you are getting the ID then using it in two separate statements, whereas you could do it all in one solution:

set transaction isolation level serializable
begin transaction
    insert into subscriber (recid) 
       SELECT (select ISNULL(max(recid)+1,1) from subscriber)
commit transaction
select * from subscriber

This should ensure that you have only consistency on your inserts. However, as you specify that legacy applications are also using this table, can you be certain that when they insert new records it won't conflict with this?

于 2009-04-17T08:47:25.450 回答