Redis 란 무엇일까?
Key, Value 구조의 정형화 되지 않은 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터 베이스 관리 시스템. In memory Key-Value NoSQL.
DISK DBMS에서는 매번 디스크에 접근해야 하기 때문에 사용자가 많아질수록 부하가 많아져서 느려질 수 있습니다. 서비스 운영 초반이거나 규모가 작거나 사용자가 많지 않은 서비스의 경우에는 데이터 베이스에 무리가 가지 않습니다. 하지만 사용자가 늘어난다면 데이터 베이스가 과부하 될 수 있기 때문에 In-Memory DBMS 를 구축하는 것이 좋습니다. 즉 캐시를 생성하여 액세스 시간을 줄이고 처리량을 늘려서 DB에 부담을 줄일 수 있습니다.
Redis에서는 다양한 자료구조를 지원합니다.
lock 을 구현할 때 다른 도구와의 비교
기능 | Redis | Memcached | Zookeeper | Etcd |
성능 | 매우 빠름 | 매우 빠름 | 느림 | 중간 |
데이터 일관성 | 약한 일관성 | 약한 일관성 | 강한 일관성 | 강한 일관성 |
설정 및 운영 | 쉬움 | 쉬움 | 복잡함 | 복잡함 |
데이터 영속성 | 지원 가능 | 지원 안 됨 | 지원 | 지원 |
TTL 지원 | 있음 | 있음 | Ephemeral Node로 가능 | Lease로 가능 |
장애 복구 | 설정 필요 | 설정 필요 | 자동 복구 | 자동 복구 |
분산 환경 | Redis Cluster 필요 | 자체 지원 안 됨 | 기본적으로 지원 | 기본적으로 지원 |
- NoSQL DBMS: Redis, Memcached, Etcd
- 분산 시스템 도구: Zookeeper
Spring 에서는 의존성 추가를 통해 Redis API 를 사용할 수 있습니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
Redis Java 클라이언트: Jedis, Lettuce, Redisson
Jedis
- 가장 오래된 Redis Java 클라이언트 라이브러리
- 장점:
- 간단하고 가벼운 라이브러리
- 대부분의 Redis 기능 지원
- 스레드 세이프하지 않아 스레드별 전용 Jedis 인스턴스 필요
- 단점:
- 비동기 작업 지원 미흡
- 커넥션 풀링 직접 구현 필요
Lettuce
- 현대적이고 Netty 기반의 논블로킹 Redis 클라이언트
- 장점:
- 완전한 비동기/논블로킹 지원
- 스프링 부트의 기본 Redis 클라이언트
- 스레드 세이프
- 높은 성능과 안정성
- 클러스터, 센티널 모드 우수한 지원
- 단점:
- 설정이 Jedis에 비해 복잡할 수 있음
Redisson
- 분산락, 분산 컬렉션 등 고급 기능 제공
- 장점:
- 분산 시스템을 위한 고급 기능 풍부
- 분산락, 분산 컬렉션, 분산 스케줄러 등 제공
- 스프링과 쉬운 통합
- 복잡한 분산 작업에 최적화
- 단점:
- 다른 라이브러리에 비해 무거움
- 고급 기능 미사용 시 오버헤드 발생
상황별 추천
- 단순 Redis 사용 (캐싱, 기본 작업)
- Jedis: 가볍고 심플한 프로젝트
- Lettuce: 스프링 부트 기본, 높은 성능 필요한 경우
- 고성능 비동기 작업
- Lettuce: 명확한 선택
- 분산 시스템, 복잡한 분산 처리
- Redisson: 분산락, 분산 컬렉션 등 필요한 경우
- 대규모 고성능 시스템
- Lettuce: 추천
- 클러스터, 센티널 모드 지원 우수
- 마이크로서비스 아키텍처
- Redisson: 분산 관련 기능 때문에 선호
참고 : https://jojoldu.tistory.com/418
Jedis 보다 Lettuce 를 쓰자
Java의 Redis Client는 크게 2가지가 있습니다. Jedis Lettuce 둘 모두 몇천개의 Star를 가질만큼 유명한 오픈소스입니다. 이번 시간에는 둘 중 어떤것을 사용해야할지에 대해 성능 테스트 결과를 공유하
jojoldu.tistory.com
스프링 부트에서는 별도 설정 없이 RedisTemplate 사용 시 Lettuce가 기본으로 사용됩니다.
왜 Lettuce가 기본 클라이언트인가?
- 성능 이점
- 논블로킹 I/O 모델 (Netty 기반)
- 높은 처리량
- 더 나은 리소스 활용
- 기술적 장점
- 스레드 세이프
- 연결 풀링 기본 지원
- 클러스터, 센티널 모드 우수한 지원
Redis에서 키와 값에 String을 사용하는 주요 이유
- 범용성
- 대부분의 데이터를 문자열로 직렬화 가능
- 정수, UUID, JSON 등 다양한 형태의 데이터를 문자열로 변환 가능
- 간단한 직렬화
// 예시
String lockValue = UUID.randomUUID().toString(); // UUID를 문자열로
String userJson = objectMapper.writeValueAsString(user); // 객체를 JSON 문자열로
// JSON 직렬화
String value = objectMapper.writeValueAsString(complexObject);
// 역직렬화
ComplexObject obj = objectMapper.readValue(value, ComplexObject.class);
- Redis의 기본 데이터 타입
- Redis는 기본적으로 문자열을 가장 기본적인 데이터 타입으로 지원
- 문자열은 가장 가볍고 빠른 연산 지원
- 분산 락 구현에 적합
// 락 키: "reservation:lock:1" (좌석 번호)
// 락 값: UUID 문자열
String lockKey = "reservation:lock:" + seatNum;
String lockValue = UUID.randomUUID().toString();
실제 구현
동작 시나리오:
- 스레드 A: 좌석 1에 대한 락 획득 시도
- 성공하면 UUID 값으로 락 생성
- 다른 스레드들은 이미 락 있어서 실패
- 스레드 A가 작업 완료 후 자신의 UUID로만 락 해제 가능
핵심:
- 한 번에 하나의 스레드만 특정 자원(좌석)에 접근 가능
- 타임아웃으로 데드락 방지
- 오직 락을 건 주체만 락 해제 가능
RedisTemplate 사용
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
/* // 직렬화 설정 (선택사항)
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());*/
return template;
}
}
RedisConfig 클래스
Redis 연결 및 RedisTemplate 설정을 담당하는 구성 클래스입니다.
메서드
- RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory):
- 역할: RedisTemplate을 생성하여 Redis와의 상호작용을 지원합니다.
- 매개변수:
- RedisConnectionFactory: Redis 연결 정보를 포함한 팩토리 객체.
- 리턴값: RedisTemplate<String, String>.
- 설명:
- RedisTemplate 인스턴스를 생성합니다.
- setConnectionFactory(connectionFactory)로 Redis 연결 팩토리를 설정.
- (선택사항) 직렬화 설정 가능:
- RedisTemplate<String, String>: 이미 문자열 형식이므로 불필요
- 필요할 때: JSON 저장 또는 키/값이 객체인 경우 등
@Repository
@RequiredArgsConstructor
public class LockRedisRepository {
private final RedisTemplate<String, String> redisTemplate;
// 락 획득 메서드
public String acquireLock(String lockKey, long expireTimeMillis) {
// 고유한 락 값 생성
String lockValue = UUID.randomUUID().toString();
// setIfAbsent() 메서드는 nx() 옵션과 동일
// Duration을 사용해 만료 시간 설정
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
Duration.ofMillis(expireTimeMillis)
);
// 락 획득 성공 시 lockValue 반환
return acquired != null && acquired ? lockValue : null;
}
// 락 해제 메서드
public boolean releaseLock(String lockKey, String lockValue) {
// Lua 스크립트로 원자적 락 해제 수행
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// RedisScript 생성
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
// 스크립트 실행
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
lockValue
);
// 락 해제 성공 여부 반환
return result != null && result > 0;
}
}
LockRedisRepository 클래스
필드
- RedisTemplate<String, String> redisTemplate:
- 역할: Redis와의 상호작용을 담당하는 Spring Data Redis의 핵심 컴포넌트입니다.
- 타입: 키와 값을 모두 String으로 설정한 RedisTemplate.
메서드
- String acquireLock(String lockKey, long expireTimeMillis):
- 역할: Redis에 지정된 키로 락을 생성합니다.
- 매개변수:
- lockKey: 락을 식별하는 Redis 키.
- expireTimeMillis: 락의 만료 시간 (밀리초 단위).
- 리턴값: 락 획득 성공 시 고유한 lockValue, 실패 시 null.
- 설명:
- UUID.randomUUID().toString()으로 고유한 락 값을 생성.
- redisTemplate.opsForValue().setIfAbsent(...) 메서드를 호출:
- NX 명령과 유사하게, 키가 없을 때만 값을 설정합니다.
- Duration.ofMillis(expireTimeMillis)로 TTL(만료 시간)을 설정.
- 반환 값(Boolean)이 true면 락 획득 성공으로 lockValue 반환, 아니면 null.
- boolean releaseLock(String lockKey, String lockValue):
- 역할: Redis에 저장된 락을 원자적으로 해제합니다.
- 매개변수:
- lockKey: 락을 식별하는 Redis 키.
- lockValue: 락 생성 시 반환된 고유 값.
- 리턴값: 락 해제 성공 여부 (true/false).
- 설명:
- Lua 스크립트를 사용해 락 해제:
- redis.call('get', KEYS[1]) == ARGV[1]로 현재 저장된 값이 lockValue와 같은지 확인.
- 값이 같으면 redis.call('del', KEYS[1])로 키 삭제.
- 다르면 0을 반환.
- RedisScript<Long>를 통해 스크립트를 실행:
- redisTemplate.execute()로 Lua 스크립트를 실행.
- 반환 값이 1 이상이면 락 해제 성공.
- Lua 스크립트를 사용해 락 해제:
-
- RedisTemplate을 사용하여 Redis에서 분산 락을 관리하는 저장소 클래스입니다.
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end"
이 스크립트의 의미:
- 현재 락의 값(redis.call('get', KEYS[1]))이
- 우리가 전달한 락 값(ARGV[1])과 일치하면
- 해당 키를 삭제(redis.call('del', KEYS[1]))
- 일치하지 않으면 0을 반환
목적: 락을 생성한 주체만 락을 해제할 수 있게 하는 안전한 메커니즘
Redis Client 사용
@Configuration
public class RedisConfig {
@Bean // redis 에 연결하기 위한 객체 생성
public RedisClient redisClient() {
return RedisClient.create("redis://localhost:6379"); // Redis URL 설정
}
@Bean // redis 서버와의 상태 기반 연결, Stateful (상태유지). 클라이언트-서버 관계에서 서버가 클라이언트의 상태를 보존함을 의미
public StatefulRedisConnection<String, String> redisConnection(RedisClient redisClient) {
return redisClient.connect();
}
@Bean // Redis 서버에서 동기식 명령을 수행하기 위한 인터페이스를 제공
public RedisCommands<String, String> redisCommands(StatefulRedisConnection<String, String> connection) {
return connection.sync(); // 동기식 명령 실행 객체를 반환
}
@Bean // RedisLock 객체를 생성하여 Redis를 활용한 분산 락 구현
public RedisLock redisLock(RedisCommands<String, String> redisCommands) {
return new RedisLock(redisCommands);
}
}
RedisConfig 클래스
- 역할: Spring Bean을 등록하여 Redis 연결과 관련된 의존성을 설정하는 구성 클래스입니다.
필드 및 메서드 설명:
- RedisClient redisClient():
- 역할: Redis에 연결하기 위한 RedisClient 객체를 생성합니다.
- 매개변수: 없음.
- 리턴값: RedisClient 객체.
- 설명: RedisClient.create("redis://localhost:6379")는 Redis 서버 URL을 기반으로 클라이언트를 초기화합니다.
- StatefulRedisConnection<String, String> redisConnection(RedisClient redisClient):
- 역할: Redis 서버와의 상태 기반 연결을 생성합니다.
- 매개변수: RedisClient (위에서 생성된 Redis 클라이언트).
- 리턴값: StatefulRedisConnection<String, String>.
- 설명: connect() 메서드를 호출해 Redis 서버에 연결합니다.
- RedisCommands<String, String> redisCommands(StatefulRedisConnection<String, String> connection):
- 역할: Redis 서버에서 동기식 명령을 수행하기 위한 인터페이스를 제공합니다.
- 매개변수: StatefulRedisConnection.
- 리턴값: RedisCommands<String, String>.
- 설명: connection.sync()를 호출해 동기식 명령 실행 객체를 반환합니다.
- RedisLock redisLock(RedisCommands<String, String> redisCommands):
- 역할: RedisLock 객체를 생성하여 Redis를 활용한 분산 락 구현을 제공합니다.
- 매개변수: RedisCommands<String, String>.
- 리턴값: RedisLock.
- 설명: Redis 기반의 락을 관리하는 RedisLock 클래스를 Bean으로 등록합니다.
public class RedisLock {
private final RedisCommands<String, String> redisCommands;
public RedisLock(RedisCommands<String, String> redisCommands) {
this.redisCommands = redisCommands;
}
public String acquireLock(String lockKey, long expireTimeMillis) {
String lockValue = UUID.randomUUID().toString(); // 고유 값 생성
String result = redisCommands.set(
lockKey,
lockValue,
SetArgs.Builder.nx().px(expireTimeMillis)
);
return "OK".equals(result) ? lockValue : null; // 성공 시 lockValue 반환
}
public boolean releaseLock(String lockKey, String lockValue) {
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisCommands.eval(luaScript, ScriptOutputType.INTEGER, new String[]{lockKey}, lockValue);
return result != null && result > 0;
}
}
RedisLock 클래스
- 역할: Redis를 이용한 분산 락을 구현하여 락의 획득과 해제를 관리합니다.
필드 및 메서드 설명:
- 필드:
- RedisCommands<String, String> redisCommands:
- 역할: Redis 서버와 통신하여 명령을 실행하는 객체.
- 초기화: 생성자 주입으로 설정됨.
- RedisCommands<String, String> redisCommands:
- String acquireLock(String lockKey, long expireTimeMillis):
- 역할: 주어진 키로 락을 획득합니다.
- 매개변수:
- lockKey: 락을 나타내는 키.
- expireTimeMillis: 락의 유효 기간(밀리초 단위).
- 리턴값: 성공 시 고유 lockValue 문자열, 실패 시 null.
- 설명:
- UUID.randomUUID().toString()으로 고유 값을 생성.
- redisCommands.set(lockKey, lockValue, SetArgs.Builder.nx().px(expireTimeMillis))를 호출해 Redis에 키-값과 유효 기간을 설정.
- NX: 키가 없을 때만 설정, PX: 만료 시간 설정.
- boolean releaseLock(String lockKey, String lockValue):
- 역할: 주어진 키와 값으로 락을 해제합니다.
- 매개변수:
- lockKey: 락을 나타내는 키.
- lockValue: 락 획득 시 반환된 고유 값.
- 리턴값: 락 해제 성공 여부 (true/false).
- 설명:
- Lua 스크립트를 사용해 원자적으로 락을 해제합니다.
- 스크립트:
- redis.call('get', KEYS[1]) == ARGV[1]: 저장된 값이 lockValue와 같은지 확인.
- 같으면 redis.call('del', KEYS[1])로 키 삭제.
- 원자성을 보장하기 위해 Redis의 eval 메서드를 사용.
@Service
@RequiredArgsConstructor // final 이 붙은 필드만 생성자로 만들어줌.
public class ReservationService {
private final ReservationRepository reservationRepository;
private final EventRepository eventRepository;
private final RedisLock redisLock;
private final String lockKeyPrefix = "reservation:lock:";
public String reserve(User user, Long eventId, ReservationRequestDTO requestDTO) {
Long seatNum = requestDTO.getSeatNum();
String lockKey = lockKeyPrefix + seatNum;
String lockValue = redisLock.acquireLock(lockKey, 5000); // 5초 유효기간
if (reservationRepository.existsBySeatNum(seatNum)) {
throw new IllegalArgumentException("해당 좌석은 예매가 완료되었습니다.");
}
if (lockValue == null) {
throw new IllegalStateException("현재 다른 사용자가 해당 좌석을 예약 중입니다. 다시 시도하세요.");
}
Event event = eventRepository.findById(eventId).orElseThrow(
() -> new IllegalArgumentException("해당 콘서트 정보가 존재하지 않습니다")
);
try {
Reservation reservation = new Reservation(seatNum, user, event);
reservationRepository.save(reservation);
return "예매 성공";
} finally {
// 락 해제
redisLock.releaseLock(lockKey, lockValue);
}
}
}
// 락을 획득하지 못했을 때의 동작 방식은 정하기 나름,
// 예를 들어 대기, 재시도, 포기 등
'기타' 카테고리의 다른 글
WSL, Git bash, Terminal (0) | 2024.12.17 |
---|---|
InfluxDB, grafana (0) | 2024.12.17 |
[개인과제] 플러스 과제 - 회고 (1) | 2024.11.21 |
백엔드 개발 첫 팀프로젝트 회고..... (2) | 2024.10.24 |
1주차 팀 소개 페이지 프로젝트 KPT (0) | 2024.08.30 |