NestJS integration for redlock-universal - Distributed Redis locks with decorators and dependency injection
NestJS wrapper for redlock-universal, providing distributed Redis locks through NestJS decorators, modules, and dependency injection.
- NestJS Native: First-class integration with dependency injection and lifecycle hooks
- Distributed Locks: Redlock algorithm for multi-instance Redis setups
- Simple API: Method decorator for zero-boilerplate distributed locking
- High Performance: <1ms lock acquisition with automatic extension
- Type-Safe: Full TypeScript support with strict type checking
- Universal: Works with both
node-redisv4+ andioredisv5+ clients
This package wraps redlock-universal, so you need to install both packages:
npm install nestjs-redlock-universal redlock-universalYou'll also need a Redis client:
# For node-redis
npm install redis
# OR for ioredis
npm install ioredisimport { Module } from '@nestjs/common';
import { RedlockModule } from 'nestjs-redlock-universal';
import { NodeRedisAdapter } from 'redlock-universal';
import { createClient } from 'redis';
// Create and connect Redis clients
const redis1 = createClient({ url: 'redis://localhost:6379' });
const redis2 = createClient({ url: 'redis://localhost:6380' });
const redis3 = createClient({ url: 'redis://localhost:6381' });
await Promise.all([redis1.connect(), redis2.connect(), redis3.connect()]);
@Module({
imports: [
RedlockModule.forRoot({
nodes: [
new NodeRedisAdapter(redis1),
new NodeRedisAdapter(redis2),
new NodeRedisAdapter(redis3),
],
defaultTtl: 30000, // 30 seconds
}),
],
})
export class AppModule {}import { Injectable } from '@nestjs/common';
import { Redlock } from 'nestjs-redlock-universal';
@Injectable()
export class PaymentService {
@Redlock({ key: 'payment:processing' })
async processPayment(orderId: string): Promise<void> {
// This method is automatically protected by a distributed lock
// Only one instance can execute at a time across all servers
}
@Redlock({ key: (userId: string) => `user:${userId}:update` })
async updateUser(userId: string, data: unknown): Promise<void> {
// Lock key is dynamically generated from method arguments
// Each user gets their own lock
}
}import { Injectable } from '@nestjs/common';
import { RedlockService } from 'nestjs-redlock-universal';
@Injectable()
export class OrderService {
constructor(private readonly redlock: RedlockService) {}
async processOrder(orderId: string): Promise<void> {
// Recommended: Automatic lock management with using()
await this.redlock.using(`order:${orderId}`, async () => {
// Lock is automatically extended if operation takes longer than TTL
// Lock is automatically released when done or on error
});
}
async manualLocking(resourceId: string): Promise<void> {
// Advanced: Manual acquire/release for fine-grained control
const handle = await this.redlock.acquire(`resource:${resourceId}`);
try {
// Critical section
} finally {
await this.redlock.release(`resource:${resourceId}`, handle);
}
}
}import { RedlockModule } from 'nestjs-redlock-universal';
import { NodeRedisAdapter } from 'redlock-universal';
RedlockModule.forRoot({
nodes: [
new NodeRedisAdapter(redis1),
new NodeRedisAdapter(redis2),
new NodeRedisAdapter(redis3),
],
defaultTtl: 30000, // Default lock TTL in milliseconds
retryAttempts: 3, // Number of retry attempts
retryDelay: 200, // Delay between retries in milliseconds
quorum: 2, // Minimum nodes for quorum (default: majority)
})import { ConfigService } from '@nestjs/config';
RedlockModule.forRootAsync({
useFactory: async (configService: ConfigService) => {
const redisUrls = configService.get<string[]>('redis.urls');
const clients = await Promise.all(
redisUrls.map(url => createClient({ url }).connect())
);
return {
nodes: clients.map(client => new NodeRedisAdapter(client)),
defaultTtl: configService.get('redis.lockTtl', 30000),
};
},
inject: [ConfigService],
})import { IoredisAdapter } from 'redlock-universal';
import Redis from 'ioredis';
const redis1 = new Redis({ host: 'localhost', port: 6379 });
const redis2 = new Redis({ host: 'localhost', port: 6380 });
const redis3 = new Redis({ host: 'localhost', port: 6381 });
RedlockModule.forRoot({
nodes: [
new IoredisAdapter(redis1),
new IoredisAdapter(redis2),
new IoredisAdapter(redis3),
],
})Automatically wraps a method with lock acquisition and release.
@Redlock(options: RedlockDecoratorOptions)
interface RedlockDecoratorOptions {
// Static key or function that generates key from arguments
key: string | ((...args: unknown[]) => string);
// Lock time-to-live in milliseconds (default: module defaultTtl)
ttl?: number;
// Number of retry attempts (default: module retryAttempts)
retryAttempts?: number;
// Delay between retries in milliseconds (default: module retryDelay)
retryDelay?: number;
}Examples:
// Static key
@Redlock({ key: 'global:config:update' })
async updateConfig() { }
// Dynamic key from arguments
@Redlock({ key: (id: string) => `resource:${id}:lock` })
async updateResource(id: string) { }
// Custom TTL
@Redlock({ key: 'long:operation', ttl: 300000 }) // 5 minutes
async longRunningTask() { }
// Multiple arguments
@Redlock({ key: (type: string, id: string) => `${type}:${id}:lock` })
async process(type: string, id: string) { }Injectable service for programmatic lock management.
Acquire a lock manually. Returns a handle that must be passed to release().
const handle = await redlockService.acquire('resource:123');
try {
// Critical section
} finally {
await redlockService.release('resource:123', handle);
}Release a previously acquired lock using its handle.
Execute a function with automatic lock management. Recommended for most use cases.
const result = await redlockService.using('resource:123', async (signal) => {
// Lock is automatically acquired, extended, and released
// Optional: Check if lock extension failed
if (signal?.aborted) {
throw new Error('Lock lost during operation');
}
return processResource();
});For advanced features like batch operations, health checks, and metrics, import directly from redlock-universal:
import { RedlockService } from 'nestjs-redlock-universal';
import { LockManager, HealthChecker, MetricsCollector } from 'redlock-universal';
@Injectable()
export class AdvancedService {
constructor(private readonly redlock: RedlockService) {}
async batchOperations() {
// Use redlock-universal directly for batch locks
const manager = new LockManager({ nodes: [...] });
const handles = await manager.acquireBatch(['key1', 'key2', 'key3']);
// ... process
await manager.releaseBatch(handles);
}
}For development or single-instance deployments:
const redis = createClient({ url: 'redis://localhost:6379' });
await redis.connect();
RedlockModule.forRoot({
nodes: [new NodeRedisAdapter(redis)],
// Automatically uses SimpleLock instead of RedLock for single node
})// ✅ Good: Specific, hierarchical keys
@Redlock({ key: (userId) => `user:${userId}:profile:update` })
// ✅ Good: Include resource type
@Redlock({ key: (orderId) => `order:${orderId}:payment` })
// ❌ Bad: Too generic
@Redlock({ key: 'update' })
// ❌ Bad: No namespace
@Redlock({ key: (id) => id })The module automatically selects the optimal lock strategy:
- 1-2 nodes: Uses
SimpleLock(single-instance locking) - 3+ nodes: Uses
RedLock(distributed Redlock algorithm)
For production deployments, always use 3 or more Redis instances for proper fault tolerance.
import { Test } from '@nestjs/testing';
import { RedlockService } from 'nestjs-redlock-universal';
const module = await Test.createTestingModule({
providers: [
YourService,
{
provide: RedlockService,
useValue: {
using: vi.fn((key, fn) => fn()),
acquire: vi.fn(),
release: vi.fn(),
},
},
],
}).compile();See TESTING.md for complete integration testing guide with Docker.
Based on redlock-universal benchmarks:
- Acquisition latency: <1ms mean (P95: <2ms)
- Throughput: 3,300+ ops/sec (single node)
- Batch operations: 500+ ops/sec (10-lock batches)
- Memory: <2KB per lock operation
@Redlock({ key: (jobId) => `job:${jobId}:process` })
async processJob(jobId: string) {
// Ensures only one worker processes this job
}@Redlock({ key: (userId) => `user:${userId}:wallet` })
async updateWallet(userId: string, amount: number) {
// Prevents race conditions in balance updates
}@Redlock({ key: 'api:external:call', ttl: 1000 })
async callRateLimitedAPI() {
// Ensures max 1 call per second across all instances
}@Redlock({ key: 'cache:rebuild' })
async rebuildCache() {
// Only one instance rebuilds cache at a time
}Problem: LockAcquisitionError: Failed to acquire lock
Solutions:
- Check Redis connectivity: Ensure all nodes are reachable
- Verify quorum settings: Need majority of nodes (or configured quorum)
- Check lock contention: Another process may hold the lock
- Increase retry attempts or delay in configuration
Problem: Lock expires during long operation
Solutions:
- Use
using()method instead of manualacquire()/release()- it auto-extends - Increase
defaultTtlin module configuration - Check if operation can be split into smaller atomic operations
Problem: Memory usage grows over time
Solutions:
- Ensure proper module cleanup (we handle this automatically via
onModuleDestroy) - Check for uncaught errors that prevent lock release
- Use
using()method to guarantee cleanup
Problem: LockManager not initialized error
Solutions:
- Ensure Redis clients are connected before module initialization
- Check for errors in
forRootAsyncfactory function - Verify
onModuleInitlifecycle hook completes successfully
For more help, see:
❌ Manual lock release ❌ No automatic extension ❌ No distributed consensus ❌ Race conditions in cleanup
✅ Automatic lifecycle management ✅ Auto-extension for long operations ✅ Distributed locking with quorum ✅ Enhanced error handling
Most NestJS Redis libraries focus on caching. This library is purpose-built for distributed locking:
- ✅ Redlock algorithm implementation
- ✅ Automatic lock extension via
using() - ✅ NestJS decorator for zero-boilerplate
- ✅ Built on
redlock-universal - ✅ Universal Redis client support (node-redis + ioredis)
MIT
- redlock-universal - The underlying distributed lock library
Issues and pull requests are welcome! Please see our contributing guidelines.