Giao diện
@digiforce-nc/lock-manager
| Package | @digiforce-nc/lock-manager |
| Depends on | (standalone) |
1) Tổng quan
@digiforce-nc/lock-manager cung cấp cơ chế distributed locking cho các thao tác đồng thời trong hệ thống Digiforce. Thiết kế theo mô hình pluggable adapter - cho phép chuyển đổi giữa lock in-process (local) và lock phân tán (Redis) mà không thay đổi code ứng dụng.
Khi nào cần lock?
Lock cần thiết khi nhiều process/request cùng truy cập một tài nguyên và cần đảm bảo chỉ một thao tác được thực hiện tại một thời điểm - ví dụ: chạy migration, xử lý webhook, cập nhật counter.
2) LockManager API
acquire(key, ttl?)
Yêu cầu lock cho một key. Trả về ILock object dùng để release khi hoàn tất.
typescript
const lock = await lockManager.acquire('migration:users', 30000);
// key: identifier duy nhất cho tài nguyên cần lock
// ttl: thời gian sống tối đa (ms), tự release nếu quá hạn
try {
await runMigration();
} finally {
await lock.release();
}| Tham số | Kiểu | Mô tả |
|---|---|---|
key | string | Identifier duy nhất cho lock |
ttl | number (optional) | Time-to-live tính bằng milliseconds. Lock tự giải phóng sau thời gian này |
release(key)
Giải phóng lock theo key (alternative cho lock.release()).
registerAdapter(name, adapter)
Đăng ký adapter mới:
typescript
lockManager.registerAdapter('redis', new RedisLockAdapter(redisClient));
lockManager.setDefaultAdapter('redis');setDefaultAdapter(name)
Chọn adapter mặc định sử dụng khi gọi acquire().
3) ILockAdapter interface
Mọi adapter phải implement interface sau:
typescript
interface ILockAdapter {
acquire(key: string, ttl?: number): Promise<ILock>;
}
interface ILock {
release(): Promise<void>;
}Quy ước adapter
acquire()phải block (await) cho đến khi lấy được lock hoặc timeout- Nếu không lấy được lock trong thời gian cho phép, throw
LockAcquireError release()phải idempotent - gọi nhiều lần không gây lỗi
4) Built-in adapters
Local adapter (mặc định)
Sử dụng thư viện async-mutex để lock trong cùng một process Node.js:
Giới hạn Local adapter
Local adapter chỉ hoạt động trong cùng một process. Khi deploy nhiều instance (cluster mode, multiple containers), lock sẽ không đồng bộ giữa các instance. Dùng Redis adapter cho môi trường production multi-instance.
Redis adapter
Cung cấp qua plugin @digiforce-nc/plugin-lock-adapter-redis:
typescript
// Plugin tự đăng ký adapter khi load
// Cấu hình trong .env:
// LOCK_ADAPTER=redis
// REDIS_URL=redis://localhost:6379Redis adapter sử dụng thuật toán Redlock để đảm bảo distributed locking an toàn ngay cả khi có failover.
5) Error types
LockAcquireError
Throw khi không thể lấy lock (timeout, adapter lỗi):
typescript
import { LockAcquireError } from '@digiforce-nc/lock-manager';
try {
const lock = await lockManager.acquire('resource:key', 5000);
} catch (error) {
if (error instanceof LockAcquireError) {
console.log('Không thể lấy lock - tài nguyên đang bận');
}
}LockAbortError
Throw khi lock bị hủy trước khi hoàn tất (ví dụ: process shutdown):
typescript
import { LockAbortError } from '@digiforce-nc/lock-manager';
try {
const lock = await lockManager.acquire('long-task', 60000);
await longRunningTask();
await lock.release();
} catch (error) {
if (error instanceof LockAbortError) {
console.log('Lock bị hủy - process đang shutdown');
}
}6) Use cases
Ngăn duplicate operations
typescript
async function processWebhook(webhookId: string, payload: any) {
const lock = await app.lockManager.acquire(`webhook:${webhookId}`, 10000);
try {
const existing = await db.getRepository('webhookLogs').findOne({
filter: { webhookId },
});
if (existing) {
return; // đã xử lý
}
await handleWebhook(payload);
await db.getRepository('webhookLogs').create({
values: { webhookId, processedAt: new Date() },
});
} finally {
await lock.release();
}
}Distributed mutex cho migration
typescript
async function runMigrations() {
const lock = await app.lockManager.acquire('db:migration', 120000);
try {
await app.db.migrator.up();
} finally {
await lock.release();
}
}Sequential task processing
typescript
async function processTask(queueName: string) {
const lock = await app.lockManager.acquire(`queue:${queueName}`);
try {
const task = await getNextTask(queueName);
if (task) {
await executeTask(task);
await markComplete(task.id);
}
} finally {
await lock.release();
}
}7) Cấu hình
| Biến môi trường | Mô tả | Mặc định |
|---|---|---|
LOCK_ADAPTER | Adapter sử dụng (local hoặc redis) | local |
LOCK_DEFAULT_TTL | TTL mặc định (ms) | 30000 |
TTL quá ngắn
Nếu TTL ngắn hơn thời gian thực thi task, lock sẽ tự giải phóng trong khi task vẫn đang chạy → race condition. Luôn đặt TTL lớn hơn worst-case execution time và sử dụng try/finally để release sớm.