0Pricing
Node.js Backend Development Bootcamp · 강의

분산 잠금 및 Redlock 알고리즘

인스턴스 간 배타적 접근을 안전하게 조정하고 분산 잠금의 한계를 이해합니다.

분산 잠금 및 Redlock 알고리즘은(는) CoddyKit의 무료 Node.js Backend Development Bootcamp 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Node.js Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

Why Distributed Locks?

When your Node.js API runs as a single process, a simple in-memory mutex is enough to serialize access to a critical section. But production backends run many instances behind a load balancer, often across multiple machines.

  • Two pods may both try to charge the same invoice.
  • Two workers may both pick up the same job from a queue.
  • Two requests may both regenerate the same expensive cache entry (a cache stampede).

An in-memory lock lives inside one process only — the other instances know nothing about it. To coordinate exclusive access across instances you need a lock that lives in shared external storage, and Redis is a popular home for it.

A First (Naive) Redis Lock

The core primitive is the atomic SET key value NX PX ttl command. NX means "only set if the key does not exist", and PX sets an expiry in milliseconds so the lock auto-releases if the holder crashes.

  • If SET returns OK, you acquired the lock.
  • If it returns null, someone else holds it.

The value must be a unique random token per acquisition — you will need it later to release safely.

const { createClient } = require('redis');
const crypto = require('crypto');

async function acquire(redis, key, ttlMs) {
  const token = crypto.randomUUID();
  // NX = set only if absent, PX = expiry in ms
  const ok = await redis.set(key, token, { NX: true, PX: ttlMs });
  return ok === 'OK' ? token : null;
}

// Usage sketch (needs a running Redis):
// const redis = createClient(); await redis.connect();
// const token = await acquire(redis, 'lock:invoice:42', 10000);
// if (token) { /* do exclusive work */ }

Releasing Safely: Check-and-Delete

Releasing is the dangerous part. A naive DEL key can delete someone else's lock: if your work overran the TTL, the lock expired, another instance acquired it, and then your late DEL wipes their lock.

The fix: only delete if the stored value still equals your token. Check-then-delete must be atomic, so we run it as a Lua script — Redis executes scripts without interleaving other commands.

const RELEASE_LUA = `
if redis.call('get', KEYS[1]) == ARGV[1] then
  return redis.call('del', KEYS[1])
else
  return 0
end`;

async function release(redis, key, token) {
  // Returns 1 if we owned and removed it, 0 otherwise
  return redis.eval(RELEASE_LUA, { keys: [key], arguments: [token] });
}

TTL: The Hardest Tuning Decision

The TTL is a bet on how long your critical section takes.

  • Too short and the lock expires mid-work, allowing a second worker in — your mutual exclusion is broken.
  • Too long and if a holder crashes, everyone waits the full TTL before anyone can proceed.

Rules of thumb: set the TTL to a few times your p99 critical-section duration, keep the protected work short, and for long tasks use a watchdog that periodically extends the lock instead of a single huge TTL.

Extending a Lock (Watchdog Pattern)

For work whose duration is uncertain, acquire a modest TTL and renew it on a timer while you still hold the lock. Like release, extension must be guarded by your token so you never extend a lock that has already moved on.

The watchdog runs at roughly one third of the TTL, giving you margin against clock jitter and GC pauses.

const EXTEND_LUA = `
if redis.call('get', KEYS[1]) == ARGV[1] then
  return redis.call('pexpire', KEYS[1], ARGV[2])
else
  return 0
end`;

function startWatchdog(redis, key, token, ttlMs) {
  const timer = setInterval(async () => {
    const ok = await redis.eval(EXTEND_LUA, {
      keys: [key], arguments: [token, String(ttlMs)],
    });
    if (ok !== 1) clearInterval(timer); // lost the lock; stop renewing
  }, Math.floor(ttlMs / 3));
  return () => clearInterval(timer);
}

Single-Node Redis Is a SPOF

Everything so far assumes one Redis node. That node is a single point of failure, so teams add a replica with failover. But Redis replication is asynchronous, and that quietly breaks mutual exclusion:

  • Client A acquires the lock on the master.
  • The master crashes before replicating the write to the replica.
  • The replica is promoted; it has no record of the lock.
  • Client B acquires "the same" lock on the new master.

Now two clients hold the lock simultaneously. The Redlock algorithm was designed to address this failover window.

The Redlock Algorithm

Redlock uses N independent Redis masters (typically 5), with no replication between them. To acquire a lock a client:

  • Records the start time, then tries to SET NX PX the same key+token on all N nodes, using a short per-node timeout.
  • Counts successes. The lock is considered acquired only if it got a quorum (N/2 + 1, i.e. 3 of 5) and the total elapsed time is less than the TTL.
  • The effective validity = TTL minus elapsed time minus a clock-drift allowance.

