문제

I am looking into SQL Server Snapshot isolation level. For simple updates, things seem straightforward and you can find lots of examples of how to handle. However, for logic which relies on data validation checks at the end of the transaction, I'm having trouble coming with up with a pattern that still allows us to ROLLBACK changes if a validation fails.

Take an example of a basic inventory table, where we want to ensure that inventory balances are not allowed to go negative. We would have a check at the end of the transaction to make sure the balance is not negative, and if it is then ROLLBACK and throw an error.

Below is a SQL script example. NOTE: your database must have snapshot isolation enabled for this example to work. It can be enabled by the following command:

ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;

WINDOW 1: Create the inventory table with one item in inventory:

IF object_id('dbo.TEST_InventoryActivity') IS NOT NULL
    DROP TABLE dbo.TEST_InventoryActivity
GO

CREATE TABLE dbo.TEST_InventoryActivity
(   activityID int not null primary key identity
    , itemID int not null
    , inOrOut char(1) not null  
    , quantity int not null
    , modBy varchar(128) not null
    , modDate datetime not null default getdate()
)
go 

INSERT INTO TEST_InventoryActivity(itemID, inOrOut, quantity, modBy)
VALUES (1,'I',100, 'setups')
;
--show all records
SELECT i.*
FROM TEST_InventoryActivity i 
;
--show inventory balances
SELECT i.itemID, inventoryBalance = sum(i.quantity)
FROM TEST_InventoryActivity i 
GROUP BY i.itemID
;

WINDOW 2: Now open a NEW query windows and execute the following, which does not commit (this will be used to simulate 2 transactions occurring at the same time).

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRAN;

DECLARE @itemID int, @quantity int;
set @itemID = 1;
set @quantity = -75;

insert into TEST_InventoryActivity(itemID, inOrOut, quantity, modBy)
values(@itemID,'O', @quantity, 'test 1') --use Item 1
;

IF EXISTS(
    SELECT i.itemID, sum(i.quantity)
    FROM TEST_InventoryActivity i 
    WHERE i.itemID = @itemID
    GROUP BY i.itemID
    HAVING sum(i.quantity) < 0
)
BEGIN
    ROLLBACK;
    RAISERROR(N'Not enough remaining inventory', 16, 1);
    RETURN;
END
;

WINDOW 3: now open another NEW window and execute the following, which will drive inventory negative.

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

BEGIN TRAN;

DECLARE @itemID int, @quantity int;
set @itemID = 1;
set @quantity = -50;

insert into TEST_InventoryActivity(itemID, inOrOut, quantity, modBy)
values(@itemID,'O', @quantity, 'test 2') --use Item 1
;

IF EXISTS(
    SELECT i.itemID, sum(i.quantity)
    FROM TEST_InventoryActivity i 
    WHERE i.itemID = @itemID
    GROUP BY i.itemID
    HAVING sum(i.quantity) < 0
)
BEGIN
    ROLLBACK;
    RAISERROR(N'Not enough remaining inventory', 16, 1);
    RETURN;
END
;

COMMIT;

WINDOW 2: Now go back to Window 2 and commit the transaction.

COMMIT;

When you check the inventory balance for Item 1, it is now -25, so our validations did not work because Window 3 had no reference to anything going on in Window 2.

--show all records
SELECT i.*
FROM TEST_InventoryActivity i 
;
--show inventory balances
SELECT i.itemID, inventoryBalance = sum(i.quantity)
FROM TEST_InventoryActivity i 
GROUP BY i.itemID

Output:

enter image description here

The inventory balance for item 1 is now negative. I understand WHY the inventory is showing as negative, because the transaction in Window 2 does not block the transaction in Window 3, but I cannot find any references to how to use this kind of validation logic within a transaction and still be able to rollback the entire transaction while using snapshot isolation. From what I understand, Oracle using optimistic concurrency out of the box, so I would imagine there would be ways around this. Are there any patterns that can be used in place of this that will work with snapshot isolation level in SQL Server?

I understand that if I simply change the isolation level to READ COMMITTED, then everything works (unless SET READ_COMMITTED_SNAPSHOT ON is enabled on the database), but am looking for optimistic concurrency patterns that would give the same results.

도움이 되었습니까?

해결책

Try using the READCOMMITTED locking hint on the EXISTS query...

IF      EXISTS(SELECT   i.itemID, 
                        sum(i.quantity)
                FROM    TEST_InventoryActivity i With (READCOMMITTED)
                WHERE   i.itemID = @itemID
                GROUP   BY i.itemID
                HAVING  sum(i.quantity) < 0)
BEGIN
        ROLLBACK;
        RAISERROR(N'Not enough remaining inventory', 16, 1);
        RETURN;
END
ELSE
BEGIN
        COMMIT;
END

I used this query at a bunch of different places throughout the execution to confirm that the READCOMMITTED table hint (compliments this post) does not change the isolation level of the transaction...

SELECT  CASE transaction_isolation_level 
        WHEN 0 THEN 'Unspecified' 
        WHEN 1 THEN 'ReadUncommitted' 
        WHEN 2 THEN 'ReadCommitted' 
        WHEN 3 THEN 'Repeatable' 
        WHEN 4 THEN 'Serializable' 
        WHEN 5 THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL 
FROM    sys.dm_exec_sessions 
WHERE   session_id = @@SPID
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top