← 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 algorithm for fault tolerance
- Lock renewal and automatic release
- Preventing race conditions
1
Simple Distributed Lock1import { SolidisFeaturedClient } from '@vcms-io/solidis/featured';
2import { randomUUID } from '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 NX: true,
34 EX: ttlSeconds,
35 });
36
37 return result === 'OK' ? 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 // Use Lua script to ensure atomicity
47 const script = `
48 if redis.call("get", KEYS[1]) == ARGV[1] then
49 return redis.call("del", KEYS[1])
50 else
51 return 0
52 end
53 `;
54
55 const result = await this.client.eval(script, {
56 keys: [key],
57 arguments: [token],
58 });
59
60 return result === 1;
61 }
62
63 /**
64 * Extend lock TTL
65 */
66 async extend(
67 resource: string,
68 token: string,
69 ttlMs: number
70 ): Promise<boolean> {
71 const key = `${this.prefix}${resource}`;
72 const ttlSeconds = Math.ceil(ttlMs / 1000);
73
74 const script = `
75 if redis.call("get", KEYS[1]) == ARGV[1] then
76 return redis.call("expire", KEYS[1], ARGV[2])
77 else
78 return 0
79 end
80 `;
81
82 const result = await this.client.eval(script, {
83 keys: [key],
84 arguments: [token, ttlSeconds.toString()],
85 });
86
87 return result === 1;
88 }
89
90 /**
91 * Execute function with lock
92 */
93 async withLock<T>(
94 resource: string,
95 fn: () => Promise<T>,
96 options: {
97 ttlMs?: number;
98 retryDelayMs?: number;
99 retryCount?: number;
100 } = {}
101 ): Promise<T> {
102 const {
103 ttlMs = 10000,
104 retryDelayMs = 100,
105 retryCount = 10,
106 } = options;
107
108 let token: string | null = null;
109 let attempts = 0;
110
111 // Try to acquire lock with retries
112 while (attempts < retryCount) {
113 token = await this.acquire(resource, ttlMs);
114 if (token) break;
115
116 attempts++;
117 await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
118 }
119
120 if (!token) {
121 throw new Error(`Failed to acquire lock for ${resource}`);
122 }
123
124 try {
125 return await fn();
126 } finally {
127 await this.release(resource, token);
128 }
129 }
130}2
Usage Examples1import { DistributedLock } from './distributed-lock';
2
3const lock = new DistributedLock();
4await lock.connect();
5
6// Example 1: Prevent duplicate payment processing
7async function processPayment(orderId: string, amount: number) {
8 await lock.withLock(
9 `payment:${orderId}`,
10 async () => {
11 // Check if payment already processed
12 const order = await db.getOrder(orderId);
13 if (order.status === 'paid') {
14 return;
15 }
16
17 // Process payment
18 await paymentGateway.charge(amount);
19
20 // Update order status
21 await db.updateOrder(orderId, { status: 'paid' });
22 },
23 { ttlMs: 30000 } // 30 seconds
24 );
25}
26
27// Example 2: Ensure single cron job execution
28async function dailyReportJob() {
29 const token = await lock.acquire('cron:daily-report', 300000); // 5 min
30
31 if (!token) {
32 console.log('Job already running');
33 return;
34 }
35
36 try {
37 await generateDailyReport();
38 } finally {
39 await lock.release('cron:daily-report', token);
40 }
41}
42
43// Example 3: Coordinate resource updates
44async function updateUserProfile(userId: string, data: any) {
45 await lock.withLock(`user:${userId}`, async () => {
46 const user = await db.getUser(userId);
47 const updatedUser = { ...user, ...data };
48 await db.updateUser(userId, updatedUser);
49 await cache.invalidate(`user:${userId}`);
50 });
51}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-extensions
Best 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
