
SETNX + EXPIRE 조합 사용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. 락 획득 실패가 너무 많이 발생
공통 시나리오

변환 값

주석에 적힌대로 대기시간, 리트라이 횟수, 락 얻도 점유할 수 있는 최대 시간 설정값
참고
락 얻는 로직에 각 스레드가 몇번째만에 락을 얻는지 확인용 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정도라 충분할듯 ??
| 시나리오 | waitMillis | maxRetries | 락 TTL (ofMillis) | 고려사항 |
|---|---|---|---|---|
| 1️⃣ | 50 | 40 | 300 | 지금 값에서 TTL조정 + 그에따른 최대 시도값만 줄여봤음 |
| 2️⃣ | 20 | 50 | 300 | 대기 시간 확 줄이면 redis 부하가 좀 올까? |
| 3️⃣ | 50 | 80 | 500 | 안전빵으로 다 성공시킬 수 있을까? |
| 4️⃣ | 10 | 100 | 300 | 2가 잘되면 해볼것 (이게 제일 빠를거같음) |
각각 3번씩 돌려보겠다.
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
택도없어서 넘어가겠다!!
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 값을 넉넉하게줘야할 것 같다.
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
기다리는 시간이 다같이 이루어져서 그런가?? 왔다갔다가 좀 많이 심한 것 같다..
현재 방식의 문제점:
모든 스레드가 거의 동시에 락 시도 → 실패 → 일정 간격으로 재시도
→ 시도 타이밍이 겹침 → 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
최종으로 이렇게 선정!! 땅땅땅
다른 방법을 찾아내면 다시오도록하겠따
저도 이번에 해야하는데 너무 잘 배워가요!