SQL Server 向けに、
「行ロックを活かす設計」と「再試行ロジック」の具体的な方法を紹介します。
1. 行ロックを活かす設計
(1) インデックスを正しく設定
- WHERE 句に使うカラムに適切なインデックスを付与
- テーブルスキャンが発生すると、意図せず広範囲のロック(ページロックやテーブルロック)が発生しやすくなる
-- 例: 顧客IDごとに更新する場合
CREATE NONCLUSTERED INDEX IX_Orders_CustomerId
ON Orders(CustomerId);
(2) ヒントを使ってロックの粒度を制御
SQL Server ではロックヒントを明示することで、不要なテーブルロックを防げます。
-- 行ロックを強制する例
UPDATE Orders WITH (ROWLOCK)
SET Status = 'Completed'
WHERE OrderId = 12345;
-- 行ロック+更新時の競合回避
SELECT * FROM Orders WITH (ROWLOCK, UPDLOCK)
WHERE OrderId = 12345;
-
ROWLOCK→ 行ロックを強制 -
UPDLOCK→ 更新のためのロックを確保(読んだ時点で更新予定を示す)
※ヒントはやりすぎると逆にスループット低下を招くので、競合が激しい部分だけに使うのがコツです。
(3) トランザクションを短くする
BEGIN TRAN;
UPDATE Orders WITH (ROWLOCK)
SET Status = 'Completed'
WHERE OrderId = 12345;
COMMIT;
- なるべく処理をまとめすぎない
- SELECT → UPDATE → 別テーブル UPDATE → COMMIT みたいに長いとロック競合が増える
2. 再試行ロジックの実装
デッドロックは「完全にゼロ」にできないので、アプリ側で「失敗したらリトライ」する仕組みが現実的です。
SQL Server はデッドロックが起きると 1205 (Deadlock victim) エラーを返すので、それをキャッチしてリトライします。
(1) T-SQL 内で再試行
DECLARE @retryCount INT = 0;
DECLARE @maxRetry INT = 3;
WHILE @retryCount < @maxRetry
BEGIN
BEGIN TRY
BEGIN TRAN;
UPDATE Orders WITH (ROWLOCK)
SET Status = 'Completed'
WHERE OrderId = 12345;
COMMIT;
BREAK; -- 成功したらループを抜ける
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 1205 -- Deadlock victim
BEGIN
ROLLBACK;
SET @retryCount += 1;
WAITFOR DELAY '00:00:01'; -- 少し待ってリトライ
END
ELSE
BEGIN
ROLLBACK;
THROW; -- 別のエラーなら即時スロー
END
END CATCH
END
(2) C# / Java などアプリ側でのリトライ例
C# の場合:
int retries = 3;
for (int i = 0; i < retries; i++)
{
try
{
using (var tran = connection.BeginTransaction())
{
var cmd = new SqlCommand(
"UPDATE Orders WITH (ROWLOCK) SET Status='Completed' WHERE OrderId=@OrderId",
connection, tran);
cmd.Parameters.AddWithValue("@OrderId", 12345);
cmd.ExecuteNonQuery();
tran.Commit();
}
break; // 成功
}
catch (SqlException ex) when (ex.Number == 1205) // Deadlock victim
{
if (i == retries - 1) throw; // 最後の試行でも失敗したら投げる
Thread.Sleep(1000); // 待機してリトライ
}
}
まとめ
-
行ロック活用:
ROWLOCK/UPDLOCKヒント、インデックス最適化、短いトランザクション - 再試行ロジック:1205 エラーをキャッチしてリトライ(T-SQL またはアプリ側で実装)
この2つを組み合わせると「実運用で耐えられる」仕組みになります。