
백엔드/서버
개발자라면 반드시 동시성
문제를 만나게 된다.
특히, 요즘 K8s
환경을 자주 사용하는데 아무리 애플리케이션에서 동시성
발생 대비를 해도
다른 Pod
에서 같은 DB
에 접근하면 동시성 문제를 처리하는 것이 어렵다.

위의 그림과 같은 경우 DB
또는 Redis
에서 제공하는 Lock
을 활용하여 데이터의 동시성 접근을 제어해야 한다.
DataBase Lock
을 이용할 경우 추가적인 인프라 구성요소 없이 동시성
을 해결할 수 있다는 장점이 있으나
Lock
획득을 위해 Waiting
되는 DataBase Connection
증가로 인해 부하가 발생할 수 있다.
Redis
를 이용하여 Distributed Lock
을 사용할 경우 DataBase Connection
증가는 방지할 수 있지만
Redis
의 관리가 필요하다.

우리는 이전 포스팅에서 DataBase Lock
인 Optimistic Lock
과 Pessimistic Lock
을 구현해보며 확인했다.
2024.09.27 - [Spring] - [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 3
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 3
2024.09.25 - [Spring] - [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 2 [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 22024.09.22 - [Spring] - [Spring] synchronized 키워드를 활
hdbstn3055.tistory.com
그렇기에 이번 포스팅에서는 Redis Distributed Lock
에 관해 알아본다.
Redis Distributed Lock
Redis
는 문서에서 Distributed Lock
를 위해 RedLock
이라는 알고리즘을 제시하고 있으며
Java
에서의 구현된 라이브러리는 Redisson
이 존재한다.
왜 spring-data-redis
의 기본 구현체인 Lettuce
가 아닌 Redisson
를 사용할까?
- Lettuce
spring-data-redis
의 기본 구현체- 기본적으로
Spin Lock
을 사용한다.- 이는 Lock을 대기하는 상황에서, Lock을 획득할 수 있는지 계속 요청을 보낸다.
- 따라서, Lock을 획득하려는 스레드가 많을 경우 Redis에 부하가 집중된다.
- Lock에 관한 타임아웃이 없어,
Unlock(잠금 해제)
를 호출하지 못한 경우Dead Lock
을 유발할 수 있다.
- Redisson
- Pub/Sub 방식을 사용한다.
- Lock을 당장 획득할 수 없으면 대기한다.
- Lock이 획득 가능할 경우 Redis에서 클라이언트로 획득 가능함을 알린다.
- Lock의 lease time 설정이 가능하다.
- 즉, 설정된
lease time
이 지난 경우 자동으로 Lock의 소유권을 회수하여 Dead Lock을 방지한다.
- 즉, 설정된
- Pub/Sub 방식을 사용한다.
Redisson Config
기본적으로 Redis
와 통신하기 위한 RedisClient
와 RedisConnectionFactory
를 생성해야 한다.
Redisson
에서는 RedissonClient
와 RedissonConnectionFactory
를 Bean
으로 생성해야 한다.
아래의 예시는 Redis
서버를 하나 구성하였을 경우이며 Redis
구성에 설정은 다음 문서를 확인하자.
application.yml(내부에 spring-boot-starter-data-redis 의존성도 존재)
/* Redis 추가 */
implementation("org.redisson:redisson-spring-boot-starter:3.21.1")
RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379");
return Redisson.create(config);
}
@Bean
public RedissonConnectionFactory redissonConnectionFactory(RedissonClient redissonClient){
return new RedissonConnectionFactory(redissonClient);
}
}
Redisson Distributed Lock
Redisson
를 활용한 Distributed Lock
사용 방식은 간단하며 다음 문서를 참고하면 된다.
RedissonClient
를 통해 해당Lock
의Key
를 설정하여RLock
생성RLock.lock
또는RLock.tryLock
를 통해Lock
획득이 가능하며waitTime
과leaseTime
설정 가능waitTime
:Lock
획득 대기 시간leaseTime
: 'Lock` 획득 이후 소유 시간
RLock.unlock
를 통해Lock
반납
@Service
@RequiredArgsConstructor
public class TicketV7Service {
private final RedissonClient redissonClient;
private final TicketRepository ticketRepository;
@Transactional
public Ticket purchaseRLock(Long ticketId, int amount) {
RLock lock = redissonClient.getLock("TEST");
try{
if(lock.tryLock(5000, 3000, TimeUnit.MILLISECONDS)){
Ticket ticket = ticketRepository.findById(ticketId).get();
ticket.setStock(ticket.getStock() - amount);
return ticket;
} else{
throw new RuntimeException();
}
} catch (InterruptedException e){
throw new RuntimeException(e);
} finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
문제 경우: 동시성이 적절히 처리되지 않았다.



왜 Lock
을 처리했음에도 문제가 해결되지 않았을까?
그건 이전 포스팅에서도 살펴보았던 @Transcational
어노테이션의 동작 방식 때문이다.
문제 이유
즉, Spring Transcational Commit
과 Distributed Lock 반납
시점차가 존재하기 때문이다.

위의 그림과 같이 Distributed Lock 반납
이 Spring Transaction Commit
보다 먼저 실행되어
해당 시점에 다른 요청이 있는 경우 Commit
이전의 데이터를 조회하여 예상과 다른 결과가 나타나게 되는 것이다.
해결 방안1: 서비스 클래스 앞단에 Lock을 위한 프록시 클래스를 두는 것이다.
이전 포스팅에서 살펴본 TicketFacade
같은 형식
아래와 같이 설정하면 @Transactional
이 동작하기 전에 Lock
이 걸리므로 정상 동작할 것이다.
즉, Service
를 타겟 클래스로 두고 해당 클래스를 접근하기 전에 Lock
을 거는 것이다.
@Component
@RequiredArgsConstructor
public class TicketFacade {
private final TicketV7Service target;
private final RedissonClient redissonClient;
@Transactional
public Ticket invoke(Long ticketId, int amount){
RLock lock = redissonClient.getLock("TEST");
try{
if(lock.tryLock(5000, 3000, TimeUnit.MILLISECONDS)){
return target.purchase(ticketId, amount);
} else{
throw new RuntimeException();
}
} catch (InterruptedException e){
throw new RuntimeException(e);
} finally {
if(lock.isLocked() && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
}
TicketV7Service
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Ticket purchase(Long ticketId, int amount) {
Ticket ticket = ticketRepository.findById(ticketId).get();
ticket.setStock(ticket.getStock() - amount);
return ticket;
}
결과




위 방식의 흐름은 다음과 같다.
Tx#1 시작
->Distributed Lock 획득
->Tx#2 시작
->로직
->Tx#2 종료
->Distributed Lock 반납
->Tx#1 종료
해결 방안2: @TransactionalEventListener
@TransactionalEventListener
를 활용하여 Distributed Lock 반납
을 Spring Transactional
이후에 실행할 수 있다.
즉, Spring ApplicationEventPublisher
를 활용해서 이벤트를 Publish
하고 AFTER_COMMIT
이벤트가 발생하면 그걸 @TransactionalEventListener
가 읽어서 Lock
을 해제한다.
@Service
@RequiredArgsConstructor
public class TicketV7Service {
private final ApplicationEventPublisher applicationEventPublisher;
private final RedissonClient redissonClient;
private final TicketRepository ticketRepository;
private final EventHandler eventHandler;
@Transactional
public Ticket purchaseRLock(Long ticketId, int amount) {
RLock lock = redissonClient.getLock("TEST");
try{
if(lock.tryLock(5000, 3000, TimeUnit.MILLISECONDS)){
Ticket ticket = ticketRepository.findById(ticketId).get();
ticket.setStock(ticket.getStock() - amount);
applicationEventPublisher.publishEvent(lock);
return ticket;
} else{
throw new RuntimeException();
}
} catch (InterruptedException e){
throw new RuntimeException(e);
} finally {
// if(lock.isLocked() && lock.isHeldByCurrentThread()){
// applicationEventPublisher.publishEvent(lock);
// }
}
}
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void unlockRLock(RLock rLock) {
if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
결과




위 방식의 흐름은 다음과 같다.
Tx#1 시작
->Distributed Lock 획득
->로직
->Tx#1 종료
->Tx#2 시작
->Distributed Lock 반납
->Tx#2 종료
즉, 핵심은 Tx 종료
이후에 Distributed Lock 반납
이 이루어져야 한다는 것이다.
'Spring > 동시성 & Lock' 카테고리의 다른 글
[캐시 & 동시성 & Lock] 선착순 티켓 시스템 고도화 - 1 (0) | 2024.11.19 |
---|---|
[Spring Data] Redis Lock을 적용해보자 - 2 (0) | 2024.10.17 |
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 3 (0) | 2024.09.27 |
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 2 (0) | 2024.09.25 |
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 1 (0) | 2024.09.22 |