Handling Distributed Locking in PHP with Redis for Concurrency Control

Handling Distributed Locking in PHP with Redis for Concurrency Control

Learn how to handle distributed locking in PHP with Redis. Prevent race conditions and ensure safe concurrent operations using SETNX, EXPIRE, and the Redlock algorithm.

Introduction

When multiple processes modify shared resources, race conditions can cause data corruption and inconsistent results. Distributed locking prevents these issues by ensuring only one process can modify data at a time.

Traditional database-based locking is slow and does not scale well. Redis offers fast, in-memory locks that work in single-server and distributed environments.

This guide covers:

  • Using Redis SETNX for basic locks
  • Implementing expiration to prevent deadlocks
  • Using the Redlock algorithm for high-availability locking
  • Best practices for safe concurrency control in PHP

1. Why Use Redis for Distributed Locking?

Locking is required in cases like:

  • Preventing duplicate payments in payment gateways
  • Ensuring unique database updates when multiple workers run in parallel
  • Processing jobs in a queue without duplication

Traditional locking solutions include:

  • MySQL row-level locking – Slow and blocks transactions
  • File-based locking – Not scalable in multi-server environments

Redis provides fast, non-blocking, and distributed locks.

2. Implementing a Basic Lock with SETNX

Redis provides SETNX (SET if Not eXists) to create a lock if it does not already exist.

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$lockKey = "process_lock";
$lockAcquired = $redis->setnx($lockKey, time());

if (!$lockAcquired) {
    die("Another process is already running.");
}

echo "Lock acquired. Processing task...";
sleep(5);

$redis->del($lockKey); // Release lock after processing
  • If another process already holds the lock, it cannot be acquired again.
  • After execution, the lock is deleted.

3. Preventing Deadlocks with Expiring Locks

A major risk with SETNX is deadlocks—if a process crashes before releasing the lock, the lock stays forever. To prevent this, set an expiration using EXPIRE.

$lockKey = "process_lock";
$lockAcquired = $redis->setnx($lockKey, time());

if ($lockAcquired) {
    $redis->expire($lockKey, 10); // Lock expires in 10 seconds
} else {
    die("Another process is running.");
}

echo "Processing task...";
sleep(5);
$redis->del($lockKey);
  • The lock automatically expires after 10 seconds if not manually released.
  • Prevents long-lasting deadlocks.

4. Using SET with NX and EX for Atomic Locking

To atomically set a lock with an expiration in one step, use:

$lockAcquired = $redis->set("process_lock", time(), ["NX", "EX" => 10]); // Lock expires in 10 seconds
  • NX ensures the lock is created only if it does not exist.
  • EX sets an automatic expiration.
  • More efficient than separate SETNX and EXPIRE calls.

5. Ensuring Safe Lock Expiry with Unique Lock Values

If a process takes longer than expected, the lock may expire while it is still running. Another process may acquire the lock and modify data simultaneously, leading to inconsistencies.

To solve this, store a unique lock ID and verify it before deleting.

$lockValue = uniqid();
$lockAcquired = $redis->set("process_lock", $lockValue, ["NX", "EX" => 10]);

if (!$lockAcquired) {
    die("Lock already acquired by another process.");
}

// Perform critical operation
sleep(5);

// Check if we still own the lock before releasing
if ($redis->get("process_lock") === $lockValue) {
    $redis->del("process_lock");
}
  • Ensures only the process that set the lock can release it.
  • Prevents race conditions where an expired lock is acquired by another process before deletion.

6. Implementing Distributed Locking with the Redlock Algorithm

For multi-node Redis deployments, a single Redis instance lock is unreliable. If Redis crashes, the lock is lost.

The Redlock algorithm ensures fault-tolerant locking across multiple Redis nodes.

Using the redislabs/php-redlock Library

Install via Composer:

composer require redislabs/php-redlock

Example implementation:

require 'vendor/autoload.php';

$redis = new \RedLock\RedLock([
    ['127.0.0.1', 6379],
    ['127.0.0.2', 6379],
    ['127.0.0.3', 6379]
]);

$lock = $redis->lock("resource_name", 10000); // Lock expires in 10 seconds

if ($lock) {
    echo "Lock acquired. Processing...";

    sleep(5); // Simulate task execution

    $redis->unlock($lock);
} else {
    echo "Failed to acquire lock.";
}
  • Ensures a lock is held across multiple Redis nodes before allowing execution.
  • Prevents data inconsistencies in distributed environments.

7. Best Practices for Redis Distributed Locking

Always set an expiration on locks to prevent deadlocks.
Use unique lock values to verify ownership before deletion.
For multi-server environments, use Redlock for fault-tolerant locking.
Use SET with NX and EX for atomic locking.
Monitor Redis locks using TTL process_lock to track expiration.

Conclusion

Redis-based distributed locking prevents race conditions and ensures safe concurrent execution in PHP applications.

This guide covered:

  • Basic locking with SETNX and expiration
  • Using SET NX EX for atomic lock management
  • Implementing unique lock values for safety
  • Using the Redlock algorithm for multi-node locking

By applying these techniques, you can handle concurrent operations efficiently while maintaining data consistency.

Leave a Reply