Lettuce를 이용해 Redis 락 구현과 테스트 과정 정리

말하는 감자·2025년 5월 20일

내일배움캠프

목록 보기
62/73

구현 방식

  • RedisTemplate 기반 분산 락
  • 락 획득은 SETNX + EXPIRE 조합 사용
  • 락 해제는 Lua 스크립트로 락 소유자 확인 후 삭제

🔐 RedisLockService 핵심 코드

lock

@Service
@RequiredArgsConstructor
@Slf4j
public class RedisLockService {
    private final RedisTemplate<String, String> redisTemplate;

    public String lock(String key) {
        final long waitMillis = 50; //락 시도 전까지 쉬는시간
        final int maxRetries = 150; //리트 횟수

        String lockValue = UUID.randomUUID().toString(); //쓰레드별로 다른 락 키 값을 줘야함 (다른 쓰레드 지워버릴수도잇음)

        for (int i = 0; i < maxRetries; i++) { //락 얻기 시도
            Boolean success = redisTemplate
                    .opsForValue()
                    .setIfAbsent(key, lockValue, Duration.ofMillis(1000));

            if (Boolean.TRUE.equals(success)) { //락 얻음
                return lockValue;
            }

            try { //waitMillis ms 초 쉬고 다시 리트
                Thread.sleep(waitMillis);
            } catch (InterruptedException e) { //Thread sleep 도중에 interrupted 발생
                Thread.currentThread().interrupt();
                break;
            }
        }
        return null; // 락 획득 실패
    }

코드 설명

String lockValue = UUID.randomUUID().toString();
-> 현재 스레드의 고유 락값 ( 해제할 때 다른 스레드의 락을 해제하면 안돼서 필요하다고 함)

for (int i = 0; i < maxRetries; i++) {
    Boolean success = redisTemplate
        .opsForValue()
        .setIfAbsent(key, lockValue, Duration.ofMillis(1000));

-> key가 비어 있으면 → lockValue를 1000ms 동안 설정
락 획득 성공 = 다른 사람이 아직 안 잡음

unlock

    public void unlock(String key, String lockValue) {
        String script = """
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            else
                return 0
            end
        """;

        Long result = redisTemplate.execute(
                RedisScript.of(script, Long.class),
                Collections.singletonList(key),
                lockValue
        );

        boolean success = result != null && result > 0;
        if (!success) { //삭제 실패
            log.warn("Unlock failed: key={}, lockValue={}", key, lockValue);
        }
    }
}

코드 설명


->Lua 스크립트로 Redis에 락 키의 소유자 확인 후 삭제
KEYS[1] = 락의 이름
ARGV[1] = 내가 가진 lockValue
→ 내가 설정한 락만 해제 가능하도록 보장 (다른 사람이 실수로 삭제 못함)
출처

Long result = redisTemplate.execute(
    RedisScript.of(script, Long.class),
    Collections.singletonList(key),
    lockValue
);

실행 결과가 1이면 → 삭제 성공
실행 결과가 0이면 → 락 해제 실패 (락이 내 것이 아니거나 없음)










⚠ 시행착오 및 트러블슈팅

❗ 1. 락 획득 실패가 너무 많이 발생

🧪 테스트 진행

공통 시나리오

  • 락 획득 성공한 스레드만 이벤트 자원 처리 가능
  • Redis 락 실패 시 "락 획득 실패"로 로그 출력

변환 값

주석에 적힌대로 대기시간, 리트라이 횟수, 락 얻도 점유할 수 있는 최대 시간 설정값

참고


락 얻는 로직에 각 스레드가 몇번째만에 락을 얻는지 확인용 sout 생성

  • 최대값에 맞게 조정할 예정!

레디스가 싱글스레드라서 처리할 요청이 많을수록 i값이 높아질 수 밖에 없음..





테스트 시나리오 (1)

  • 300개 스레드가 동시에 participateEvent() 호출

