Redlock vs PostgreSQL Advisory Locks: Kedy Nepotrebujete Redis na Distributed Locking
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ť
| Aspekt | PostgreSQL Advisory | Redis Redlock |
|---|---|---|
| Infraštruktúra | Už máš DB | Potrebuješ 5 Redis inštancií |
| Failure módy | Single point (DB) | Komplexné (majority) |
| Závislosť na hodinách | Nie | Áno (problematické!) |
| Network partitions | Jednoduché | Komplexné |
| Monitoring | Štandardné DB nástroje | Dodatočný tooling |
Výkon (1000 lock/unlock cyklov)
| Metrika | PostgreSQL | Redis (single) | Redlock (5) |
|---|---|---|---|
| Acquire p50 | 0.8ms | 0.3ms | 2.5ms |
| Acquire p99 | 3.2ms | 1.1ms | 8.4ms |
| Release p50 | 0.5ms | 0.2ms | 1.8ms |
| Ops/sec | 850 | 2,400 | 280 |
Bezpečnostné Garancie
| Vlastnosť | PostgreSQL | Redis (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é:
- Jednoduchšie operácie - O jednu službu menej na správu
- Dostatočný výkon - 850 ops/sec stačí pre väčšinu prípadov
- Lepšia bezpečnosť - Žiadne problémy so závislosťou na hodinách
- Zadarmo - Už máš PostgreSQL
Pridaj Redis pre locking len keď naozaj potrebuješ >1000 locks/sec.
Súvisiace články
- Connection Pool Sizing s Little’s Law - Database connections
- PostgreSQL HOT Updates - PostgreSQL internals
Súvisiace články
Keď Prepared Statements Spravia PostgreSQL 10× Pomalším: Generic Plan Trap
Rovnaký query, rovnaké parametre, ale prod je pomalý a staging funguje. Ukážem ako reprodukovať generic plan problém s pgBouncer, Java/Go a ako ho fixnúť.
HTTP Keep-Alive Connection Reset: Prečo Vaše Requesty Zlyhávajú s 'Connection Reset by Peer'
Sporadické 'connection reset by peer' chyby v produkcii. Ukážem ako nesúlad keep-alive timeoutov medzi klientom a serverom toto spôsobuje a ako to opraviť.
Adaptive Concurrency Limits: Prestaňte Hádať Veľkosti Thread Poolov
Thread pool 200 lebo to hovorí Stack Overflow? Netflix algoritmus upravuje konkurenciu automaticky podľa latencie. Ukážem ako funguje s benchmarkmi.
Kubernetes CPU Throttling Pitva: Prečo p99 Latencia Exploduje pri 40% CPU Usage
CPU vyzerá OK, ale tail latencia je katastrofálna. Ukážem ako korelovať CFS throttling s latency spikes a prečo odstránenie CPU limitov môže paradoxne pomôcť.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.