Back to blog

Redlock vs PostgreSQL Advisory Locks: When You Don't Need Redis for Distributed Locking

|
| postgresql, redis, distributed-locks, redlock, advisory-locks, java, go

We debated Redlock vs Postgres locks after a nasty outage. “We need Redis for distributed locks.” I asked why. “Because that’s how you do distributed locking.” The team was about to add Redis to their stack—five instances for Redlock—purely to prevent duplicate payment processing. They already had PostgreSQL. They didn’t know PostgreSQL had advisory locks.

This is a pattern I’ve seen repeatedly: teams reaching for specialized infrastructure when their existing database already provides the capability they need. PostgreSQL advisory locks are less glamorous than Redlock. They don’t have the same industry buzz. But for most use cases—and especially for teams that already operate PostgreSQL—they’re simpler to deploy, simpler to operate, and actually safer.

The irony is that Redlock, despite its complexity, has known safety issues. Martin Kleppmann’s famous critique “How to do distributed locking” exposed fundamental problems with the algorithm, particularly around clock skew and GC pauses. PostgreSQL advisory locks, by contrast, are simpler and don’t depend on clock synchronization. They have their own limitations—single point of failure if your database is down—but those limitations are usually already addressed by your database HA strategy.

The real question isn’t “Redis vs PostgreSQL.” It’s “do you need to add infrastructure for this, or can you use what you have?” If you already operate PostgreSQL well, advisory locks are essentially free. If you’d have to add Redis just for locking, the operational burden is significant.

Tested on: PostgreSQL 16, Redis 7, Java 21, Go 1.22

When Do You Need Distributed Locks?

Use Cases

1. Preventing duplicate processing
   - Payment processing (charge once)
   - Job scheduling (run once)
   - Data migration (process each row once)

2. Coordinating access to external resources
   - Rate-limited APIs (one caller at a time)
   - Shared files (exclusive write)
   - Hardware devices (single access)

3. Leader election
   - Single active instance
   - Scheduled tasks (one executor)

When NOT to Use Distributed Locks

❌ Database row-level locking
   → Use SELECT FOR UPDATE or transactions

❌ Preventing race conditions in DB
   → Use database constraints, transactions

❌ Cache stampede prevention
   → Use probabilistic early expiration

❌ Rate limiting
   → Use token bucket or sliding window

PostgreSQL Advisory Locks

Session-Level Locks

-- Acquire lock (blocks if held by another session)
SELECT pg_advisory_lock(12345);

-- Try to acquire (returns immediately)
SELECT pg_try_advisory_lock(12345);  -- Returns TRUE or FALSE

-- Release lock
SELECT pg_advisory_unlock(12345);

-- Lock is automatically released when session ends

Transaction-Level Locks

-- Automatically released at end of transaction
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- ... do work ...
COMMIT;  -- Lock released automatically

Lock Key Strategies

-- Single integer key
SELECT pg_advisory_lock(hashtext('payment:order_123'));

-- Two integer keys (for namespacing)
SELECT pg_advisory_lock(1, 123);  -- (namespace, id)

-- Entity-based locking
SELECT pg_advisory_lock(
    hashtext('orders'),           -- table
    hashtext('order_123')         -- id
);

Redis Redlock

Basic Redis Lock

# Single Redis instance (NOT safe for distributed systems)
SET lock:payment:123 owner_id NX EX 30

# NX = Only if not exists
# EX 30 = Expire after 30 seconds

Redlock Algorithm (5 Redis instances)

1. Get current time in milliseconds
2. Try to acquire lock on all N (5) Redis instances
3. Lock acquired if:
   - Majority (N/2+1 = 3) instances locked
   - Total time < lock validity time
4. Effective lock time = validity - acquisition time
5. If failed, unlock all instances

Comparison

Operational Complexity

AspectPostgreSQL AdvisoryRedis Redlock
InfrastructureAlready have DBNeed 5 Redis instances
Failure modesSingle point (DB)Complex (majority)
Clock dependencyNoYes (problematic!)
Network partitionsSimpleComplex
MonitoringStandard DB toolsAdditional tooling

Performance (1000 lock/unlock cycles)

MetricPostgreSQLRedis (single)Redlock (5)
Acquire p500.8ms0.3ms2.5ms
Acquire p993.2ms1.1ms8.4ms
Release p500.5ms0.2ms1.8ms
Ops/sec8502,400280

Safety Guarantees