    final long waitMillis = 80; //락 시도 전까지 쉬는시간
    final int maxRetries = 200; //리트 횟수
    .setIfAbsent(key, lockValue, Duration.ofMillis(1000)); //락 점유 최대시간


같은 조건에서 여러번 실행시킬때마다 결과값이 다르게 나옴

여기서 각각 waitMillis, maxRetries, ofMillis 를 조절하면서 어떻게 해야 스레드 2000개 까지 잘 버틸지 수를 정해보자!!





생각해보면 락을 얻고 하는일이
1. 이벤트 불러오기
2. 당첨 레디스 읽고 계산하기 -> 당첨 성공/실패뽑기
3. 성공이면 남은 당첨자수 수정하고 저장하기

이렇게 최대 읽기두번, 쓰기 두번이 일어난다. + 락 생성/삭제도

읽기 1ms 정도, 쓰기는 AWS 쓰고 클러스터(X),분산DB(X) 가정하에 한건당 5ms 정도 나오는 것 같다.
혹시모를 충돌을 생각해서 총 요청 시간에서 X50를 해줘도 300ms정도라 충분할듯 ??

테스트 시나리오

시나리오waitMillismaxRetries락 TTL (ofMillis)고려사항
1️⃣5040300지금 값에서 TTL조정 + 그에따른 최대 시도값만 줄여봤음
2️⃣2050300대기 시간 확 줄이면 redis 부하가 좀 올까?
3️⃣5080500안전빵으로 다 성공시킬 수 있을까?
4️⃣101003002가 잘되면 해볼것 (이게 제일 빠를거같음)

각각 3번씩 돌려보겠다.







1️⃣지금 값에서 TTL조정 + 그에따른 최대 시도값만 줄여봤음

final long waitMillis = 50; //락 시도 전까지 쉬는시간
final int maxRetries = 40; //리트 횟수
.setIfAbsent(key, lockValue, Duration.ofMillis(300)); //락 점유 최대시간

300번

락 획득 성공 스레드 130
락 획득 실패 스레드 170
⏱ 걸린 시간: 2507ms

락 획득 성공 스레드 89
락 획득 실패 스레드 211
⏱ 걸린 시간: 2510ms

기존에서 5배가 빨라졌지만 실패 엄청해한다... maxRetries 값이 너무 적은 것 같다.

2000번

== 모든 스레드 작업 종료 ==
락 획득 성공 스레드 162
락 획득 실패 스레드 1838
⏱ 걸린 시간: 2817ms

택도없어서 넘어가겠다!!




2️⃣대기 시간 확 줄이면 redis 부하가 좀 올까?

final long waitMillis = 50; //락 시도 전까지 쉬는시간
final int maxRetries = 40; //리트 횟수
.setIfAbsent(key, lockValue, Duration.ofMillis(300)); //락 점유 최대시간

300번

락 획득 성공 스레드 84
락 획득 실패 스레드 216
⏱ 걸린 시간: 1620ms

락 획득 성공 스레드 104
락 획득 실패 스레드 196
⏱ 걸린 시간: 1598ms

락 획득 성공 스레드 97
락 획득 실패 스레드 203
⏱ 걸린 시간: 1614ms

아까보다 정확도가 떨어지는 모습이다.
아무래도 거의 동시에 스레드가 동작하고 다같이 쉬고 다같이 다음 요청하는거랑 비슷하다보니, 2000개 까지 버틸려면 maxRetries 값을 넉넉하게줘야할 것 같다.




3️⃣ 안전빵으로 다 성공시킬 수 있을까?

final long waitMillis = 50; //락 시도 전까지 쉬는시간
final int maxRetries = 80; //리트 횟수
.setIfAbsent(key, lockValue, Duration.ofMillis(500)); //락 점유 최대시간

락 획득 성공 스레드 166
락 획득 실패 스레드 134
⏱ 걸린 시간: 5015ms

락 획득 성공 스레드 139
락 획득 실패 스레드 161
⏱ 걸린 시간: 5007ms

락 획득 성공 스레드 180
락 획득 실패 스레드 120
⏱ 걸린 시간: 5001ms

정확도는 1,2번과 비교했을때보단 훨씬 나아졌긴했다.
그만큼 시간이 2, 3배가 소요되었다..




번외>>
final long waitMillis = 50; //락 시도 전까지 쉬는시간
final int maxRetries = 180; //리트 횟수
.setIfAbsent(key, lockValue, Duration.ofMillis(500)); //락 점유 최대시간

락 획득 성공 스레드 277
락 획득 실패 스레드 23
⏱ 걸린 시간: 11152ms

락 획득 성공 스레드 300
락 획득 실패 스레드 0
⏱ 걸린 시간: 7958ms

기다리는 시간이 다같이 이루어져서 그런가?? 왔다갔다가 좀 많이 심한 것 같다..




❓ 랜덤 waitMillis가 효과 있을까?

현재 방식의 문제점:
모든 스레드가 거의 동시에 락 시도 → 실패 → 일정 간격으로 재시도
→ 시도 타이밍이 겹침 → Redis에 락 요청 몰림 → 충돌율↑

waitMillis 값에 +- 20ms씩 스레드마다 랜덤하게 잡아서 실행시키면 충돌이 덜 일어날 것이라고 예상한다.

        long baseWait = 50;
        long jitter = ThreadLocalRandom.current().nextLong(-20, 21); // -20 ~ +20

        final long waitMillis = baseWait + jitter; //락 시도 전까지 쉬는시간

1번 다시 돌려보겠다.

락 획득 성공 스레드 300
락 획득 실패 스레드 0
⏱ 걸린 시간: 948ms

락 획득 성공 스레드 300
락 획득 실패 스레드 0
⏱ 걸린 시간: 915ms

대박ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 찾았다 찾았어~~~ 기다리는시간을 각 스레드마다 랜덤하게 차이를 주니 갑자기 엄청 성능이 좋아졌다.

1번으로 2000번 돌려보겠다!!
<전>

<후>

락 획득 성공 스레드 1252
락 획득 실패 스레드 748
⏱ 걸린 시간: 3040ms

확실히 정확도는 눈에 띄게 올라갔다.
대기시간은 -20되었을 때 50으로 맞추고
maxRetries 수를 좀 늘려보겠다!

✔️ 효과 완전 좋다





final long waitMillis = 50~90; //락 시도 전까지 쉬는시간
final int maxRetries = 100; //리트 횟수
.setIfAbsent(key, lockValue, Duration.ofMillis(300)); //락 점유 최대시간

락 획득 성공 스레드 2000
락 획득 실패 스레드 0
⏱ 걸린 시간: 3924ms

락 획득 성공 스레드 2000
락 획득 실패 스레드 0
⏱ 걸린 시간: 3791ms

최종으로 이렇게 선정!! 땅땅땅
다른 방법을 찾아내면 다시오도록하겠따

profile
대충 데굴데굴 굴러가는 개발?자

3개의 댓글

comment-user-thumbnail
2025년 5월 20일

저도 이번에 해야하는데 너무 잘 배워가요!

1개의 답글