문제 상황
어떤 데이터에 대해 매우 빠르게 수정이 일어날 때 동시성 문제가 발생할 수 있다.
예를 들어 A라는 데이터를 수정하는 로직이 0.1초 소요되는데, 0.001초 간격으로 A라는 데이터를 수정하는 요청이 여러 번 들어왔을 때 값이 제대로 수정되지 않을 수 있다.
이럴 때, DB 데이터 수정의 순차적 처리를 보장하기 위한 방법으로 Lock이 있다.
Lock
트랜잭션 락과 비슷한 개념으로 어떤 데이터 수정, 접근 등에 Lock을 필요로 하게 설정하는 것이다.
데이터를 수정하려면 우선 Lock을 획득하고 수정 후에 Lock을 반납한다.
만약 Lock을 획득할 수 없는 상황에서는 Lock을 획득할 수 있을 때까지 대기하거나 수정을 취소할 수 있다.
분산 락(Distributed Lock)
하나의 공유 자원에 대한 경쟁 상황에서 데이터에 접근할 때, 데이터의 결함이 발생하지 않도록 원자성(atomic)을 보장하는 기법이다.
동시성 이슈
하나의 스레드가 데이터를 수정 중인 상황에서 다른 스레드에서 수정 전의 데이터를 조회하여 수정함으로써 데이터의 정합성(consistency)이 깨지는 문제를 말한다.
락 획득 시나리오
분산락 구현을 위한 Redis 사용
스프링 부트에서 Redis는 Lettuce, Redisson 등 여러 가지 라이브러리로 지원된다.
이 중, Redisson을 이용할 것이다.
분산락을 구현하는 데 있어서 Redisson과 Lettuce의 차이점을 알아보자.
Lettuce
Lettuce는 공식적으로 분산락 기능을 제공하지 않는다. 따라서 직접 구현해서 사용해야 한다.
Lettuce의 락 획득 방식은 락을 획득하지 못한 경우 락을 획득하기 위해 Redis에 계속해서 요청을 보내는 스핀락(spin lock)으로 구성되어 있다. 이 스핀 락 방식은 계속해서 요청을 보내는 방식으로 인해 redis에 부하가 생길 수 있다는 단점이 있다.
Redisson
Redisson은 락 획득 시 스핀 락 방식이 아닌 pub/sub 방식을 이용한다.
pub/sub 방식은 락이 해제될 때마다 subscribe중인 클라이언트에게 "이제 락 획득을 시도해도 된다."라는 알림을 보내기 때문에, 클라이언트에서 락 획득을 실패했을 때, redis에 지속적으로 락 획득 요청을 보내는 과정이 사라지고, 이에 따라 부하가 발생하지 않게 된다.
또한 Redisson은 RLock이라는 락을 위한 인터페이스를 제공한다. 이 인터페이스를 이용하여 비교적 손쉽게 락을 사용할 수 있다.
RLock 라이브러리
락 획득 : tryLock
boolean tryLock(long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException;
- waitTime: 락 획득을 위해 기다리는 시간
- leaseTime: 락을 임대하는 시간
- timeUnit: 시간 단위
이 설정에 따라 락 획득을 요청했을 때 락을 획득할 수 없다면 waitTime만큼 기다리고,
락을 획득했다면, 최대 leaseTime만큼 락을 점유할 수 있다.
락 획득과 반납 코드
RLock rLock = redissonClient.getLock(lockName);
try {
boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);
if(!available){
return false; //락 획득 실패
}
//락 획득 후 로직 수행
}catch (InterruptedException e){
//락을 얻으려고 시도하다가 인터럽트를 받았을 때 발생하는 예외
}finally{
try{
rLock.unlock();
log.info("unlock complete: {}", rLock.getName());
}catch (IllegalMonitorStateException e){
//이미 종료된 락일 때 발생하는 예외
}
}
Redis 설정
Redis 설치
아래 글을 확인하자
2023.11.07 - [Database] - [Redis] 레디스 설치 및 기본 기능
build.gradle implementation
dependencies {
//...
implementation 'org.redisson:redisson-spring-boot-starter:3.17.0'
}
application.properties
spring.redis.host=localhost
spring.redis.port=6379
Configuration
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + ":" + port);
return Redisson.create(config);
}
}
실제 사용 예제
public void update(long id){
Member member = memberRepository.findById(id);
String lockName = "MEMBER" + member.getId();
RLock rLock = redissonClient.getLock(lockName);
long waitTime = 5L;
long leaseTime = 3L;
TimeUnit timeUnit = TimeUnit.SECONDS;
try {
boolean available = rLock.tryLock(waitTime, leaseTime, timeUnit);
if(!available){
throw new CustomException(ErrorCode.LOCK_NOT_AVAILABLE);
}
//=== 락 획득 후 로직 수행 ===
// ...
// === 로직 수행 완료 ===
}catch (InterruptedException e){
//락을 얻으려고 시도하다가 인터럽트를 받았을 때 발생하는 예외
throw new CustomException(ErrorCode.LOCK_INTERRUPTED_ERROR);
}finally{
try{
rLock.unlock();
log.info("unlock complete: {}", rLock.getName());
}catch (IllegalMonitorStateException e){
//이미 종료된 락일 때 발생하는 예외
throw new CustomException(ErrorCode.UNLOCKING_A_LOCK_WHICH_IS_NOT_LOCKED);
}
}
}
이 글에서는 기본적으로 분산락을 이용해 보는 예제를 다뤘다.
Spring에서는 AOP 기술을 이용하여 더 효율적으로 분산락을 다룰 수 있다.
다음 글에서는 AOP 기술을 이용하여 annotation 기반으로 사용해 볼 예정이다.
2023.11.10 - [전체 글 보기] - [Redis] Redisson 분산락(DistributedLock) AOP 적용
'Database' 카테고리의 다른 글
[Database / SQLD] 데이터 모델링의 이해 (Part1 - Ch01) (0) | 2024.04.19 |
---|---|
[Redis] Redisson 분산락(DistributedLock) AOP 적용 (0) | 2023.11.10 |
[Redis] 레디스 설치 및 기본 기능 (0) | 2023.11.07 |
[H2 DB] 인메모리 방식 연결 방법 (미설치) (1) | 2023.08.18 |
[DB] H2 데이터베이스 설치하고 시작하기 (0) | 2023.05.29 |