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 through @vcms-io/solidis-extensions
  • Lock renewal and automatic release
  • Preventing race conditions
1
Simple Distributed Lock
This 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 Examples
1import 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-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