はじめに
たまによく使うテーブル設計の手法で、ロックテーブル?セマフォテーブル?というものがあります(正式名称が分かりません...)。今回はそれについての備忘録です。
改訂履歴
- 2026/05/18 : 初版公開。
本文
1. 設計
複数のプロセス(プログラムやスレッド、サーバーなど)で同じリソースを同時に使用すると、データの破損や二重実行などの問題が発生するため、排他制御が必要になります。本設計では行ロックを利用することで、排他制御を行います。
-- ロックテーブルの作成。
CREATE TABLE m_lock (
lock_id VARCHAR(50) NOT NULL, -- ロック ID
lock_name NVARCHAR(100) NOT NULL, -- ロック名称
-- lock_id を主キーに設定する。
CONSTRAINT PK_m_lock PRIMARY KEY CLUSTERED (lock_id)
);
-- レコードの登録。
INSERT INTO m_lock (lock_id, lock_name)
VALUES ('0', 'XXXX 処理ロック');
例えば lock_id = '0' のレコードを「XXXX 処理」のロックとして使用します。複数のプロセスがこのレコードに対してロックを取得しようとすると DB のロック機構によって同時実行を制御できます。
2. 実装
C# + SQL Server での実装例になります。トランザクション内で WITH (UPDLOCK, ROWLOCK, NOWAIT) を使用することで、トランザクションが終わるまで、同じロック対象を取得しようとする別のトランザクションとの競合を発生させることができます。
またロックはトランザクションに紐付いているため、接続が切断されてトランザクションが終了すると、SQL Server 側でロックも解放されます。そのため、アプリケーション側でロック情報を削除するなどの後始末は基本的に必要なく、楽です。
private void Example()
{
try
{
// 1. トランザクションを開始する。
DB.BeginTransaction();
// 2. クエリを実行して、行ロックを試みる。
// ※ 先客がいる場合、待機せずに即座に例外が発生する。
DB.Execute("SELECT * FROM m_lock WITH (UPDLOCK, ROWLOCK, NOWAIT) WHERE lock_id = '0'");
// 3. ロックが取得できた場合、処理を行う。
ExecuteProcess();
// 4. 正常に処理が完了したら、トランザクションをコミットする。
DB.CommitTransaction();
}
catch (SqlException ex)
{
if (ex.Number == 1222)
{
// 1222 : ロック要求タイムアウトの場合、実行をスキップする。
Logger.Info("処理は既に別のプロセスで実行中のため、実行をスキップします。");
DB.RollbackTransaction();
}
else
{
Logger.Error(ex, "例外が発生しました。");
DB.RollbackTransaction();
}
}
catch (Exception ex)
{
Logger.Error(ex, "例外が発生しました。");
DB.RollbackTransaction();
}
}
3. 参考
言及している記事が思ったよりもありませんでした。Web 屋さんのマイクロサービスなどで利用される分散ロックと、考え方の方向性は同じかと思います。
4. 余談
SQL Server の場合 sp_getapplock という組込のストアドプロシージャを使用すると、テーブルを用意しなくても実現できるようです。
おわりに
逆に Web 業界でも似たような考え方をするんだって気付きになりました。