PropertyPostgreSQLRedis (single)Redlock
Mutual exclusion❌*⚠️**
Deadlock-free✅ (timeout)✅ (TTL)✅ (TTL)
Fault-tolerant⚠️***
* Redis single: Lock can be held by two processes after failover
** Redlock: Martin Kleppmann's critique shows edge cases
*** Requires majority available, clock sync issues

Implementation

Java - PostgreSQL Advisory Lock

// AdvisoryLock.java
@Repository
public class AdvisoryLockRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public boolean tryLock(String key, Duration timeout) {
        long lockId = hashKey(key);

        // Try to acquire with timeout
        String sql = "SELECT pg_try_advisory_lock(?)";
        Boolean acquired = jdbcTemplate.queryForObject(sql, Boolean.class, lockId);

        if (acquired != null && acquired) {
            return true;
        }

        // Retry with timeout
        long deadline = System.currentTimeMillis() + timeout.toMillis();
        while (System.currentTimeMillis() < deadline) {
            acquired = jdbcTemplate.queryForObject(sql, Boolean.class, lockId);
            if (acquired != null && acquired) {
                return true;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
        return false;
    }

    public void unlock(String key) {
        long lockId = hashKey(key);
        jdbcTemplate.execute("SELECT pg_advisory_unlock(" + lockId + ")");
    }

    private long hashKey(String key) {
        return key.hashCode() & 0x7FFFFFFFL;  // Positive integer
    }
}

// Usage
@Service
public class PaymentService {

    @Autowired
    private AdvisoryLockRepository lockRepo;

    public void processPayment(String orderId) {
        String lockKey = "payment:" + orderId;

        if (!lockRepo.tryLock(lockKey, Duration.ofSeconds(30))) {
            throw new LockAcquisitionException("Could not acquire lock");
        }

        try {
            // Process payment
            doPayment(orderId);
        } finally {
            lockRepo.unlock(lockKey);
        }
    }
}

Java - Redlock with Redisson

// RedlockService.java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

@Service
public class RedlockService {

    @Autowired
    private RedissonClient redisson;

