Skip to content

Distributed Locks: Ensuring Data Consistency

Distributed Locks: Ensuring Data Consistency

In a distributed system, where multiple instances of a service are running, a standard lock (like C#โ€˜s lock or Pythonโ€™s threading.Lock) wonโ€™t work because it only applies to a single process. A Distributed Lock is needed to ensure only one instance of a service performs a specific task at a time.

๐Ÿ—๏ธ Why Use a Distributed Lock?

  • Prevent Double Processing: Ensure a scheduled job (like sending an email) is only run once across all nodes.
  • Resource Protection: Guard a shared resource (like a specific database row or a file) from being modified by multiple services simultaneously.
  • Consistency: Maintain strict order and consistency in data processing.

๐Ÿš€ Common Implementations

  1. Redis (Redlock): One of the most popular choices due to its performance and simplicity. It uses a Time-to-Live (TTL) to ensure the lock is eventually released.
  2. ZooKeeper: A more robust, strongly consistent choice. It uses ephemeral nodes that disappear if the client disconnects.
  3. Database (Advisory Locks): SQL Server, PostgreSQL, and MySQL support advisory locks, though they are often slower than Redis.

๐Ÿ“Š Key Features & Challenges

1. TTL (Time-to-Live)

Every distributed lock must have an expiration time. This ensures that if the service instance crashes while holding the lock, the lock will eventually be freed.

2. Fencing Tokens

To prevent a โ€œstaleโ€ process (one that was paused or delayed) from performing an action after its lock expired, use an incrementing Fencing Token. The target resource (e.g., the database) should only accept the highest token it has seen.

3. Renewal (Keep-Alive)

If a task takes longer than expected, the service should actively โ€œrenewโ€ the lock to prevent it from expiring.

๐Ÿ› ๏ธ Code Example (Redis-based Concept)

// 1. Acquire the lock with a unique ID and a TTL
bool acquired = await redis.StringSetAsync("my_lock", processId, TimeSpan.FromSeconds(30), When.NotExists);

if (acquired)
{
    try 
    {
        // 2. Perform the critical task
        await ProcessTaskAsync();
    }
    finally 
    {
        // 3. Release ONLY if we still own it (check the processId!)
        await redis.ExecuteLuaAsync(releaseScript, new { key = "my_lock", value = processId });
    }
}

๐Ÿ’ก Best Practices

  • Granularity: Keep locks as specific as possible. Lock only what is necessary.
  • Duration: Hold the lock for the shortest time possible.
  • Wait Time: Implement a โ€œwait-then-retryโ€ strategy with exponential backoff if the lock is already held.