블로그 프로젝트의 조회수 증가를 Redis로 관리하기로 결정했다.
Redis가 무엇인지 알아보고, 왜 Redis를 써야하는지와 어떻게 사용할 것인지에 대해 정리해보자.
Redis 개요
Redis는 Remote Dictionary Server의 약자이다.
- Remote: 외부를 의미하고
- Dictionary: HashMap과 같은 key - value 자료 구조
- Server: 서버를 의미한다.
즉, 외부에 있는 key - value의 자료구조를 사용하는 서버이고 데이터베이스를 의미한다.
인메모리 데이터 저장소
레디스 공식 홈페이지를 보면, 이렇게 설명한다.
데이터베이스, 캐시, 스트리밍 엔진 및 메시지 브로커를 인메모리 데이터 저장소
로 사용한다.
그렇다, 핵심은 인메모리
이다.
Cache 개념
나중의 요청에 대한 결과를 미리 저장했다가 빠르게 사용하는 것으로
변경이 적은 것들에 적용하여 사용한다. css 같은 것들이 될 수 있겠다.
정리하자면, DB보다 더 빠른 메모리에 접근하여, 변경이 적은 데이터를 저장하는 목적으로
나온 것이 Redis
가 나온 것이다. 우리는 조회수
를 일괄적용하기 위해 캐시 서버
로 사용할 것이다.
Redis 자료구조
Redis는 key - value의 형태의 자료구조를 가진다고 했다. 하지만 String뿐 아닌 여러 자료 구조를 지원한다.
더욱 상세한 내용은 공식 문서 참고
sorted set에 대해서만 간단히 알아보면, 랭킹 데이터 같은 것들을 관리하기 쉽다.
물론 DB에서 직접 order by를 해도 되겠지만, 데이터가 많이질수록 속도가 느려지기 때문에 캐싱해두고 사용하는데 활용할 수 있다.
비슷한 기술로 memcached
라는 key - value 형태의 저장소가 있는데,
- Redis처럼 다양한 자료구조를 지원하지 않는다.
그렇기 때문에 펀의성이 떨어진다고 볼 수 있다. - Replication을 지원하지 않음
- 클러스터링을 제공하지 않음.
- 메모리 관리가 편하다. Redis는 메모리 파편화가 일어날 수 있다.
Java의 HashMap을 쓰면되지 않을까?
서버가 여러대인 경우 Consistency 문제
서버가 여러대인 경우 데이터를 공유할 수 없다. 예를 들어, 세션 로그인을 한다고 하면,
다른 서버에서는 로그인이 되지 않은 상태가 되는 현상과 같은 것들이다.
멀티 쓰레드 환경에서 Race Condition 문제
Critical Section(임계영역) 문제
- 두 개 이상의 스레드들이 하나의 리소스에 접근하기 위해 경쟁하는 상태
- 경쟁하는 상태가 되면, 컨텍스트 스위칭을 하다가 의도치 않은 결과가 발생할 수 있음.(아래 예시)
- 통장에 잔고가 100만원이 있다. 컴퓨터를 구매하기 위해 100만원을 이체하는 도중에
청약 저축 10만원이동시에 자동이체가 된 것이다.
이유는 컨텍스트 스위칭을 하던 중
두 쓰레드 모두 100만원의 잔액을 가져오고, 이체를 했기 때문이다.
이런 영역을Critical Section(임계영역)
이라고 한다.
- 통장에 잔고가 100만원이 있다. 컴퓨터를 구매하기 위해 100만원을 이체하는 도중에
Race Condition 해결
- 싱글 스레드 사용
- Redis의 자료구조는 Atomic Critical Section에 대한 동기화를 제공한다.
- 즉, 서로 다른 스레드들이 읽기/쓰기를 할 때 이를 동기화하여 원치않는 결과를 막아준다.
- Atomic이란 중단되지 않는 연산이라고 볼 수 있다. 즉, 동기화 작업이라고 볼 수 있다.
예를 들어,
1) 컴퓨터를 구매하는데 결제는 성공하고, 주문이 들어가지 않을 수는 없다.
주문이 들어가지 않으면, 결제도 실패해야한다. 이와 같은 것을 의미한다.
2) 조회수 증가를 구현한다고 할 때, 트랜잭션1(T1)이 1번 게시물(P1)의 조회수를 조회한 결과가 10이었다.
트랜잭션2(T2)도 P1의 조회수를 조회한 결과가 10이었다. T1과 T2는 조회수를 1증가 시켰다.
최종적으로 결과는 12가 되어야 하지만 11이 되는 현상이 발생하는데 이를 동기화하여 12로 만들어야 한다.
이런 작업을 의미한다. - 그래도 잘못 코딩하면 발생한다.???
언제 사용해야 할까?
- 여러 서버에서 같은 데이터를 공유할 때
RDB를 사용해도 되겠지만, 비효율적이라면 고려해볼만 함. - 싱글 서버라면?
- Atomic 자료구조
나는 조회수 구현에 사용한다고 했는데 조회수는 결제만큼 중요하지는 앟지만,
누락하지 않아야 한다면, 충분히 사용할만한 이유이다. 특히 유튜브 같은 조회수가
수익으로 연결되는 서비스라면 더욱 그럴 것이다. - Cache 서버
조회수를 조회할 때 마다 I/O를 하지 않기 위해 캐싱하는 용도로 사용할 수 있다.
- Atomic 자료구조
인메모리를 사용할 이유
- Write back일 경우
- Look aside일 경우
Redis 주의사항
O(N)은 지양하자.
Redis는 Single Thread이다.
- 즉, 동시에 단 1개의 명령만 처리할 수 있다.
- 하지만 단순한 get / set의 경우, 10만 TPS(Transaction Per Second)이상 가능(CPU 속도에 영향을 받음)
- 만약 1번 Transaction이 1초가 지연되면, 나머지 99,999개도 1초가 지연되는 것.
- 아래 그림과 같이 Packet으로 하나의 Command가 완성되면, Process Command에서 실제로 실행이 되기 때문에,
지연되면 나머지 Packet들도 지연이 되는 것이다.
대표적인 O(N) 명령들
1개의 명령만 처리할 수 있기 때문에 오래 걸리는 명령어는 사용하면 안된다.
물론 몇백개라면 크게 상관은 없다. 하지만 백만개를 지운다거나 십만개를 가져온다거나 하면 오래걸리므로 지연이 발생
keys
scan
명령으로 사용하면 하나의 긴 keys 명령을 짧은 여러번의 명령으로 바꿀 수 있다.scan
을 다시 명령하는 찰나의 순간에 다른 작업이 수행될 수 있기 때문이다.scan
은 커서 방식으로 다음 커서를 지정하게 되고, 10개씩 key를 조회하게 된다.
flushAll, flushDB
- 필요하면 써야함. 어쩔 수가 없는 부분
delete collections
get all collections
- 큰 Collection을 작은 여러개의 Collection으로 나눠서 저장
- UserRanks → UserRank1, UserRank2
- 하나 당 몇천개 안쪽으로 저장하는게 좋음.
- 큰 Collection을 작은 여러개의 Collection으로 나눠서 저장
메모리 관리
메모리 파편화
메모리를 할당하고 해제하는 과정에서 빈 공간이 생기게 된다. 이후 빈 공간보다 더 큰 메모리를 할당하게 되면,
빈 공간은 채워지지 않게되고, 메모리 낭비가 되어 서버가 터질 수도 있다. 그렇기 떄문에 메모리는 여유롭게 사용하는 것이 좋다.
메모리 SWAP
메모리가 부족하면 swap을 하게된다. swap을 사용해서 서버가 죽지는 않지만, 레이턴시가 발생한다.
하지만 swap이 없다면, 죽게된다. 또한, swap이 한 번이라도 된 메모리는 계속 swap이 일어남.
→ Redis 2.6이상 부터는 swap을 사용하지 않는다고 한다.
즉, swap을 사용해야 할지 말지 고려하고, Redis의 메모리 용량을 모니터링 하는 것이 중요하다.
Replication(복제)
Redis는 메모리에 저장하므로, 데이터 유실을 생각해야 한다. 그래서 Redis는 Replication(복제)가 가능하다.
Redis 서버는 Master-Replica(구: Slave)형태로 구성된다. Master → Replica로 복제한다.
Async Replication(비동기 복제)
Replicaof(Slaveof) <hostname> <port>
명령어로 설정할 수 있다.
비동기 복제로 Replication Lag이 발생할 수 있다. 예를 들어, Master 인스턴스트에서 <A, 100>이 저장되어 있고,
Replica 인스턴스로 복제를 실행했다. 그 순간 Master에 <B, 200>이 저장하고, 누군가 Replica 인스턴스에서 key값을 조회하면,
A만 조호될 수도 있다.
복제 시 메모리가 부족하지 않은지 확인
Replicaof 명령 시 fork가 발생하는데 이는 전체 메모리 용량이 8GB인 서버에서 Master가 사용중인 메모리 용량이
5GB라고 하면, fork로 순간적인 메모리 사용량이 2배가 될 수 있다.
주의할 것은 fork를 하는 과정에서 메모리가 가득 차 있으면 복제가 안될 수도 있고, 서버가 터질 수도 있다.
즉, 메모리의 60 ~ 70%까지만 사용하는 것이 이상적이다.
strings VS hash
먼저 우리가 필요한 구조는 아래와 같다.
- 조회할 때 마다 조회수는 증가한다.
- Redis에 캐싱 처리를 하여 write back역할을 한다.
- 우리가 원하는 구조는 key field value의
hash
구조이다.
ex)key: posts:1, field:hits, value: 조회수 - 필드는 오직 hits 1개만 가진다.
strings가 아닌 hash를 선택한 이유
먼저 사용법을 살펴보면, 크게 차이가 없다. 우리는 hits라는 필드만 존재하게 때문이다.
hash의 hset으로 아래와 같이 개별로 사용할 수 있다.
hset posts:1 title "1번 포스트"
hset posts:1 hits 10
hset posts:2 title "2번 포스트"
hset posts:2 hits 20
...
strings의 set을 사용하면 아래와 같이 할 수 있다.
set posts:1:title "1번 포스트"
set posts:1:hits 10
set posts:2:title "2번 포스트"
set posts:2:hits 20
...
hmset을 사용하면, 한 번에 처리할 수 있다.
hset posts:1 title "1번 포스트" hits 10
hset posts:2 title "2번 포스트" hits 20
성능 이점이 장점이다.
해시는 매우 작은 공간에 인코딩이 되어서 가능하면 해시를 이용하는게 좋다고 한다.
Redis의 strings을 이용할 경우 더 많은 공간을 사용해야하고, 검색 속도도 해시에 비해 느리다고 한다.
@Test
void hash2() {
HashOperations hashOperations = redisTemplate.opsForHash();
String hashKey = "hits";
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 500_000; i++) {
hashOperations.put("posts:hits" + i, hashKey, i);
}
long stopTime = System.currentTimeMillis();
System.out.println("걸린 시간 = " + (stopTime - startTime) / 1000);
// 걸린 시간 = 537s
}
@Test
void strings() {
ValueOperations valueOperations = redisTemplate.opsForValue();
long startTime = System.currentTimeMillis();
for (int i = 1; i <= 500_000; i++) {
valueOperations.set("posts:hits" + i, i);
}
long stopTime = System.currentTimeMillis();
System.out.println("걸린 시간 = " + (stopTime - startTime) / 1000);
// 걸린 시간 = 540s
}
RedisRepository VS RedisTemplate
Spring Data Redis는 두 가지를 제공한다.
RedisRepository
Spring Data Redis를 이용하면 간단하게 Domain Entity를 Redis Hash로 만들 수 있다.
하지만 트랜잭션은 지원하지 않고, 다양한 자료구조를 선택할 수는 없어서 set, hash 구조를 사용해야 한다.
RedisTemplate
특정 Entity 이외에도 여러 자료구조 타입을 선택할 수 있고, 트랜잭션을 지원한다.
나에게 필요한 것은 hash 자료구조만 필요하기 때문에 RedisTemplate 사용.
RedisRepository는 set으로 key를 만들고 하위 key-value를 hash로 함.
참고
https://zangzangs.tistory.com/72
Redis 운영 이슈와 효율적 이용을 위한 redis.conf 설정
'기타 IT' 카테고리의 다른 글
RedisTemplate CastClassException(직렬화, 역직렬화 이슈) (0) | 2022.05.12 |
---|---|
Spring Data Redis에서 O(N) 명령어인 Keys를 Scan으로 대체하기 (0) | 2022.05.11 |
테스트 더블 테스트 VS 실제 객체 테스트(classicist vs mockist) (0) | 2022.04.15 |
SSH란? (0) | 2022.03.22 |
@ParameterizedTest를 언제 사용해야 할까? (0) | 2022.03.03 |