    public void processPayment(String orderId) {
        RLock lock = redisson.getLock("payment:" + orderId);

        try {
            // Try to acquire lock for 30 seconds, hold for 10 seconds
            boolean acquired = lock.tryLock(30, 10, TimeUnit.SECONDS);

            if (!acquired) {
                throw new LockAcquisitionException("Could not acquire lock");
            }

            try {
                doPayment(orderId);
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}

Go - PostgreSQL Advisory Lock

// advisory_lock.go
package lock

import (
    "context"
    "database/sql"
    "hash/fnv"
    "time"
)

type AdvisoryLock struct {
    db *sql.DB
}

func (l *AdvisoryLock) TryLock(ctx context.Context, key string, timeout time.Duration) (bool, error) {
    lockID := hashKey(key)

    deadline := time.Now().Add(timeout)
    for time.Now().Before(deadline) {
        var acquired bool
        err := l.db.QueryRowContext(ctx,
            "SELECT pg_try_advisory_lock($1)", lockID).Scan(&acquired)
        if err != nil {
            return false, err
        }
        if acquired {
            return true, nil
        }
        time.Sleep(50 * time.Millisecond)
    }
    return false, nil
}

func (l *AdvisoryLock) Unlock(ctx context.Context, key string) error {
    lockID := hashKey(key)
    _, err := l.db.ExecContext(ctx, "SELECT pg_advisory_unlock($1)", lockID)
    return err
}

func hashKey(key string) int64 {
    h := fnv.New64a()
    h.Write([]byte(key))
    return int64(h.Sum64() & 0x7FFFFFFFFFFFFFFF)
}

Failure Scenarios

PostgreSQL Advisory Lock Failure

Scenario: Database connection dies while holding lock

1. Application acquires lock
2. Application crashes (or connection pool issue)
3. PostgreSQL detects disconnection
4. Lock automatically released
5. Another process can acquire lock

Result: Safe, automatic cleanup

Redlock Failure Scenarios

Scenario 1: Clock skew

1. Process A acquires lock (TTL 10s)
2. Clock on Redis instance 3 is 5s ahead
3. Lock on instance 3 expires early
4. Process B acquires majority including instance 3
5. Both A and B think they have the lock!

Scenario 2: Network partition

1. Process A acquires lock on 3/5 instances
2. Network partition isolates 2 instances
3. Process A's locks expire on isolated instances
4. When partition heals, state is inconsistent

Scenario 3: GC pause

1. Process A acquires lock (TTL 10s)
2. Process A has 8 second GC pause
3. Lock expires during GC
4. Process B acquires lock
5. Process A's GC ends, still thinks it has lock

Decision Matrix

Use PostgreSQL Advisory Locks When:

✅ You already have PostgreSQL
✅ Lock contention is low (<100 locks/sec)
✅ You need simplicity over performance
✅ Single database is acceptable (with standby)
✅ Session-based locking is acceptable

Use Redis/Redlock When:

✅ You need high throughput (1000+ locks/sec)
✅ You already have Redis infrastructure
✅ Lock TTL is critical (prevent stuck locks)
✅ You accept the complexity
✅ You've read Kleppmann's critique and accept risks

Use Neither - Use Fencing Tokens

For truly safe distributed locking:
1. Get lock + monotonic fencing token
2. Pass token to downstream systems
3. Downstream rejects if token < previously seen

This protects against all timing-related issues.

Monitoring

PostgreSQL Advisory Locks

-- View current locks
SELECT * FROM pg_locks WHERE locktype = 'advisory';

-- Count by session
SELECT pid, count(*)
FROM pg_locks
WHERE locktype = 'advisory'
GROUP BY pid;

Prometheus Metrics

# Lock acquisition time
histogram_quantile(0.99, rate(lock_acquisition_duration_seconds_bucket[5m]))

# Lock held duration
histogram_quantile(0.99, rate(lock_held_duration_seconds_bucket[5m]))

# Lock contention (waiters)
sum(lock_waiters_total)

Checklist

## Distributed Lock Selection

### Requirements Analysis
- [ ] Do you actually need distributed locks?
- [ ] Can you use database transactions instead?
- [ ] What is acceptable lock throughput?
- [ ] What happens if lock mechanism fails?

### PostgreSQL Advisory Locks
- [ ] Use session locks for long-running operations
- [ ] Use transaction locks for DB-bound operations
- [ ] Implement proper hash function for keys
- [ ] Add timeout for lock acquisition

### Redis/Redlock
- [ ] Deploy 5 independent Redis instances
- [ ] Use proper Redlock library
- [ ] Implement fencing tokens for safety
- [ ] Monitor clock skew between instances

### Monitoring
- [ ] Track lock acquisition latency
- [ ] Track lock hold duration
- [ ] Alert on lock contention
- [ ] Alert on lock timeouts

Conclusion

PostgreSQL advisory locks represent the kind of pragmatic solution that doesn’t get talked about at conferences. They’re not exciting. They don’t require new infrastructure. They don’t have fancy algorithms. But for most teams, they’re the right choice—simpler to operate, fewer moving parts, and actually safer than the alternatives.

The key insight is that operational complexity has real costs. Running five Redis instances for Redlock means monitoring five instances, managing failover, debugging split-brain scenarios, and training your team on Redis operations. If you already run PostgreSQL well, advisory locks add zero operational burden—they’re just another SQL query.

The safety story is also underappreciated. Redlock has known issues with clock skew and GC pauses. If one of your Redis instances has a clock that’s drifting, or if your application has a long garbage collection pause at the wrong moment, you can have two processes that both believe they hold the lock. PostgreSQL advisory locks don’t have these issues—the database is the single source of truth.

That said, there are legitimate reasons to use Redis: very high lock throughput (>1000/sec), or the need for lock TTL that’s decoupled from database connections. If you’re processing millions of events and need microsecond lock operations, PostgreSQL won’t keep up. Know your requirements.

Key principles:

  1. Simpler operations—one less service to manage, one less thing to break
  2. Good enough performance—850 ops/sec covers the vast majority of use cases
  3. Better safety—no clock dependency issues, no GC pause vulnerabilities
  4. Free—you already have PostgreSQL, advisory locks are built in
  5. Use Redis when you need it—genuine high throughput or TTL-based locks without database connections

Add Redis for locking only when you genuinely need capabilities PostgreSQL can’t provide. For most teams, that day never comes.


Related posts

Cite this article

If you reference this post, please link to the original URL and credit the author.

Michal Drozd. "Redlock vs PostgreSQL Advisory Locks: When You Don't Need Redis for Distributed Locking". https://www.michal-drozd.com/en/blog/redlock-vs-postgres-advisory-locks/ (Published July 13, 2025).