メインコンテンツまでスキップ

分散ロック

共有リソースへのアクセスをリアルタイムで管理し、望ましい順序でデータをトランザクション処理します

分散ロックを使う理由

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 は、複数のクライアントおよびアプリケーションインスタンス間でアクセスを調整する分散ロックを提供します。クライアントがロックを取得すると、同じロックを要求する他のすべてのクライアントは、解放されるまで待機します。

仕組み

  1. クライアントが、共有リソースを識別する mutex 文字列を指定して acquireLock() を呼び出す
  2. Squid が Redis 上でアトミックにロックを取得する
  3. 別のクライアントがすでにロックを保持している場合、ロックが解放されるかタイムアウトが切れるまでリクエストは待機する
  4. ロックは保持中、バックグラウンドで自動更新される
  5. release() を呼び出して明示的に解放するか、クライアントが切断された場合に自動的に解放される

分散ロックを使うべき場面

ユースケース推奨
共有リソースへの同時変更を防ぐ分散ロック
カウンターのインクリメントや残高更新を安全に行う分散ロック
複数クライアント間で排他的アクセスを調整する分散ロック
単一ドキュメントのアトミックな更新Database transactions を使用
backend 関数にレート制限を適用するRate and quota limiting を使用

クイックスタート

前提条件

  • @squidcloud/client がインストールされた Squid frontend プロジェクト
  • ロックアクセスを許可する security rule を持つ Squid backend プロジェクト

Step 1: backend でロックアクセスを認可する

デフォルトでは、すべての分散ロックは拒否されます。アクセスを許可する security rule を追加します。

Backend code
import { secureDistributedLock, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@secureDistributedLock()
allowLocks(): boolean {
return this.isAuthenticated();
}
}

Step 2: backend をデプロイする

ローカル開発では、Squid CLI を使って backend をローカルで実行します。

squid start

クラウドへデプロイするには、deploying your backend を参照してください。

Step 3: client でロックを取得して使用する

Client code
const lock = await squid.acquireLock('my-resource');
try {
// Safely read and update a shared resource
} finally {
lock.release();
}

コアコンセプト

ロックを取得する

acquireLock() を使用して、指定した mutex に対する分散ロックを取得します。mutex は、保護したい共有リソースを一意に識別する文字列です。

Client code
const lock = await squid.acquireLock('my-resource');

別のクライアントがすでにロックを保持している場合、この promise はロックが利用可能になるか、取得タイムアウトが切れるまで待機します。

ロックオプション

acquireLock() は、ロックの挙動を設定するための任意の第2引数を受け取れます。

Client code
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
オプションデフォルト説明
acquisitionTimeoutMillisnumber2000ロックが利用可能になるのを待つ最大時間(ミリ秒)。この時間内にロックを取得できない場合、promise は reject されます。
maxHoldTimeMillisnumber制限なしロックを保持できる最大時間(ミリ秒)。この時間を超えると自動的に解放されます。ロックが無期限に保持されるのを防ぐための安全策として有用です。

DistributedLock オブジェクト

acquireLock() が解決されると、次のインターフェースを持つ DistributedLock オブジェクトが返ります。

プロパティ / メソッド説明
resourceIdstringこのロックを識別する mutex 文字列
lockIdstringこのロックインスタンスの一意な識別子
release()Promise<void>ロックを解放する
isReleased()booleanロックが解放済みなら true を返す
observeRelease()Observable<void>ロックが解放されたときに emit する

ロックを解放する

release() を呼び出してロックを解放すると、他のクライアントが取得できるようになります。

Client code
lock.release();

ロックは以下の場合にも自動的に解放されます。

  • クライアントがサーバーから切断されたとき
  • maxHoldTimeMillis の期間が満了したとき
  • Squid client インスタンスが破棄されたとき

ロックステータスを確認する

isReleased() を使って、ロックが解放済みかどうかを確認します。

Client code
console.log(lock.isReleased()); // true or false

ロック解放を監視する

observeRelease() を使うと、明示的な解放でも切断による解放でも、ロックが解放されたときに反応できます。

Client code
// observeRelease() returns an RxJS Observable that emits once when released
lock.observeRelease().subscribe(() => {
console.log('Lock released, resource is now available');
});

コールバックでロックを管理する

withLock() を使うと、ロックを取得し、コールバックを実行し、コールバック完了時に自動でロックを解放できます。これにより、手動の try/finally ブロックが不要になります。

Client code
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 されます。

Client code
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 するため、予期しない解放を検知できます。

Client code
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 connectedWebSocket 接続が確立されていないロック取得前に Squid client が初期化済みで接続されていることを確認する
Unauthorizedこの mutex のロックアクセスを許可する security rule がないbackend に @secureDistributedLock ルールを追加する

ベストプラクティス

finally ブロックで必ず解放する

エラーが起きてもロックが解放されるように、ロック使用は try/finally で包みます。あるいは、これを自動で処理する withLock() を使用してください。

Client code
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}

説明的な mutex 名を使う

保護対象リソースが明確に分かる mutex 名を選びます。これによりデバッグが容易になり、意図しない衝突も防げます。

Client code
// 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 を使用します。

Client code
const lock = await squid.acquireLock('critical-resource', {
maxHoldTimeMillis: 10000, // Force release after 10 seconds
});

ロック保持時間を短く保つ

必要最小限の時間だけロックを保持します。非クリティカルな作業(logging、UI 更新、通知)はロック区間の外で行ってください。

Client code
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);

分散ロックのセキュリティ

デフォルトでは、分散ロックは完全に保護されており、backend の security rule が許可しない限り取得できません。@secureDistributedLock デコレーターを使用して、どのクライアントがどのロックを取得できるかを制御します。

mutex ごとの認可を含むロック security rule の設定についての詳細は、Securing distributed locks を参照してください。