Skip to main content

Command Palette

Search for a command to run...

Mastering Distributed Locks in NestJS with RedisX

Published
2 min read
Mastering Distributed Locks in NestJS with RedisX

Getting a Grip on Distributed Locks in NestJS with RedisX

Distributed systems are complex beasts. When multiple instances of your application decide to access a shared resource at the same time, chaos can ensue. We're talking race conditions, corrupted data, and system crashes. Not what you want. Enter distributed locks, the unsung heroes that ensure only one instance accesses a critical section at any given time.

RedisX: Your Go-To for Locking

If you're working with NestJS, RedisX is your friend. It's got a neat plugin architecture for Redis, including distributed locks. The @nestjs-redisx/locks package makes it super smooth to implement Redis-backed locks. Auto-renewal? Check. Deadlock prevention? Double check. Let's see how to set it up.

Installing the Essentials

First things first, get the packages:

npm install @nestjs-redisx/core @nestjs-redisx/locks

Next, configure the Redis module in your AppModule. Here's a snippet to get you rolling:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-redisx/core';
import { LocksPlugin } from '@nestjs-redisx/locks';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    RedisModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      plugins: [
        LocksPlugin.registerAsync({
          imports: [ConfigModule],
          inject: [ConfigService],
          useFactory: (config: ConfigService) => ({
            defaultTtl: config.get('LOCK_DEFAULT_TTL', 30000),
            retry: {
              maxRetries: config.get('LOCK_MAX_RETRIES', 3),
              initialDelay: config.get('LOCK_RETRY_DELAY', 100),
            },
            autoRenew: { enabled: config.get('LOCK_AUTO_RENEW', true) },
          }),
        }),
      ],
      useFactory: (config: ConfigService) => ({
        clients: {
          type: 'single',
          host: config.get('REDIS_HOST', 'localhost'),
          port: config.get('REDIS_PORT', 6379),
        },
      }),
    }),
  ],
})
export class AppModule {}

Locking Down Your Services

With RedisX in place, you're ready to put those locks to work. Use the @WithLock decorator to handle lock acquisition and release around your method. It's like magic, but real.

import { Injectable } from '@nestjs/common';
import { WithLock } from '@nestjs-redisx/locks';

@Injectable()
export class PaymentService {
  @WithLock({ key: 'payment:{0}', ttl: 10000 })
  async processPayment(orderId: string): Promise<Payment> {
    const order = await this.orderRepository.findById(orderId);
    if (order.status === 'paid') {
      throw new PaymentAlreadyProcessedError(orderId);
    }
    const result = await this.paymentGateway.charge(order);
    await this.orderRepository.update(orderId, { status: 'paid' });
    return result;
  }
}

Auto-Renewal and Deadlock: The Safety Nets

RedisX's lock plugin isn't just about getting locks. It offers auto-renewal for long-running operations, extending the lock's TTL as needed. Plus, it prevents deadlocks by letting locks expire if the process crashes. This ensures that other instances won't be stuck waiting forever.

Wrap-Up

Distributed locks with RedisX in a NestJS application simplify concurrency control. They help maintain data integrity and system stability. By focusing on these robust features, you can spend more time building cool stuff and less time worrying about distributed locking complexities.