Suppose I have these two tables:
Invoice
-------
iInvoiceID int PK not null
dtCompleted datetime null
InvoiceItem
-----------
iInvoiceItemID int PK not null
iInvoiceID int FK (Invoice.iInvoiceID) not null
dtCompleted datetime null
Each InvoiceItem might be fulfilled by a different process (executable) that runs on a different machine. When the process is complete, I want it to call a stored procedure to stamp the InvoiceItem.dtCompleted field, and I want this stored procedure to return back a flag indicating whether the entire invoice has been completed. Whichever process happens to be the one that finished the invoice is going to kick off another process to do some final business logic on the invoice, e.g. stamp the dtCompleted and send a receipt email. Obviously I want this other process to fire only once for a given Invoice.
Here is my naive implementation:
CREATE PROCEDURE dbo.spuCompleteInvoiceItem
@iInvoiceItemID INT
AS
BEGIN
BEGIN TRAN
UPDATE InvoiceItem
SET dtCompleted = GETDATE()
WHERE iInvoiceItemID = @iInvoiceItemID
IF EXISTS(SELECT * FROM InvoiceItem WHERE dtCompleted IS NULL
AND iInvoiceID = (SELECT iInvoiceID FROM InvoiceItem
WHERE iInvoiceItemID=@iInvoiceItemID))
SELECT 'NotComplete' AS OverallInvoice
ELSE
SELECT 'Complete' AS OverallInvoice
COMMIT
END
Is this sufficient? Or do I need to increase the transaction serialization level and if so, what level would provide the best balance of performance and safety?
Pre-emptive comments:
- I know I could achieve the same business goal by implementing a central concurrency service at the process/executable level, but I think that's overkill. My instinct is that if I craft my stored procedure and transaction well, I can use SQL Server as my inter-process concurrency service for this simple operation without heavily impacting performance or increasing deadlock frequency (have my cake and eat it too.)
- I'm not worrying about error handling in this example. I'll add the proper TRY/CATCH/ROLLBACK/RAISERROR stuff after.
Update 1:
According to the experts, not only do I need the most restrictive transaction isolation level -- serializable -- but I also need to lock all the InvoiceItems of a particular invoice before I do anything else, to ensure that other concurrent calls to the stored procedure will block until the current one completes. Otherwise I might get deadlocks. Here's my latest version of the implementation:
CREATE PROCEDURE dbo.spuCompleteInvoiceItem
@iInvoiceItemID INT
AS
BEGIN
IF @iInvoiceItemID IS NULL RAISERROR('@iInvoiceItemID cannot be null.', 16, 1)
BEGIN TRAN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
DECLARE @iInvoiceID INT
SELECT @iInvoiceID = iInvoiceID
FROM InvoiceItem
WHERE dtCompleted IS NULL
AND iInvoiceID = (SELECT iInvoiceID FROM InvoiceItem WHERE iInvoiceItemID=@iInvoiceItemID)
IF @iInvoiceID IS NULL
BEGIN
-- Should never happen
SELECT 'AlreadyComplete' AS Result
END
ELSE
BEGIN
UPDATE InvoiceItem SET dtCompleted = GETDATE() WHERE iInvoiceItemID = @iInvoiceItemID
IF EXISTS(SELECT * FROM InvoiceItem WHERE iInvoiceID=@iInvoiceID AND dtCompleted IS NULL)
SELECT 'NotComplete' AS Result
ELSE
SELECT 'Complete' AS Result
END
COMMIT
Thanks,
Jordan Rieger