分散ロック
共有リソースへのアクセスをリアルタイムで管理し、希望する順序でデータを処理できるようにします
分散ロックを使用する理由
2人のユーザーが在庫最後の1個の商品に対して同時に「Buy」をクリックします。両者が在庫を 1 と読み取り、両者がそれを減算し、実際には1個しか存在しないにもかかわらず両方の注文が通ってしまいます。これはレースコンディションです。逐次的に実行されるべき2つの操作が、並行して実行されています。
// Without a lock: race condition
const stock = await getStock(itemId); // Both clients read 1
await setStock(itemId, stock - 1); // Both clients write 0 -- two orders, one item
// With a lock: sequential access
const lock = await squid.acquireLock('inventory-item-123');
try {
const stock = await getStock(itemId); // Only one client reads at a time
if (stock > 0) {
await setStock(itemId, stock - 1);
}
} finally {
lock.release();
}
分散ロックは、共有リソースへのアクセスを同時に保持できるクライアントを1つに限定し、並行操作によるデータ破損を防ぎます。
概要
Squid は、複数のクライアントおよびアプリケーションインスタンス間でアクセスを調整する分散ロックを提供します。クライアントがロックを取得すると、同じロックを要求する他のすべてのクライアントは、ロックが解放されるまで待機します。
仕組み
- クライアントが、共有リソースを識別する mutex 文字列を指定して
acquireLock()を呼び出す - Squid が Redis でロックをアトミックに取得する
- すでに別のクライアントがロックを保持している場合、ロックが解放されるかタイムアウト期限が切れるまでリクエストは待機する
- ロックは保持中、バックグラウンドで自動更新される
release()を呼び出して明示的に解放するか、クライアントが切断された場合は自動的に解放される
分散ロックを使用する場面
| ユースケース | 推奨 |
|---|---|
| 共有リソースへの同時変更を防ぐ | 分散ロック |
| カウンターのインクリメントや残高更新を安全に行う | 分散ロック |
| 複数クライアント間で排他的アクセスを調整する | 分散ロック |
| アトミックな単一ドキュメント更新 | Database transactions を使用 |
| バックエンド関数のレート制限を強制する | Rate and quota limiting を使用 |
クイックスタート
前提条件
@squidcloud/clientがインストールされた Squid frontend プロジェクト- ロックアクセスを許可する security rule を備えた Squid backend プロジェクト
手順 1: バックエンドでロックアクセスを承認する
デフォルトでは、すべての分散ロックは拒否されます。アクセスを許可するための security rule を追加します。
import { secureDistributedLock, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureDistributedLock()
allowLocks(): boolean {
return this.isAuthenticated();
}
}
手順 2: バックエンドをデプロイする
ローカル開発では、Squid CLI を使ってバックエンドをローカルで実行します。
squid start
クラウドへデプロイする場合は、deploying your backend を参照してください。
手順 3: クライアントでロックを取得して使用する
const lock = await squid.acquireLock('my-resource');
try {
// Safely read and update a shared resource
} finally {
lock.release();
}
コアコンセプト
ロックの取得
acquireLock() を使用して、指定した mutex に対する分散ロックを取得します。mutex は、保護したい共有リソースを一意に識別する文字列です。
const lock = await squid.acquireLock('my-resource');
すでに別のクライアントがロックを保持している場合、promise はロックが利用可能になるまで、または取得タイムアウトが切れるまで待機します。
ロックオプション
acquireLock() は、ロックの挙動を設定するためのオプションの第2引数を受け取ります。
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
acquisitionTimeoutMillis | number | 2000 | ロックが利用可能になるのを待つ最大時間(ミリ秒)。この時間内に取得できない場合、promise は reject されます。 |
maxHoldTimeMillis | number | 制限なし | ロックを保持できる最大時間(ミリ秒)。この時間を超えると自動的に解放されます。ロックが無期限に保持されるのを防ぐ安全策として有用です。 |
DistributedLock オブジェクト
acquireLock() が resolve されると、次のインターフェースを持つ DistributedLock オブジェクトが返されます。
| プロパティ / メソッド | 型 | 説明 |
|---|---|---|
resourceId | string | このロックを識別する mutex 文字列 |
lockId | string | このロックインスタンスの一意な識別子 |
release() | Promise<void> | ロックを解放します |
isReleased() | boolean | ロックが解放済みの場合に true を返します |
observeRelease() | Observable<void> | ロックが解放されたときに emit します |
ロックの解放
release() を呼び出してロックを解放し、他のクライアントが取得できるようにします。
lock.release();
ロックは次の場合にも自動的に解放されます。
- クライアントがサーバーから切断された場合
maxHoldTimeMillisの期間が満了した場合- Squid client インスタンスが破棄された場合
ロック状態の確認
isReleased() を使用して、ロックが解放されたかどうかを確認します。
console.log(lock.isReleased()); // true or false
ロック解放の監視
observeRelease() を使用して、明示的な解放または切断による解放を含め、ロックが解放されたときに反応できます。
// observeRelease() returns an RxJS Observable that emits once when released
lock.observeRelease().subscribe(() => {
console.log('Lock released, resource is now available');
});
コールバックでロックを管理する
withLock() を使うと、ロックを取得してコールバックを実行し、コールバック完了時にロックを自動的に解放できます。これにより、手動の try/finally ブロックが不要になります。
const result = await squid.withLock('my-resource', async (lock) => {
// The lock is held for the duration of this callback
const data = await readSharedData();
await updateSharedData(data + 1);
return data + 1;
});
// Lock is automatically released here, even if the callback throws
コールバックは DistributedLock オブジェクトを引数として受け取るため、コールバック内で状態確認や解放イベントの監視ができます。
エラーハンドリング
取得タイムアウト
acquisitionTimeoutMillis(デフォルト: 2秒)以内にロックを取得できない場合、promise は LOCK_TIMEOUT エラーで reject されます。
try {
const lock = await squid.acquireLock('busy-resource', {
acquisitionTimeoutMillis: 3000,
});
// Use the lock...
lock.release();
} catch (error) {
console.error('Could not acquire lock:', error.message);
// Handle timeout: retry later, notify the user, etc.
}
接続喪失
クライアントがサーバーとの接続を失うと、保持しているすべてのロックは自動的に解放されます。この場合 observeRelease() の observable が emit するため、想定外の解放を検知できます。
const lock = await squid.acquireLock('my-resource');
let releasedByUs = false;
lock.observeRelease().subscribe(() => {
if (!releasedByUs) {
console.warn('Lock was released unexpectedly (possible disconnection)');
}
});
// Later, when releasing intentionally:
releasedByUs = true;
lock.release();
よくあるエラー
| エラー | 原因 | 解決策 |
|---|---|---|
LOCK_TIMEOUT | 別のクライアントが acquisitionTimeoutMillis より長くロックを保持している | タイムアウトを増やす、またはリトライロジックを実装する |
| Client not connected | WebSocket 接続が確立されていない | ロック取得前に Squid client が初期化され接続されていることを確認する |
| Unauthorized | この mutex のロックアクセスを許可する security rule がない | バックエンドに @secureDistributedLock ルールを追加する |
ベストプラクティス
必ず finally ブロックで解放する
try/finally でロック利用を囲み、エラーが発生しても必ず解放されるようにします。あるいは、これを自動的に処理する withLock() を使用してください。
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}
分かりやすい mutex 名を使う
保護するリソースを明確に識別できる mutex 名を選びます。これによりデバッグが容易になり、意図しない衝突を防げます。
// Good: specific and descriptive
await squid.acquireLock('inventory-update-sku-12345');
await squid.acquireLock('user-balance-user-abc');
// Avoid: generic names that may collide
await squid.acquireLock('lock1');
await squid.acquireLock('update');
安全策として maxHoldTimeMillis を設定する
バグや未処理のエラーによりロックが無期限に保持されるのを防ぐために、maxHoldTimeMillis を使用します。
const lock = await squid.acquireLock('critical-resource', {
maxHoldTimeMillis: 10000, // Force release after 10 seconds
});
ロック保持時間を短く保つ
必要最小限の時間だけロックを保持します。ロックが不要な作業(ログ、UI 更新、通知など)はロック区間の外で行ってください。
const lock = await squid.acquireLock('shared-counter');
let newValue: number;
try {
const current = await readCounter();
newValue = current + 1;
await writeCounter(newValue);
} finally {
lock.release();
}
// Do non-critical work after releasing
console.log('Counter updated to', newValue);
分散ロックのセキュリティ
デフォルトでは、分散ロックは完全に保護されており、バックエンドの security rule が許可した場合にのみ取得できます。@secureDistributedLock デコレーターを使用して、どのクライアントがどのロックを取得できるかを制御します。
mutex ごとの認可を含むロックの security rule 設定の詳細は、Securing distributed locks を参照してください。