If it fails to reach quorum (or runs out of time), it unlocks all nodes and retries after a small random delay.

Using the redlock Library

You rarely implement Redlock by hand. The redlock npm package takes an array of independent Redis clients and exposes using(), which acquires, auto-extends, and releases the lock around your callback.

  • retryCount / retryDelay control how hard it tries before giving up.
  • using() hands you a signal — check signal.aborted to detect that the lock was lost mid-work.
const Client = require('ioredis');
const Redlock = require('redlock').default;

const nodes = [
  new Client({ host: 'redis-a' }),
  new Client({ host: 'redis-b' }),
  new Client({ host: 'redis-c' }),
];

const redlock = new Redlock(nodes, {
  retryCount: 10,
  retryDelay: 200,   // ms between attempts
  driftFactor: 0.01, // clock-drift allowance
});

async function chargeInvoice(id) {
  await redlock.using([`lock:invoice:${id}`], 5000, async (signal) => {
    await doCharge(id);
    if (signal.aborted) throw signal.error; // lost the lock
  });
}

Fencing Tokens: The Real Safety Net

Martin Kleppmann's well-known critique: no lock based on timeouts can guarantee safety if a holder is paused (GC, VM stall) past the TTL. The lock expires, another client proceeds, and the paused client wakes up still believing it holds the lock.

The robust defense is a fencing token: a monotonically increasing number issued with each lock grant. The protected resource itself rejects any write carrying a token lower than the highest it has already seen — so a stale, paused writer is fenced out at the destination.

// Resource-side guard: reject writes with a stale fencing token.
function makeFencedStore() {
  let highestSeen = 0;
  const data = {};
  return {
    write(key, value, token) {
      if (token <= highestSeen) {
        throw new Error(`fenced: token ${token} <= ${highestSeen}`);
      }
      highestSeen = token;
      data[key] = value;
      return token;
    },
  };
}

const store = makeFencedStore();
store.write('balance', 100, 33);     // ok, token 33
try {
  store.write('balance', 999, 32);   // stale writer, fenced out
} catch (e) {
  console.log(e.message);            // fenced: token 32 <= 33
}
console.log('stored:', store.write('balance', 200, 34)); // 34

Locks vs. Idempotency

A lock reduces the chance of concurrent execution, but timeouts and failovers mean you can never make it a hard guarantee. Treat the lock as an optimization, not the last line of defense.

  • Make the protected operation idempotent — running it twice produces the same result.
  • Use database unique constraints or conditional updates (compare-and-set) so a duplicate write fails loudly.
  • Use fencing tokens where the resource supports them.

Best practice: lock to avoid wasted work and contention, but design the system so a rare double-execution is still correct.

Do You Even Need Redlock?

Redlock adds operational cost: five independent Redis deployments, careful clock management, and retry tuning. The redis maintainers themselves note that for efficiency use cases (avoid doing the same work twice), a single-node lock is fine — an occasional double-run just wastes a little work.

  • Efficiency lock (cache rebuild, dedup): single Redis SET NX PX is enough.
  • Correctness lock (money, inventory): don't rely on any timeout lock alone — add idempotency and fencing, regardless of Redlock.

Reach for Redlock only when single-node HA failover is genuinely unacceptable and you cannot fence at the resource.

Quick Check

Test your understanding of distributed locking safety.

Recap

You learned how to coordinate exclusive access across Node.js instances — and where the limits are.

  • Acquire with atomic SET key token NX PX ttl; the token must be unique per acquisition.
  • Release and extend only via a token-checked Lua script so you never touch someone else's lock; renew long tasks with a watchdog.
  • TTL is a tradeoff: too short breaks exclusion, too long delays recovery after a crash.
  • Redlock uses a quorum across N independent masters to survive single-node failover, but it is still timeout-based.
  • No timeout lock is safe against long pauses — add fencing tokens and idempotency for correctness-critical work.
  • Use a single-node lock for efficiency; reserve Redlock for genuine HA needs.

자주 묻는 질문

“분산 잠금 및 Redlock 알고리즘” 강의는 무료인가요?

네 — “분산 잠금 및 Redlock 알고리즘” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Node.js Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. Node.js Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.

“분산 잠금 및 Redlock 알고리즘”에서 뭘 배우나요?

인스턴스 간 배타적 접근을 안전하게 조정하고 분산 잠금의 한계를 이해합니다. 브라우저에서 직접 실행하는 실습 코드로 Node.js Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Node.js Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Node.js Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“분산 잠금 및 Redlock 알고리즘” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Node.js Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Node.js Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 캐시 별도, 쓰기 관통 및 TTL 전략
  2. 분산 잠금 및 Redlock 알고리즘
  3. Redis를 활용한 Pub/Sub, 스트림 및 호출률 제한
  4. 캐시 스탬피드 및 우르르 몰림 방지
← Node.js Backend Development Bootcamp(으)로 돌아가기