포트폴리오에 Redis를 적어두면, 요즘 워낙 Redis를 흔히 사용하는 분위기여서 그런지 많은 질문들을 받을 수 있다. 나는 세션 공유 개념에서 정말 좋다고 생각해서 가져다 쓴 기술이지만, 제대로 알지 못한 상태에서 사용하는 것은 안좋은 것 같다. 이것은 연구에서도 남의 논문에 대해 제대로 읽지 못한 상태에서 관련 연구를 작성하지 못하는 것과 유사한데, 논문에서 내가 신문물을 대하는 태도를 개발에서도 가져갈 수 있도록 노력해야겠다. 아무래도 이전의 Redis 게시글과 관련해서 알아가면, 정말 많이 도움될 것 같다.
Redis로 분산락 관리하기
목차
위의 목차를 클릭하면 해당 글로 자동 이동 합니다.
락?
- 락은 여러 트랜잭션들을 관리하기 위해서 필요하다. 예를 들면, 동일한 자원에 대해 트랜잭션이 여러 개 있을 때 순서대로 처리하기 위함이다.
- 이를 통해, 동시성 문제를 해결한다.
- 락은 아래와 같이 크게 두 종류가 있다.
- 공유 락(Shared lock)
- 데이터를 변경하지 않는 읽기 명령에 대해 주어지는 락이다.
- 여러 사용자가 동시에 데이터를 읽더라도 데이터의 일관성에는 영향이 없기 때문에 공유락끼리는 동시 접근이 가능하다.
- 베타락(Exclusive lock)
- 데이터에 변경을 가하는 쓰기 명령들에 대해 주어지는 락이다.
- 베타락이 설정된 자원은 다른 곳에서 접근할 수 없다.
- 멀티스레딩 환경에서 임계 영역을 안전하게 관리하기 위해 활용되는 뮤텍스와 유사하다.
- 베타락은 트랜잭션 동안 유지된다.
Redis로 락 구현하기
- Redis로 락을 구현할 때 두 가지의 방식이 있다.
- 첫째, Spin lock이다.
- 이 경우, Lettuce 라이브러리를 사용하며, 락을 획득할 때까지 Redis에 요청을 보낸다.
- 따라서, Redis에 부하를 줄 수 있다.
- 둘째, Pub/Sub 방식이다.
- 일종의 옵저버 패턴과 유사하다고 볼 수 있다.
- 락을 획득하고 싶은 스레드는 먼저 락을 획득할 수 있는지 살핀다.
- 만약 획득할 수 없는 상태라면 락의 획득 유무를 알려주는 채널을 구독한다.
- 이미 락을 점유하고 있던 스레드가 트랜잭션을 종료하면서 락을 풀면, 채널을 통해 락이 해제되었다고 알림이 전송된다.
- 채널을 구독하고 있는 알림을 받아서 스레드들을 락을 획득하기 위해 요청을 보낸다.
- 이 경우는 Redis에 계속 요청을 가하는 방식이 아니기 때문에 Redis에 부하를 덜 준다.
- 하지만, 락을 획득하기 위해 기다리는 시간이나 락을 얼마나 점유하고 있을지와 같은 하이퍼 파라미터들을 조정할 필요가 있다.
- Redisson 라이브러리를 사용한다.
- 본 게시글에서는 Redisson 라이브러리를 사용할 것이다.
실제 구현
- 나는 다중화된 API 서버에서 좋아요 수를 +1하는 로직이 동시성 문제를 겪지 않도록 Redisson을 설정했다.
- 해당 프로젝트에서 어떻게 Redisson을 설정했는지 본다.
- 본 프로젝트는 Spring 프레임워크가 적용되어 있다.
1. 라이브러리
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.1</version>
</dependency>
2. @Config 클래스
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
@Configuration
@PropertySource("classpath:config/database.properties")
public class RedissonConfig {
@Autowired
private Environment env;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
RedissonClient redisson = null;
String redisHost = env.getProperty("redis.address");
int redisPort = Integer.parseInt(env.getProperty("redis.port"));
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
redisson = Redisson.create(config);
return redisson;
}
}
- 나는 Redis의 포트와 주소를 properties 파일에 저장해두었기 때문에, 불러와서 사용했다.
- 그렇지 않은 사람들은 직접 설정하면 된다.
- 설정이 간단하기 때문에 더 이상 건드릴 부분은 없었다.
- 아주 당연하게도 @Configuration 어노테이션을 쓰려면, SpringContext.xml에 설정이 되어 있어야 한다.
3. 실제 코드
// Lock
final String lockName = Integer.toString(id) + ":lock";
final RLock lock = redissonClient.getLock(lockName);
try {
// 락 획득을 위해 기다리는 시간, 락 획득후 점유할 수 있는 최대 시간, 시간 단위
if (lock.tryLock(1, 3, TimeUnit.SECONDS))
// 트랜잭션이 적용된 메소드
} catch (Exception e) {
throw new RuntimeException("Error");
} finally {
if (lock != null && lock.isLocked())
lock.unlock();
}
- tryLock()의 파라미터는 주석에 적혀있는 대로이다.
- 앞서도 얘기했듯이 락은 커밋 이후에 관리가 되어야 하기 때문에 @Transactional이 적용된 메소드 바깥에서 관리해야 한다.
- 그 이유는 @Transactional 은 Spring AOP로 트랜잭션 코드를 가지고 있는 프록시 빈 객체가 트랜잭션이 적용될 메소드를 호출하기 때문이다. 그래서 프록시 빈 객체의 메소드까지 전부 끝난 뒤에 락을 회수하는 것이 맞다.
- Pub/Sub 방식이 복잡하겠지만, 실제로는 Redisson에서 쉽게 설정이 가능하다.
- 안타깝게도 tryLock()의 파라미터들은 적절하게 튜닝될 필요가 있다.
- 그렇지 않으면, 트랜잭션이 종료되기 전에 락 점유가 풀리거나 락을 획득하기 위한 대기 시간이 너무 짧아 결국 로직이 수행되지 못하는 경우가 생길 것이다.
이제 Redis가 살짝 무서울 따름이다. 아 그리고 사실 어떤 값을 +1 하는 메소드는 Redis에서 incr()이라는 Atomic한 메소드를 지원해주기 때문에, 싱글 스레드로 동작하는 Redis에서 +1을 위해서는 분산락 처리를 할 필요가 없다. 어짜피 하나씩 하나씩 순차적으로 처리된다. 그렇지만 Atomic한 메소드를 두 개 이상 쓰거나, 하나의 트랜잭션에서 여러 연산이 필요한 경우라면 위와 같이 분산락을 고려해야 한다.
추천글
2025.01.19 - [개발] - Redis의 고가용성 전략(Sentinel, Clustering)
Redis의 고가용성 전략(Sentinel, Clustering)
최근에 Redis가 고가용성을 어떻게 보장하는가?에 대한 면접 질문을 받게 되어서 조금 공부를 해보았습니다. Redis의 고가용성 전략(Sentinel, Clustering) Redis의 고가용성 1. Sentinel 2. Clustering추천
se-dobby.tistory.com
'개발' 카테고리의 다른 글
CORS에서 Cross-Origin과 쿠키의 Same-Site (0) | 2025.02.10 |
---|---|
트랜잭션과 격리성 그리고 낙관적 락과 비관적락에 대한 얘기 (0) | 2025.02.09 |
Redis의 고가용성 전략(Sentinel, Clustering) (0) | 2025.01.19 |
Spring Web MVC에서 Spring session redis 적용 (0) | 2025.01.19 |
적절한 DB Connection Pool 설정하기 (0) | 2025.01.01 |