Redis 기반 분산 락의 한계와 해결 방안
- 재진입 불가 문제: 동일 스레드가 같은 락을 두 번 이상 획득할 수 없음. 예를 들어 메서드 A에서 메서드 B를 호출하고, 둘 다 락을 필요로 한다면, 첫 번째 호출에서 락을 획득한 후 두 번째 호출 시 락을 재획득하지 못해 데드락 발생 가능.
- 재시도 기능 부족: 락 획득 시도는 단 한 번만 수행되고 실패 시 즉시 반환되며, 재시도 메커니즘이 없어 데이터 손실 위험이 있음. 예컨대 스레드 1이 락을 확보하고 데이터베이스에 기록하려는 도중 다른 스레드가 락을 차지하면, 스레드 1은 실패 후 종료되어 작업이 사라짐.
- 타임아웃 자동 해제의 리스크: 타임아웃 설정은 데드락을 줄이는 데 도움이 되지만, 처리 시간이 길어질 경우 락이 조기에 해제되어 보안 취약점이 생김. 너무 짧으면 비정상 종료 시 락이 해제되며, 너무 길면 데드락 발생 가능성 증가. 이는 균형 잡힌 해결책이 필요함. 해결법으로는 짧은 타임아웃과 함께 헬스체크 및 자동 연장 기술을 적용할 수 있음.
- 마스터-슬레이브 일관성 문제: Redis 클러스터 환경에서는 마스터와 슬레이브 간 복제 지연이 발생할 수 있어, 일부 노드에서 락 상태가 일치하지 않을 수 있음.
해결책: Redisson 프레임워크 활용
Redisson는 고도로 발전된 Redis 클라이언트 라이브러리로, 분산 락, 동시성 제어, 분산 컬렉션, 분산 객체 등 다양한 분산 시나리오에 대한 완전한 솔루션 제공.
핵심 로직 구현
- 요청 제한: Guava RateLimiter 사용
매 초 최대 10건의 요청만 허용하도록 설정. 1초 내에 토큰을 획득하지 못하면 실패 처리.
private final RateLimiter rateLimiter = RateLimiter.create(10);
if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
return Result.fail("현재 네트워크가 혼잡합니다. 다시 시도해 주세요");
}
- 사용자 식별: ThreadLocal 기반 세션 관리
현재 사용자의 식별자 추출.
Long userId = UserHolder.getUser().getId();
- Lua 스크립트 실행
Redis에 저장된 `seckill.lua` 스크립트를 원자적으로 실행하여 동시성 문제를 방지.
- 스크립트 인자: 쿠폰 ID, 사용자 ID
- 인자 전달: 빈 리스트로 매개변수 대체
- 실행 명령:
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(),
userId.toString()
);
- Lua 스크립트 내용 (seckill.lua)
-- 입력 파라미터
local voucherId = ARGV[1] -- 쿠폰 ID
local userId = ARGV[2] -- 사용자 ID
-- Redis 키 정의
local stockKey = 'seckill:stock:' .. voucherId -- 재고 키
local orderKey = 'seckill:order:' .. voucherId -- 주문 기록 키
-- 재고 확인
if tonumber(redis.call('get', stockKey)) <= 0 then
return 1 -- 재고 부족
end
-- 중복 주문 여부 확인
if redis.call('sismember', orderKey, userId) == 1 then
return 2 -- 이미 주문한 사용자
end
-- 재고 감소
redis.call('incrby', stockKey, -1)
-- 사용자 추가 (주문 기록)
redis.call('sadd', orderKey, userId)
return 0 -- 성공
- 주문 생성 및 큐 전송
Lua 스크립트 결과가 0(성공)일 경우, 주문 정보 생성 후 RabbitMQ로 비동기 전송.
long orderId = redisIdWorker.nextId("order");
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
return Result.ok(orderId);
종합 요약
Redis 분산 락의 핵심 원칙:
- `SET key value NX EX` 명령을 통해 락 획득 및 만료 시간 설정
- 락 해제 시, 소유자 식별자 검증 후 삭제
특징:
- `NX` 옵션으로 상호 배타성 보장
- `EX` 옵션으로 장애 시 락 자동 해제, 데드락 방지
- 클러스터링 지원으로 고가용성 및 고성능 달성