Solidis LogoSolidis
← 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 Lock
1import { 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 Examples
1import { 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 TTL
    Prevent deadlocks from crashed processes
  • Use unique lock tokens
    Prevent accidental lock release by other processes
  • Implement lock renewal for long operations
    Extend TTL while work is in progress