Enhancing Data Consistency in ASP.NET Core Microservices on Kubernetes with Distributed Locks

In modern microservices architectures, particularly those running in Kubernetes, managing concurrent access to shared resources is critical. Distributed locks ensure that only one instance of a microservice can access a critical section or resource at a time, preventing race conditions and ensuring consistency. This article explores how to implement distributed locks in an ASP.NET Core Web API microservice on Kubernetes, using PostgreSQL advisory locks and the DistributedLock NuGet package.

Source Code found here

Table of Contents

  1. Introduction
  2. Understanding Advisory Locks
  3. Microservice Architecture on Kubernetes
  4. Setting Up PostgreSQL
  5. Installing the DistributedLock Package
  6. Implementing the Distributed Lock
  7. Using the Distributed Lock in a Web API
  8. Testing the Implementation
  9. Conclusion
  10. References

Introduction

In a Kubernetes environment, microservices are often scaled horizontally, resulting in multiple pods running simultaneously. Managing concurrent access to shared resources across these pods can lead to challenges such as race conditions. Distributed locks address these challenges by ensuring exclusive access to resources. This guide demonstrates implementing distributed locks in an ASP.NET Core microservice deployed on Kubernetes using PostgreSQL advisory locks.

Understanding Advisory Locks

What Are Advisory Locks?

Advisory locks in PostgreSQL are application-level locks that enable the management of concurrent access to resources by locking and unlocking arbitrary integer keys. These locks depend on the application to manage them, as PostgreSQL does not enforce restrictions at the database level.

How Advisory Locks Work

Advisory locks can be session-level or transaction-level:

  • Session-Level Locks: Held for the duration of the database session.
  • Transaction-Level Locks: Held for the duration of a transaction.

Key Functions:

  • pg_try_advisory_lock: Tries to acquire a session-level lock.
  • pg_advisory_unlock: Releases a session-level lock.
  • pg_try_advisory_xact_lock: Tries to acquire a transaction-level lock.
  • pg_advisory_unlock_all: Releases all session-level locks held by the session.

Example:

-- Acquire a session-level lock
SELECT pg_try_advisory_lock(12345);

-- Release a session-level lock
SELECT pg_advisory_unlock(12345);

Microservice Architecture on Kubernetes

Scenario: Multiple Pods

In Kubernetes, microservices are typically deployed as pods, which can be scaled horizontally to handle increased load. Consider a scenario where a microservice handles order processing. Multiple instances (pods) of this service may be running, each trying to process orders from a shared queue in PostgreSQL. Without proper synchronization, two pods might process the same order simultaneously, leading to inconsistencies.

Problem: Without distributed locking, concurrent access to shared resources (e.g., processing the same order) can cause data inconsistencies.

Benefits of Distributed Locking

Distributed locking solves this by:

  • Ensuring exclusive access to critical sections or resources.
  • Preventing race conditions by allowing only one pod to hold a lock on a resource.
  • Enhancing data consistency and integrity across multiple instances.

Setting Up PostgreSQL

  1. Install PostgreSQL: Download and install PostgreSQL from here.
  2. Create a Database
  3. Connection String: Note your PostgreSQL connection string to use in the ASP.NET Core application.

Installing the DistributedLock Package

  1. Open your ASP.NET Core project.
  2. Install the DistributedLock.Postgres package via NuGet: dotnet add package DistributedLock.Postgres

Implementing the Distributed Lock

Program.cs file

string connectionString = "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=yourpassword;";
builder.Services.AddSingleton<IDistributedLockProvider>(sp =>
    new PostgresDistributedSynchronizationProvider(connectionString));

Using the Distributed Lock in a Web API

Creating a Controller

Implement a controller to handle lock-related requests.

 [ApiController]
 [Route("[controller]")]
 public class WeatherForecastController(ILogger<WeatherForecastController> logger, IDistributedLockProvider distributedLockProvider,
     IDistributedReaderWriterLockProvider distributedReaderWriterLockProvider) : ControllerBase
 {
     private static readonly string[] Summaries = new[]
     {
         "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
     };

     private readonly ILogger<WeatherForecastController> _logger = logger;
     private readonly IDistributedLockProvider distributedLockProvider = distributedLockProvider;
     private readonly IDistributedReaderWriterLockProvider distributedReaderWriterLockProvider = distributedReaderWriterLockProvider;
     private static List<WeatherForecast> _forecasts;

     static WeatherForecastController()
     {
         _forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
         {
             Date = DateTime.Now.AddDays(index),
             TemperatureC = Random.Shared.Next(-20, 55),
             Summary = Summaries[Random.Shared.Next(Summaries.Length)]
         }).ToList();
     }

     [HttpGet(Name = "GetWeatherForecast")]
     public async Task<IEnumerable<WeatherForecast>> Get(string lockname, CancellationToken cancellationToken = default(CancellationToken))
     {
         using (await distributedLockProvider.AcquireLockAsync(lockname, TimeSpan.FromSeconds(30), cancellationToken))
         {
             _logger.LogInformation("Entered critical phase");
             await Task.Delay(20000);

             _logger.LogInformation("exit critical phase");
             return _forecasts;
         }
     }

Testing the Implementation

  1. Run the API
  2. Simulate Concurrent Requests:
    • Use tools like Postman or custom scripts to send concurrent POST requests to /WeatherForecast with the same lock name to test lock acquisition and release.
  3. Observe Lock Behavior:
    Only one request should process the order while others wait for the lock or receive a conflict response.

Conclusion

In a Kubernetes-based microservices architecture, where multiple pods may handle shared resources simultaneously, distributed locks using PostgreSQL advisory locks provide a robust mechanism for managing concurrency. By integrating these locks with ASP.NET Core microservices, you can maintain data integrity and avoid race conditions in your distributed systems. The DistributedLock.Postgres package simplifies the implementation of these locks, leveraging PostgreSQL’s powerful advisory locking mechanism.

References

Leave a comment