Skip to content

Instantly share code, notes, and snippets.

@kelvinndmo
Created July 24, 2023 13:58
Show Gist options
  • Save kelvinndmo/cf56bfc306a8aa797bb6eafee6f2349b to your computer and use it in GitHub Desktop.
Save kelvinndmo/cf56bfc306a8aa797bb6eafee6f2349b to your computer and use it in GitHub Desktop.

Optimistic locking is a concurrency control technique used to manage data consistency in multi-user environments, particularly when dealing with distributed systems or databases. In our specific case, we are implementing optimistic locking with Redis to address concurrency issues when multiple requests attempt to update the same data simultaneously. The core idea behind optimistic locking is to allow multiple operations to proceed independently until the point of updating the shared data. Rather than blocking all but the first request, optimistic locking relies on versioning the data. Each time the data is updated, a version number is associated with it. When a request wants to modify the data, it checks the version number to ensure it matches the expected value. If the version matches, the update proceeds, and the version is incremented for the next update. If the version does not match, it indicates that the data has been modified by another request, and the current request must retry the operation with the updated version. This approach optimistically assumes that conflicts will be rare and allows concurrent operations to progress, leading to improved performance and reduced contention. By incorporating optimistic locking in our Redis-based data store, we can ensure data consistency while still achieving fast and efficient operations in our NestJS project.

Steps to Implement Optimistic Locking

  1. Update the RedisService to include versioning:

In the RedisService, we will add a version number to the stored data. When updating the data, we'll check if the version number matches the expected value before making changes. This ensures that only one request will succeed in updating the data, while others will need to retry with the updated version.

// redis.service.ts

import { Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';

@Injectable()
export class RedisService {
  private readonly redisClient: Redis.Redis;

  constructor() {
    this.redisClient = new Redis(); // You can add Redis configuration here if needed
  }

  async setValueWithVersion(key: string, value: any, version: number): Promise<boolean> {
    const transaction = this.redisClient.multi();
    transaction.watch(key);

    const currentVersion = await this.redisClient.get(`${key}:version`);
    if (Number(currentVersion) === version) {
      transaction.set(key, JSON.stringify(value));
      transaction.incr(`${key}:version`);
      const result = await transaction.exec();

      if (result) {
        return true; // Update successful
      }
    }

    return false; // Update failed due to concurrency issue
  }

  async getValue(key: string): Promise<any> {
    const value = await this.redisClient.get(key);
    return value ? JSON.parse(value) : null;
  }
}
  1. Modify the ConversationService to use optimistic locking:

In the ConversationService, we'll use the setValueWithVersion method from the RedisService, passing the conversation data and its version number as parameters.

// conversation.service.ts

import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service';

@Injectable()
export class ConversationService {
  constructor(private readonly redisService: RedisService) {}

  async saveConversation(conversationId: string, conversationData: any, version: number): Promise<boolean> {
    return this.redisService.setValueWithVersion(`conversation:${conversationId}`, conversationData, version);
  }

  async getConversation(conversationId: string): Promise<any> {
    return this.redisService.getValue(`conversation:${conversationId}`);
  }
}
  1. Handling updates and retries in the controller:

In the NestJS controller, we need to handle updates from different socket connections. To manage concurrency, we'll implement a while loop that retries the update for a maximum number of times. If the update is successful, we'll return a success response; otherwise, we'll return a message indicating that the update failed due to concurrency issues, and the user should try again later.

// your.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { ConversationService } from './conversation.service';

@Controller('conversations')
export class YourController {
  constructor(private readonly conversationService: ConversationService) {}

  @Post(':id')
  async updateConversation(@Param('id') conversationId: string, @Body() data: any): Promise<any> {
    const maxRetries = 3;
    let retries = 0;
    let version = data.version || 0; // Assuming the client sends the current version

    while (retries < maxRetries) {
      const conversationData = await this.conversationService.getConversation(conversationId);

      if (conversationData) {
        const updatedVersion = version + 1; // Increment the version for the update
        const success = await this.conversationService.saveConversation(conversationId, data, updatedVersion);

        if (success) {
          return { success: true };
        }
      }

      retries++;
    }

    return { success: false, message: 'Update failed due to concurrency issues. Please try again later.' };
  }
}

By following these steps and implementing optimistic locking, we can ensure data consistency and prevent conflicts when multiple requests try to update the same conversation simultaneously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment