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      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    // 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, [key], [token]);
56
57    return result === 1;
58  }
59
60  /**
61   * Extend lock TTL
62   */
63  async extend(
64    resource: string,
65    token: string,
66    ttlMs: number
67  ): Promise<boolean> {
68    const key = `${this.prefix}${resource}`;
69    const ttlSeconds = Math.ceil(ttlMs / 1000);
70
71    const script = `
72      if redis.call("get", KEYS[1]) == ARGV[1] then
73        return redis.call("expire", KEYS[1], ARGV[2])
74      else
75        return 0
76      end
77    `;
78
79    const result = await this.client.eval(script, [key], [token, ttlSeconds.toString()]);
80
81    return result === 1;
82  }
83
84  /**
85   * Execute function with lock
86   */
87  async withLock<T>(
88    resource: string,
89    fn: () => Promise<T>,
90    options: {
91      ttlMs?: number;
92      retryDelayMs?: number;
93      retryCount?: number;
94    } = {}
95  ): Promise<T> {
96    const {
97      ttlMs = 10000,
98      retryDelayMs = 100,
99      retryCount = 10,
100    } = options;
101
102    let token: string | null = null;
103    let attempts = 0;
104
105    // Try to acquire lock with retries
106    while (attempts < retryCount) {
107      token = await this.acquire(resource, ttlMs);
108      if (token) break;
109
110      attempts++;
111      await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
112    }
113
114    if (!token) {
115      throw new Error(`Failed to acquire lock for ${resource}`);
116    }
117
118    try {
119      return await fn();
120    } finally {
121      await this.release(resource, token);
122    }
123  }
124}
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