分散ロック
共有リソースへのアクセスをリアルタイムで管理し、望ましい順序でデータを処理します
Distributed Locks を使用する理由
在庫最後の 1 点に対して、2 人のユーザーが同時に「Buy」をクリックしたとします。両方が在庫を 1 として読み取り、両方がそれをデクリメントし、商品は 1 つしか存在しないにもかかわらず両方の注文が通ってしまいます。これは race condition です。順番に実行されるべき 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();
}
Distributed locks は、共有リソースへのアクセスを一度に 1 つの client だけが保持できるようにし、同時操作によるデータ破損を防ぎます。
概要
Squid は、複数の client や application instance 間でアクセスを調整する distributed locks を提供します。 client が lock を取得すると、同じ lock を要求する他のすべての client は、その lock が解放されるまで待機します。
仕組み
- client が、共有リソースを識別する mutex 文字列を指定して
acquireLock()を呼び出します - Squid が Redis で lock を atomically に取得します
- 別の client がすでに lock を保持している場合、その request は lock が解放されるか timeout が期限切れになるまで待機します
- lock は保持されている間、background で自動的に更新されます
- lock は
release()を呼び出すことで明示的に解放されます。または、client が切断された場合に自動的に解放されます
Distributed locks を使用するタイミング
| ユースケース | 推奨事項 |
|---|---|
| 共有リソースへの同時変更を防ぐ | Distributed lock |
| counter の increment や balance の更新を安全に行う | Distributed lock |
| 複数の client 間で排他的アクセスを調整する | Distributed lock |
| 単一 document の atomic update | Database transactions を使用 |
| backend functions に rate limit を適用する | Rate and quota limiting を使用 |
クイックスタート
前提条件
@squidcloud/clientがインストールされた Squid frontend project- lock access を許可する security rule を持つ Squid backend project
Step 1: backend で lock access を許可する
デフォルトでは、すべての distributed locks は拒否されます。アクセスを許可する security rule を追加します。
import { secureDistributedLock, SquidService } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@secureDistributedLock()
allowLocks(): boolean {
return this.isAuthenticated();
}
}
Step 2: backend を deploy する
local development では、Squid CLI を使用して backend をローカルで実行します。
squid start
cloud に deploy するには、backend の deploy を参照してください。
Step 3: client で lock を取得して使用する
const lock = await squid.acquireLock('my-resource');
try {
// Safely read and update a shared resource
} finally {
lock.release();
}
主要な概念
lock を取得する
指定した mutex の distributed lock を取得するには、acquireLock() を使用します。mutex は、保護したい共有リソースを一意に識別する文字列です。
const lock = await squid.acquireLock('my-resource');
別の client がすでに lock を保持している場合、promise は lock が利用可能になるか acquisition timeout が期限切れになるまで待機します。
Lock options
acquireLock() は、lock の動作を設定するための任意の第 2 parameter を受け取ります。
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
| Option | Type | Default | Description |
|---|---|---|---|
acquisitionTimeoutMillis | number | 2000 | lock が利用可能になるまで待機する最大時間(milliseconds)。この時間内に lock を取得できない場合、promise は reject されます。 |
maxHoldTimeMillis | number | No limit | lock が自動的に解放されるまで保持できる最大時間(milliseconds)。lock が無期限に保持されるのを防ぐ safety net として役立ちます。 |
DistributedLock object
acquireLock() が resolve されると、次の interface を持つ DistributedLock object が返されます。
| Property / Method | Type | Description |
|---|---|---|
resourceId | string | この lock を識別する mutex 文字列 |
lockId | string | この lock instance の一意の identifier |
release() | Promise<void> | lock を解放します |
isReleased() | boolean | lock が解放されている場合に true を返します |
observeRelease() | Observable<void> | lock が解放されたときに emit します |
lock を解放する
他の client が取得できるように lock を解放するには、release() を呼び出します。
lock.release();
lock は次の場合にも自動的に解放されます。
- client が server から切断された場合
maxHoldTimeMillisの期間が期限切れになった場合- Squid client instance が破棄された場合
lock status を確認する
lock が解放されているかどうかを確認するには、isReleased() を使用します。
console.log(lock.isReleased()); // true or false
lock release を監視する
lock が明示的に解放された場合でも、切断によって解放された場合でも、lock release に反応するには observeRelease() を使用します。
// observeRelease() returns an RxJS Observable that emits once when released
lock.observeRelease().subscribe(() => {
console.log('Lock released, resource is now available');
});
callback で lock を管理する
lock を取得し、callback を実行し、callback が完了したら自動的に lock を解放するには、withLock() を使用します。これにより、手動の try/finally block が不要になります。
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
callback は parameter として DistributedLock object を受け取るため、callback 内でその status を確認したり、release event を監視したりできます。
withLock() は、任意の第 3 argument として acquireLock() と同じ lock options も受け取ります。これにより、callback の lock に対して acquisition timeout と maximum hold time を調整できます。
const result = await squid.withLock(
'my-resource',
async (lock) => {
return await updateSharedResource();
},
{
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
},
);
Error Handling
Acquisition timeout
acquisitionTimeoutMillis(デフォルト: 2 秒)以内に lock を取得できない場合、promise は LOCK_TIMEOUT error で 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.
}
Connection loss
client が server への接続を失うと、保持されているすべての locks は自動的に解放されます。この場合、observeRelease() observable が emit されるため、予期しない release を検出できます。
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();
よくある error
| Error | Cause | Solution |
|---|---|---|
LOCK_TIMEOUT | 別の client が acquisitionTimeoutMillis より長く lock を保持している | timeout を増やすか、retry logic を実装します |
| Client not connected | WebSocket connection が確立されていない | lock を取得する前に Squid client が初期化され、接続されていることを確認します |
| Unauthorized | この mutex への lock access を許可する security rule がない | backend に @secureDistributedLock rule を追加します |
Best Practices
常に finally block で解放する
error が発生した場合でも lock が確実に解放されるように、lock の使用を try/finally で wrap します。または、これを自動的に処理する withLock() を使用します。
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}
説明的な mutex 名を使用する
保護対象のリソースを明確に識別できる mutex 名を選択します。これにより debugging が容易になり、偶発的な collision を防げます。
// 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');
safety net として maxHoldTimeMillis を設定する
bug や未処理の error が原因で lock が無期限に保持されるのを防ぐために、maxHoldTimeMillis を使用します。
const lock = await squid.acquireLock('critical-resource', {
maxHoldTimeMillis: 10000, // Force release after 10 seconds
});
lock duration を短く保つ
lock は必要最小限の時間だけ保持します。critical ではない作業(logging、UI updates、notifications)は、locked section の外で実行します。
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);
Distributed Locks の保護
デフォルトでは、distributed locks は完全に保護されており、backend security rule が許可した場合にのみ取得できます。どの client がどの locks を取得できるかを制御するには、@secureDistributedLock decorator を使用します。
mutex ごとの authorization を含む lock security rules の設定に関する詳細は、distributed locks の保護 を参照してください。