https://pius712.tistory.com/25
https://pius712.tistory.com/26
위의 포스팅에서, database, redis setnx 를 통해 동시성 처리 예제를 만들어보았다.
이번의 포스팅에서는 redis 의 라이브러리 redisson 을 활용한 동시성 제어를 다루어보려고한다.
저번 예제와 마찬가지로, redis 자체에 대해서는 깊게 다루지 않을 것이다. (깊게 알지도 못함)
참고로, 컬리 블로그에서 https://helloworld.kurly.com/blog/distributed-redisson-lock/ AOP 를 활용하여 구현한 것도 있다.
Why Redisson ?
redisson 은 redis client 라이브러리인데, redis 공식문서에서 red lock 에 설명되어있는 라이브러리이다.
lettuce 의 경우 pub/sub 을 따로 지원하지 않고, lock 과 관련된 api 를 제공하지 않는 반면
redisson 은 pub/sub 과 lock 관련 api 를 제공하고 있다.
redisson 의 RLock 인터페이스 의 경우, 기본적으로 java 의 lock 인터페이스를 구현하고 있다.
java lock 인터페이스를 얘기하자면, ReentrantLock 과 같은 구현체에 대해서 이야기 해야하는데 주제를 벗어난거 같아서 이번 포스팅에서는 생략하고자한다.
public interface RLock extends Lock, RLockAsync { }
redisson 을 사용하는 이유에 관해서는 하이퍼커넥트의 문서에 잘 나와있다.
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
Lock 인터페이스 살펴보기
tryLock
// RLock 인터페이스
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException;
// 아래는 java lock 인터페이스
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
boolean tryLock();
- waitTime: 락 획득을 위한 대기 시간
- leaseTime: 락 획득 후 유지할 시간 (해당 시간 후 락 해제됨)
// RLock 인터페이스
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException;
- time: 락 획득을 위한 대기 시간
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
lock
tryLock 의 경우, 일정 시간 이상 대기하며 락 획득을 기다리지만, lock 메서드의 경우 무한히 블록된 상태로 lock 을 대기하게 된다.
// RLock 인터페이스
void lock(long leaseTime, TimeUnit unit);
// java lock 인터페이스
void lock();
구현
- 먹이 주는 로직
@Component
class FeedingExecutorV5(
private val foodRepositoryV5: FoodRepositoryV5,
private val petRepositoryV5: PetRepositoryV5,
) {
@Transactional
fun feed(petId: Long) {
val foodEntity = foodRepositoryV5.findByPetId(petId) ?: throw RuntimeException("Food not found")
if (foodEntity.count <= 0) return;
val petEntity = petRepositoryV5.findByIdOrNull(petId) ?: throw RuntimeException("Pet not found")
foodEntity.count -= 1
petEntity.power += 1;
}
}
- 락 획득, 락 해제 로직
@Service
class FeedingServiceV5(
private val feedingExecutorV5: FeedingExecutorV5,
private val redissonClient: RedissonClient,
) {
val logger = LoggerFactory.getLogger(javaClass)
fun feed(
petId: Long,
) {
val lock = redissonClient.getLock(petId.toString())
try {
val isLocked = lock.tryLock(10, 2, TimeUnit.SECONDS)
if (isLocked) {
feedingExecutorV5.feed(petId)
} else {
logger.warn("Failed to acquire lock")
}
} finally {
lock.unlock()
}
}
}
우선, redis 를 호출하는 부분과 db 의 트랜잭션은 분리하자.
만약, redis 쪽이 잘못되어 오랜기간 응답을 주지 않는 경우 transaction이 길어질 수 있기 때문이다.
lock 을 사용하는 경우, redis 에서 문제가 발생시 스레드가 블락되기 때문에 tryLock 을 통해서 대기시간을 정해두었다. 예제에서는 10초 시간을 주었지만, 실무에서는 이 수치를 잘 조정해야한다.
실행됨을 보장하기 위해서는 lock 메서드를 사용한다면 실행을 보장할 수 있다.
간단 삽질기
락을 획득, 해제하는 곳에 transactional 코드를 달면 이상하게 실패한다.
왜 그런지 살펴보려고하는데, 로그를 봐도 잘 안남아서, 애를 먹었다.
@Service
class FeedingServiceV5(
private val feedingExecutorV5: FeedingExecutorV5,
private val redissonClient: RedissonClient,
) {
val logger = LoggerFactory.getLogger(javaClass)
@Transactional
fun feed(
petId: Long,
) {
val lock = redissonClient.getLock(petId.toString())
try {
val isLocked = lock.tryLock(10, 2, TimeUnit.SECONDS)
if (!isLocked) {
logger.warn("Failed to acquire lock")
}
feedingExecutorV5.feed(petId)
} finally {
lock.unlock()
}
}
}
가설 1. lock 이 없는데 lock 을 해제하려고 해서 그런것
기본적으로 lock 이 없는 상태에서 lock 해제하려고 하는 경우, java lock 의 경우 예외를 던졌던 것 같다.
아래와 같이 코드를 수정하고, transactional 여부에 따라 테스트 결과를 확인해보았다.
여전히 실패한다.
fun feed(
petId: Long,
) {
val lock = redissonClient.getLock(petId.toString())
val isLocked = lock.tryLock(10, 3, TimeUnit.SECONDS)
if (isLocked) {
feedingExecutorV5.feed(petId)
lock.unlock()
} else {
logger.warn("Failed to acquire lock")
}
}
참고로 java lock 에는 구현체가 에러를 던지는 경우 문서화하라고 되어있는데, RLock 은 따로 문서화하지는 않았음.
가설 2. 순서 이슈? → 정답
위 로직은 아래와 같은 방식으로 진행된다.
그런데, 작업을 완료한 스레드가 커밋을 하기 전에
다른 스레드가 값을 변경한다면 ? → lock 을 했음에도, 작업의 내용이 반영되기 전에 다른 스레드가 이전의 값을 기반으로 계산을 할 수도 있다.
참고자료
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
https://helloworld.kurly.com/blog/distributed-redisson-lock/
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
'동시성 처리' 카테고리의 다른 글
분산환경 동시성 처리 2 - redis SETNX 사용 (0) | 2024.05.17 |
---|---|
분산환경 동시성 처리 1 - db 사용 (0) | 2024.05.17 |