Redis를 이용한 분산 락(Distributed Lock) 구현: 3대 플랫폼 실전 가이드
여러 서버 노드로 구성된 분산 시스템 환경에서 동일 자원에 대칭되는 여러 트랜잭션이 충돌하지 않도록 보장하기 위해 사용하는 대표적인 기술이 Redis 분산 락(Distributed Lock)입니다.
1. 분산 락이 필요한 이유
단일 서버 JVM 메모리 락(예: `ReentrantLock` 혹은 `synchronized`)은 로컬 서버 프로세스 안에서만 동작합니다. 하지만 오토스케일링을 통해 다중 노드로 구성된 현대적인 클라우드 환경에서는 여러 인스턴스가 독립적으로 수행되므로, 단일 공유 저장소인 Redis를 락 브로커로 사용하여 전역 상호 배제(Mutual Exclusion)를 만족시켜야 합니다.
2. 분산 락의 핵심 조건
- 임대 시간 (TTL / Lease Time): 락을 획득한 노드가 장애로 비정상 종료되더라도 데드락이 발생하지 않도록 일정 시간 후 자동 만료되도록 설계해야 합니다.
- 해제 토큰 검증: A 노드가 획득한 락의 임대 시간이 종료되어 자동으로 풀린 상태에서 뒤늦게 A가 락 해제를 호출하여 다른 B 노드가 획득한 락을 강제 해제하지 않도록, 고유 토큰(UUID)을 발급하고 검증 후 해제(Lua 스크립트 활용)해야 합니다.
3. Go, NestJS, Spring Boot 구현 예제
Go (Golang) - SETNX & Lua Unlock
Go에서는 go-redis를 사용해 SETNX 커맨드를 구현하고, 언락 시 Lua 스크립트를 사용하여 원자성을 보장합니다.
package main
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type RedisLock struct {
client *redis.Client
}
const unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
func (l *RedisLock) AcquireAndRelease(ctx context.Context, lockKey string, ttl time.Duration) error {
token := uuid.New().String()
// 1. SETNX로 락 획득 시도 (Value로 고유 토큰 지정)
success, err := l.client.SetNX(ctx, lockKey, token, ttl).Result()
if err != nil {
return err
}
if !success {
return errors.New("failed to acquire lock")
}
defer func() {
// 2. Lua 스크립트로 안전하게 락 해제 (토큰이 일치할 때만 삭제)
l.client.Eval(ctx, unlockScript, []string{lockKey}, token)
}()
// 비즈니스 로직 수행
time.Sleep(100 * time.Millisecond)
return nil
}
Node.js (NestJS) - ioredis RedisLock
NestJS에서 ioredis 라이브러리를 사용하여 분산 락을 처리하는 서비스 컴포넌트 예시입니다.
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RedisLockService {
private redisClient = new Redis(); // 실제 설정에서 주입받아 사용
private unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
async executeWithLock(
lockKey: string,
ttlMs: number,
action: () => Promise,
): Promise {
const token = uuidv4();
// 1. SETNX & EX (NX: 존재하지 않을 때만, PX: 밀리초 단위 TTL 설정)
const acquired = await this.redisClient.set(lockKey, token, 'NX', 'PX', ttlMs);
if (acquired !== 'OK') {
throw new Error('Lock acquisition failed');
}
try {
return await action();
} finally {
// 2. 토큰을 확인하여 자신이 획득한 락인 경우에만 해제
await this.redisClient.eval(this.unlockScript, 1, lockKey, token);
}
}
}
Kotlin (Spring Boot) - Redisson RLock
Spring Boot 환경에서는 스핀 락 대신 레디스의 Pub/Sub 채널을 청취하여 리액티브하게 대기하는 Redisson 라이브러리를 표준으로 사용합니다.
@Service
class RedissonLockService(private val redissonClient: RedissonClient) {
fun executeWithLock(lockKey: String, leaseTimeSeconds: Long, action: () -> Unit) {
val lock: RLock = redissonClient.getLock(lockKey)
// tryLock(대기시간, 임대시간, 시간단위)
val acquired = lock.tryLock(5, leaseTimeSeconds, TimeUnit.SECONDS)
if (!acquired) {
throw RuntimeException("Could not acquire lock for key: $lockKey")
}
try {
action() // 비즈니스 로직 실행
} finally {
if (lock.isHeldByCurrentThread) {
lock.unlock() // 임대가 유효하고 현재 스레드가 쥐고 있는 경우에만 안전하게 언락
}
}
}
}
Distributed Locking with Redis: SETNX & Redisson in Go, NestJS, and Spring Boot
To prevent transaction collisions on shared resources across multiple server nodes in a distributed environment, Redis Distributed Locks are the industry standard.
1. Why Distributed Locks?
Single-server JVM memory locks (e.g., `ReentrantLock` or `synchronized`) only coordinate threads within that local instance. However, in modern cloud architectures with autoscale-enabled replicas, you must use a centralized storage broker like Redis to enforce global mutual exclusion.
2. Design Rules
- Lease Time (TTL): Always assign a TTL when acquiring locks, preventing deadlocks if a node crashes mid-transaction.
- Release Token Matching: Issue a unique token (like a UUID) per lock request. Only delete the lock key if the token matches, verifying with Lua scripts to prevent Node A from releasing Node B's active lock.
3. Go, NestJS, and Spring Boot Implementations
Go (Golang) - SETNX & Lua Unlock
In Go, leverage `go-redis` to send `SetNX` and clean up utilizing atomic Lua script evaluations.
// Refer to the KO tab code for Go implementation details.
// SetNX registers the unique UUID token, and Eval frees it safely.
Node.js (NestJS) - ioredis RedisLock
Configure a NestJS provider with `ioredis` utilizing `set(key, token, 'NX', 'PX', ttl)` and Lua script evaluations.
// Refer to the KO tab code for NestJS implementation details.
// Executing the action is guarded by try-finally blocks for absolute release.
Kotlin (Spring Boot) - Redisson RLock
Spring Boot JVM services leverage `Redisson` to subscribe to lock release signals over Redis Pub/Sub, avoiding wasteful spin-lock polling.
// Refer to the KO tab code for Kotlin/Spring Boot implementation details.
// tryLock awaits signals and isHeldByCurrentThread checks ownership before unlocking.