Späť na blog

Redlock vs PostgreSQL Advisory Locks: Kedy Nepotrebujete Redis na Distributed Locking

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

Po jednom neprijemnom vypadku sme riesili Redlock vs Postgres advisory locks. “Potrebujeme Redis pre distributed locks.” Prečo? “Lebo tak sa robí distributed locking.”

Ak už máte PostgreSQL, možno Redis nepotrebujete. PostgreSQL advisory locks poskytujú distributed locking s menšou operačnou zložitosťou.

Testované na: PostgreSQL 16, Redis 7, Java 21, Go 1.22

Kedy Potrebujete Distributed Locks?

Use Cases

1. Prevencia duplicitného spracovania
   - Spracovanie platby (účtuj raz)
   - Plánovanie úloh (spusti raz)
   - Migrácia dát (spracuj každý riadok raz)

2. Koordinácia prístupu k externým zdrojom
   - Rate-limited API (jeden volajúci naraz)
   - Zdieľané súbory (exkluzívny zápis)
   - Hardvérové zariadenia (single access)

3. Leader election
   - Jedna aktívna inštancia
   - Plánované úlohy (jeden executor)

Kedy NEPOUŽÍVAŤ Distributed Locks

❌ Database row-level locking
   → Použi SELECT FOR UPDATE alebo transakcie

❌ Prevencia race conditions v DB
   → Použi database constraints, transakcie

❌ Cache stampede prevention
   → Použi probabilistic early expiration

❌ Rate limiting
   → Použi token bucket alebo sliding window

PostgreSQL Advisory Locks

Session-Level Locks

-- Získaj lock (blokuje ak drží iná session)
SELECT pg_advisory_lock(12345);

-- Pokús sa získať (vracia okamžite)
SELECT pg_try_advisory_lock(12345);  -- Vracia TRUE alebo FALSE

-- Uvoľni lock
SELECT pg_advisory_unlock(12345);

-- Lock je automaticky uvoľnený pri ukončení session

Transaction-Level Locks

-- Automaticky uvoľnený na konci transakcie
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- ... urob prácu ...
COMMIT;  -- Lock uvoľnený automaticky

Lock Key Stratégie

-- Jeden integer kľúč
SELECT pg_advisory_lock(hashtext('payment:order_123'));

-- Dva integer kľúče (pre namespacing)
SELECT pg_advisory_lock(1, 123);  -- (namespace, id)

-- Entity-based locking
SELECT pg_advisory_lock(
    hashtext('orders'),           -- tabuľka
    hashtext('order_123')         -- id
);

Redis Redlock

Základný Redis Lock

# Jedna Redis inštancia (NIE JE bezpečné pre distributed systémy)
SET lock:payment:123 owner_id NX EX 30

# NX = Len ak neexistuje
# EX 30 = Expiruj po 30 sekundách

Redlock Algoritmus (5 Redis inštancií)

1. Získaj aktuálny čas v milisekundách
2. Pokús sa získať lock na všetkých N (5) Redis inštanciách
3. Lock získaný ak:
   - Väčšina (N/2+1 = 3) inštancií zamknutá
   - Celkový čas < platnosť locku
4. Efektívny čas locku = platnosť - čas získania
5. Ak zlyhalo, odomkni všetky inštancie

Porovnanie

Operačná Zložitosť

AspektPostgreSQL AdvisoryRedis Redlock
InfraštruktúraUž máš DBPotrebuješ 5 Redis inštancií
Failure módySingle point (DB)Komplexné (majority)
Závislosť na hodináchNieÁno (problematické!)
Network partitionsJednoduchéKomplexné
MonitoringŠtandardné DB nástrojeDodatočný tooling

Výkon (1000 lock/unlock cyklov)

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

Bezpečnostné Garancie

VlastnosťPostgreSQLRedis (single)Redlock
Mutual exclusion❌*⚠️**
Deadlock-free✅ (timeout)✅ (TTL)✅ (TTL)
Fault-tolerant⚠️***
* Redis single: Lock môže držať dva procesy po failoveri
** Redlock: Martin Kleppmannova kritika ukazuje edge cases
*** Vyžaduje väčšinu dostupnú, problémy s clock sync

Implementácia

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);

        // Pokús sa získať s timeoutom
        String sql = "SELECT pg_try_advisory_lock(?)";
        Boolean acquired = jdbcTemplate.queryForObject(sql, Boolean.class, lockId);

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

        // Retry s timeoutom
        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;  // Pozitívne celé číslo
    }
}

// Použitie
@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 {
            // Spracuj platbu
            doPayment(orderId);
        } finally {
            lockRepo.unlock(lockKey);
        }
    }
}

