[SQL Server] 銀行口座処理のようにデータを1件ずつロックしながら処理する方法(sp_getapplock)

inno
2015-08-05 21:09 1854 0
銀行口座の処理を例として説明します。

ユーザー「A」の口座には1000円があるとします。
ユーザー「A」とユーザー「B(ハッカー(?))」が 同時にユーザー「A」の口座から100円引き出し処理を行ったとします。

理想的には下記のように1個が処理された後にその残高で次の処理を行うのが一番いいのです。
1.ユーザー「A」 : 1000 – 100 = 900
2.ユーザー「B」 : 900 – 100 = 800

ですが、同時に行った場合、下記のように処理される可能性があります。

1.ユーザー「A」 : 1000 – 100 = 900
2.ユーザー「B」 : 1000 – 100 = 900

1000円からユーザー「A」、「B」より100円ずつ引き出されたのに残高は900円が残っています。
これはマズイですよね~

もしくはこういう例もあります。
あるサイトから「クーポン発行」ボタンをクリックするとクーポン番号を発行するキャンベーンがあります。
また、ユーザー「A」と「B」が同時に「クーポン発行」ボタンをクリックした場合下記のような処理が行われ問題が発生する可能性があります。
ユーザー「A」にクーポン番号「A0001」を発行して発行済みフラグを更新する前にユーザー「B」にも同じ番号「A0001」を発行後発行済みに処理する可能性があります。
これは実際に発生してた問題です。世の中には悪い人が多いですからね。
こういう場合は、ユーザー「A」にクーポン番号を発行して発行済みに更新するまではユーザー「B」にクーポン発行する作業を待機させる必要があります。
ユーザー「A」のすべての作業が終わったらユーザー「B」の処理をすることで問題を解決できます。
では、解決方法を説明したいと思います。
また、銀行口座からお金を引き出したときの話に戻して説明します。
下記のSQLを実行してテーブルとデータを登録しましょう。

CREATE TABLE dbo.Bank
(
UserID varchar(10)
, Amount money
)
GO

INSERT INTO dbo.Bank (UserID, Amount) VALUES ('A', 1000)
INSERT INTO dbo.Bank (UserID, Amount) VALUES ('B', 1000)
INSERT INTO dbo.Bank (UserID, Amount) VALUES ('C', 1000)
GO

BankテーブルにユーザーA,B,Cに残高があそれぞれ1000円があるとします。
では、下記のSQLを実行して引き出し用のSPを作成してみましょう。


CREATE PROCEDURE [dbo].[pBank_Update]
@UserID varchar(10)
, @Amount money
AS
BEGIN TRAN 

DECLARE @RtnCode INT; 
--ロック開始
EXEC @RtnCode = sp_getapplock @Resource = @UserID, @LockMode = 'Exclusive'

--処理確認の為にWaitFor Delayを追加しました。
WAITFOR DELAY '00:00:03'

IF (@RtnCode >= 0)
BEGIN
UPDATE dbo.Bank SET Amount = Amount - @Amount WHERE UserID = @UserID 
END 

--ロック解除
EXEC sp_releaseapplock @Resource = @UserID 
COMMIT TRAN
GO

上記のSPの中にはテストの為にわざと「WAITFOR DELAY '00:00:03'」を追記しました。

それでは、クエリウィンドウを2~3個開いて下記のSQLを実行してみてください。

EXEC dbo.pBank_Update 'A', 10

ユーザー「A」の口座から10円を引き出す処理をしていますが、最初に実行されたSPが終わったら次のSPが実行されることが確認できると思います。
SPの中で下記の部分がロック開始する部分です。

EXEC @RtnCode = sp_getapplock @Resource = @UserID, @LockMode = 'Exclusive'

「@Resource」部分に「@UserID」を指定していますが、個別口座をロックさせるためです。
つまりユーザー「A」口座の処理中にはユーザー「A」の口座に対してロックされますが、他の口座には影響が発生しません。ユーザー「A」口座の処理中でもユーザー「B」口座の処理はできるということです。
ですが、下記のように「@Resource」を指定して設定しますと、ユーザー「A」口座の処理中でもユーザー「B」口座の処理はロックされます。


EXEC @RtnCode = sp_getapplock @Resource = 'innobank', @LockMode = 'Exclusive'

こういう作業はクーポン発行に適した方法となります。
クーポン発行の場合は個別処理ではなくキャンペーンに対してロックをかける必要がありますからね。

そしてすべての処理が終わったらロック解除する作業が必要になります。

EXEC sp_releaseapplock @Resource = @UserID

説明が長くなりましたが、実際にんテストしてみた方が分かりやすいと思いますので、上記のSQLを利用してテストをしてみてください。

コメント