Distributed Locking in Practice: Redis Redlock vs. PostgreSQL Advisory Locks

Mutual exclusion across distributed worker nodes is essential for billing jobs and inventory syncs. Compare the operational guarantees of Redis Redlock against PostgreSQL transaction-scoped advisory locks.

Pragmatic Guideline: If your critical section is strictly confined to mutations within a single relational database transaction, prefer row-level locking via Django's select_for_update instead of introducing distributed lock overhead.

The Concurrency Hazard in Distributed Microservices

In distributed web architectures, multiple background workers, API servers, and cron runners frequently compete for shared, non-idempotent resources. Common examples include aggregating hourly user billing statements, synchronizing inventory allocations across warehouse endpoints, executing payout batches, or invalidating edge cache hierarchies. Without reliable mutual exclusion, concurrent executions lead to race conditions, duplicate financial transfers, and state corruption.

Engineers often default to primitive distributed locking mechanisms, such as writing a Redis key with SET key val NX EX 30. While suitable for basic throttling, single-node Redis locks fail catastrophically during master failovers because asynchronous replication allows duplicate locks to be issued simultaneously. To achieve enterprise safety, teams must choose between distributed quorum algorithms (like Redis Redlock) and relational database locks (like PostgreSQL Advisory Locks).

1. Mechanism Analysis: Redlock vs. Advisory Locks

To evaluate these two locking paradigms effectively, we must examine their underlying mechanics:

  1. Redis Redlock: Redlock requires deploying an odd number of independent Redis master instances (typically 5) that do not replicate between one another. A client attempts to acquire the lock sequentially across all 5 nodes with a small timeout. If it acquires the lock on a strict majority ($N/2 + 1 = 3$) within the validity window, the lock is granted. However, Redlock remains controversial because it relies on assumptions regarding system clock drift, process pauses (garbage collection or virtual machine descheduling), and network partition duration.
  2. PostgreSQL Advisory Locks: PostgreSQL provides application-level locks using 64-bit integer keys stored entirely in server shared memory. Unlike row-level locks, advisory locks do not write to disk, do not generate WAL records, and never block table vacuuming. Most importantly, when invoked via pg_try_advisory_xact_lock(), the lock is intrinsically bound to the current database transaction and is automatically released upon commit or rollback.

2. Feature & Operational Comparison

The architectural differences between Redlock and PostgreSQL advisory locks are detailed below:

Operational Metric Redis Redlock (5 Independent Masters) PostgreSQL Advisory Locks
Lock Scope & Lifecycle Explicit time-to-live (TTL) expiration with heartbeat extension Transaction or Session-scoped (auto-released on commit/disconnect)
Clock Drift Sensitivity High (System clock leaps can violate mutual exclusion) None (Independent of server wall-clock drift)
Operational Overhead Requires managing 5 separate non-clustered Redis instances Zero additional infrastructure (Reuses primary PostgreSQL cluster)
Acquisition Latency Sub-5ms across network roundtrips to 5 nodes Sub-1ms (Direct query to existing connection pool)
Failure Cleanup Relies on TTL expiration if worker crashes before release Instantaneous (Server drops lock immediately upon socket closure)
Best Suited For High-frequency ephemeral caching, cross-language services Transactional data mutations, financial ledgers, scheduled jobs

3. Implementing PostgreSQL Transactional Locks in Python

PostgreSQL's pg_try_advisory_xact_lock attempts to acquire the lock non-blockingly and returns a boolean. Because it is transaction-scoped, no worker crash or network failure can ever leak the lock:

import contextlib
import hashlib
from django.db import connection, transaction
import logging

logger = logging.getLogger("locking")

def string_to_lock_id(lock_key: str) -> int:
    # Convert an arbitrary string key to a signed 64-bit bigint for PostgreSQL
    digest = hashlib.sha256(lock_key.encode('utf-8')).hexdigest()
    return int(digest[:16], 16) - (1 << 63)

@contextlib.contextmanager
def postgres_advisory_lock(lock_key: str):
    # Acquire a transaction-scoped PostgreSQL advisory lock.
    # If another worker holds the lock, immediately raises BlockingIOError.
    lock_id = string_to_lock_id(lock_key)
    with transaction.atomic():
        with connection.cursor() as cursor:
            cursor.execute("SELECT pg_try_advisory_xact_lock(%s);", [lock_id])
            acquired = cursor.fetchone()[0]
            
            if not acquired:
                logger.warning(f"Could not acquire lock for key: {lock_key}. Resource busy.")
                raise BlockingIOError(f"Resource {lock_key} is currently locked by another worker.")
            
            logger.info(f"Acquired transaction lock for: {lock_key}")
            try:
                yield
            finally:
                # Lock is released automatically when transaction.atomic() commits or rolls back
                pass

4. Coordinating Background Jobs with Advisory Locks

Below is how a scheduled task runner leverages the advisory lock to guarantee singleton execution across a fleet of twenty Celery workers:

def process_hourly_subscription_billing():
    lock_name = "billing:hourly_cycle:2026-09-24-10"
    try:
        with postgres_advisory_lock(lock_name):
            logger.info("Executing billing reconciliation pipeline...")
            # Perform atomic ledger balance calculations
            perform_subscription_debits()
            logger.info("Billing cycle completed successfully.")
    except BlockingIOError:
        logger.info("Hourly cycle already in progress on another node. Skipping execution.")
Architectural Continuity & Deep Dives

For related production architectures and system implementations, explore these companion guides:

Key Architectural Takeaways

For systems that already rely on PostgreSQL as their primary source of truth, PostgreSQL advisory locks provide superior correctness, lower latency, and zero infrastructure overhead compared to Redis Redlock. Because advisory locks participate in PostgreSQL ACID transaction lifecycles, they eliminate clock drift vulnerabilities and guarantee that crashes will never cause lingering zombie locks.

All Insights
Chat on WhatsApp