Java - Redlock s 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 {
            // Pokús sa získať lock na 30 sekúnd, drž 10 sekúnd
            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 Scenáre

PostgreSQL Advisory Lock Failure

Scenár: Database spojenie zomrie počas držania locku

1. Aplikácia získa lock
2. Aplikácia crashne (alebo problém s connection pool)
3. PostgreSQL detekuje odpojenie
4. Lock automaticky uvoľnený
5. Iný proces môže získať lock

Výsledok: Bezpečné, automatické upratanie

Redlock Failure Scenáre

Scenár 1: Clock skew

1. Proces A získa lock (TTL 10s)
2. Hodiny na Redis inštancii 3 sú 5s vpredu
3. Lock na inštancii 3 expiruje skoro
4. Proces B získa väčšinu vrátane inštancie 3
5. Obaja A aj B si myslia že majú lock!

Scenár 2: Network partition

1. Proces A získa lock na 3/5 inštanciách
2. Network partition izoluje 2 inštancie
3. A-čkove locky expirujú na izolovaných inštanciách
4. Keď sa partition vyrieši, stav je nekonzistentný

Scenár 3: GC pauza

1. Proces A získa lock (TTL 10s)
2. Proces A má 8 sekundovú GC pauzu
3. Lock expiruje počas GC
4. Proces B získa lock
5. A-čkov GC skončí, stále si myslí že má lock

Rozhodovacia Matica

Použi PostgreSQL Advisory Locks Keď:

✅ Už máš PostgreSQL
✅ Lock contention je nízka (<100 locks/sec)
✅ Potrebuješ jednoduchosť nad výkon
✅ Single database je akceptovateľná (so standby)
✅ Session-based locking je akceptovateľný

Použi Redis/Redlock Keď:

✅ Potrebuješ vysoký throughput (1000+ locks/sec)
✅ Už máš Redis infraštruktúru
✅ Lock TTL je kritický (prevent stuck locks)
✅ Akceptuješ zložitosť
✅ Prečítal si Kleppmannovu kritiku a akceptuješ riziká

Nepouži Ani Jedno - Použi Fencing Tokens

Pre naozaj bezpečný distributed locking:
1. Získaj lock + monotónny fencing token
2. Pošli token downstream systémom
3. Downstream odmietne ak token < predtým videný

Toto chráni proti všetkým timing-related problémom.

Monitoring

PostgreSQL Advisory Locks

-- Zobraz aktuálne locky
SELECT * FROM pg_locks WHERE locktype = 'advisory';

-- Počet podľa session
SELECT pid, count(*)
FROM pg_locks
WHERE locktype = 'advisory'
GROUP BY pid;

Prometheus Metriky

# Čas získania locku
histogram_quantile(0.99, rate(lock_acquisition_duration_seconds_bucket[5m]))

# Čas držania locku
histogram_quantile(0.99, rate(lock_held_duration_seconds_bucket[5m]))

# Lock contention (čakajúci)
sum(lock_waiters_total)

Checklist

## Výber Distributed Lock

### Analýza Požiadaviek
- [ ] Naozaj potrebuješ distributed locks?
- [ ] Môžeš použiť database transakcie namiesto?
- [ ] Aký je akceptovateľný lock throughput?
- [ ] Čo sa stane ak lock mechanizmus zlyhá?

### PostgreSQL Advisory Locks
- [ ] Použi session locks pre dlhobežiace operácie
- [ ] Použi transaction locks pre DB-bound operácie
- [ ] Implementuj správnu hash funkciu pre kľúče
- [ ] Pridaj timeout pre získanie locku

### Redis/Redlock
- [ ] Deploy 5 nezávislých Redis inštancií
- [ ] Použi správnu Redlock knižnicu
- [ ] Implementuj fencing tokens pre bezpečnosť
- [ ] Monitoruj clock skew medzi inštanciami

### Monitoring
- [ ] Sleduj latenciu získania locku
- [ ] Sleduj čas držania locku
- [ ] Alert na lock contention
- [ ] Alert na lock timeouty

Záver

PostgreSQL advisory locks sú podceňované:

  1. Jednoduchšie operácie - O jednu službu menej na správu
  2. Dostatočný výkon - 850 ops/sec stačí pre väčšinu prípadov
  3. Lepšia bezpečnosť - Žiadne problémy so závislosťou na hodinách
  4. Zadarmo - Už máš PostgreSQL

Pridaj Redis pre locking len keď naozaj potrebuješ >1000 locks/sec.


Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Redlock vs PostgreSQL Advisory Locks: Kedy Nepotrebujete Redis na Distributed Locking". https://www.michal-drozd.com/sk/blog/redlock-vs-postgres-advisory-locks/ (Publikované 13. júla 2025).