← Back to Tutorials
Advanced
30 min
Distributed Locking with Redis
Learn how to implement distributed locks to coordinate operations across multiple processes and servers.
What You'll Learn
- Simple distributed lock implementation
- RedLock through @vcms-io/solidis-extensions
- Lock renewal and automatic release
- Preventing race conditions
1
Simple Distributed LockThis lock uses DELEX, which needs Redis 8.4 or later, and SET with IFEQ, which needs Redis 8.4 or Valkey 8.1. Valkey has no DELEX.
1import { SolidisFeaturedClient } from '@vcms-io/solidis/featured';
2import { randomUUID } from 'node:crypto';
3
4export class DistributedLock {
5 private client: SolidisFeaturedClient;
6 private prefix: string;
7
8 constructor(options: { host?: string; port?: number; prefix?: string } = {}) {
9 this.client = new SolidisFeaturedClient({
10 host: options.host || '127.0.0.1',
11 port: options.port || 6379,
12 });
13 this.prefix = options.prefix || 'lock:';
14 }
15
16 async connect(): Promise<void> {
17 await this.client.connect();
18 }
19
20 /**
21 * Acquire a lock
22 */
23 async acquire(
24 resource: string,
25 ttlMs: number = 10000
26 ): Promise<string | null> {
27 const key = `${this.prefix}${resource}`;
28 const value = randomUUID();
29 const ttlSeconds = Math.ceil(ttlMs / 1000);
30
31 // SET NX EX: Set if not exists with expiry
32 const result = await this.client.set(key, value, {
33 setIfKeyNotExists: true,
34 expireInSeconds: ttlSeconds,
35 });
36
37 return result !== null ? value : null;
38 }
39
40 /**
41 * Release a lock
42 */
43 async release(resource: string, token: string): Promise<boolean> {
44 const key = `${this.prefix}${resource}`;
45
46 // DELEX IFEQ (Redis 8.4+): Delete only if we still hold the lock
47 const result = await this.client.delex(key, { ifValueEquals: token });
48
49 return result === 1;
50 }
51
52 /**
53 * Extend lock TTL
54 */
55 async extend(
56 resource: string,
57 token: string,
58 ttlMs: number
59 ): Promise<boolean> {
60 const key = `${this.prefix}${resource}`;
61 const ttlSeconds = Math.ceil(ttlMs / 1000);
62
63 const result = await this.client.set(key, token, {
64 setIfValueEquals: token,
65 expireInSeconds: ttlSeconds,
66 });
67
68 return result !== null;
69 }
70
71 /**
72 * Execute function with lock
73 */
74 async withLock<T>(
75 resource: string,
76 fn: () => Promise<T>,
77 options: {
78 ttlMs?: number;
79 retryDelayMs?: number;
80 retryCount?: number;
81 } = {}
82 ): Promise<T> {
83 const {
84 ttlMs = 10000,
85 retryDelayMs = 100,
86 retryCount = 10,
87 } = options;
88
89 let token: string | null = null;
90 let attempts = 0;
91
92 // Try to acquire lock with retries
93 while (attempts < retryCount) {
94 token = await this.acquire(resource, ttlMs);
95 if (token) break;
96
97 attempts++;
98 await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
99 }
100
101 if (!token) {
102 throw new Error(`Failed to acquire lock for ${resource}`);
103 }
104
105 try {
106 return await fn();
107 } finally {
108 await this.release(resource, token);
109 }
110 }
111}2
Usage Examples1import type { CacheManager } from './cache-manager';
2import { DistributedLock } from './distributed-lock';
3
4declare const db: any;
5declare const paymentGateway: any;
6declare const cache: CacheManager;
7declare function generateDailyReport(): Promise<void>;
8
9const lock = new DistributedLock();
10await lock.connect();
11
12// Example 1: Prevent duplicate payment processing
13async function processPayment(orderId: string, amount: number) {
14 await lock.withLock(
15 `payment:${orderId}`,
16 async () => {
17 // Check if payment already processed
18 const order = await db.getOrder(orderId);
19 if (order.status === 'paid') {
20 return;
21 }
22
23 // Process payment
24 await paymentGateway.charge(amount);
25
26 // Update order status
27 await db.updateOrder(orderId, { status: 'paid' });
28 },
29 { ttlMs: 30000 } // 30 seconds
30 );
31}
32
33// Example 2: Ensure single cron job execution
34async function dailyReportJob() {
35 const token = await lock.acquire('cron:daily-report', 300000); // 5 min
36
37 if (!token) {
38 console.log('Job already running');
39 return;
40 }
41
42 try {
43 await generateDailyReport();
44 } finally {
45 await lock.release('cron:daily-report', token);
46 }
47}
48
49// Example 3: Coordinate resource updates
50async function updateUserProfile(userId: string, data: any) {
51 await lock.withLock(`user:${userId}`, async () => {
52 const user = await db.getUser(userId);
53 const updatedUser = { ...user, ...data };
54 await db.updateUser(userId, updatedUser);
55 await cache.delete(`user:${userId}`);
56 });
57}RedLock Algorithm (Using Extensions)
For production-grade distributed locking
For production use, consider using the @vcms-io/solidis-extensions package which includes a battle-tested RedLock implementation.
npm install @vcms-io/solidis-extensionsBest Practices
- ✓Always set appropriate TTLPrevent deadlocks from crashed processes
- ✓Use unique lock tokensPrevent accidental lock release by other processes
- ✓Implement lock renewal for long operationsExtend TTL while work is in progress
