Redis란?
Remote Dictionay Server(Redis)의 약자로, 원격 Dictionary 자료구조 서버라는 직관적인 이름을 가진 레디스는 NoSQL 데이터베이스 중 가장 많이 사용되는 Key-Value 데이터베이스이다.
레디스는 메모리 기반의 In-Memory 데이터베이스이기 때문에, 하드디스크에 저장하는 RDMBS보다 처리속도가 훨씬 빠르다. Key로 가질 수 있는 자료형은 기본적으로 String이지만, Value에는 다양한 타입을 저장할 수 있게 지원한다.
Key-Value 형태에 메모리에 저장한다면 Java의 HashMap을 사용하지 왜 Redis를 사용하는지 의문이 들 수 있다.
이는 서버가 여러 대인 분산 환경에서를 살펴보면 이해할 수 있다. 여러 대의 서버에서 각자의 HashMap으로 데이터를 관리하면, 원격 프로세스간의 데이터를 동기화하기 위한 별도의 프로세스가 필요하다.
별도의 레디스 서버를 둔다면, 메모리 기반의 빠른 응답을 확보하면서도 여러 서버 간의 데이터 불일치 문제를 해결할 수 있다.
데이터베이스가 있음에도 Redis를 사용하는 이유는 캐시 서버로 사용하여 서비스의 속도를 올리고 데이터베이스의 부하를 줄이기 위해서 사용된다. 데이터베이스에서 데이터를 읽어온 후 Redis에 저장해두고, 같은 요청이 반복해서 들어오는 경우 캐시 서버에서 데이터를 찾아 바로 결과를 보내준다.
Redis의 특성
데이터 저장
Redis는 In-Memory 스토리지의 빠른 데이터 입출력을 제공한다. 또한 Key-Value 형태의 저장방식으로 Key의 등록과 조회에 O(1)의 성능을 보장한다.
더불어 Value에 여러 자료 구조를 가진 Collection을 지원하여, 저장하고자 하는 데이터의 형태에 맞게 효율적이고 빠르게 사용할 수 있도록 보장한다.
이러한 특성으로 인해 일반적으로 Redis는 캐시(cache)로 많이 사용되며, Redis의 영속성(Presistent) 기능을 사용해 데이터베이스로 사용되기도 한다.
스레드
Redis 4.0부터는 기본적으로 일반 명령어를 처리하는 메인 스레드 1개와 별도의 시스템 명령어들을 사용하는 전용 서브 스레드 3개로 동작한다. 실제 사용자가 사용하는 명령어들은 메인 스레드에서만 처리되니 싱글 스레드로 동작한다고 볼 수 있다.
Redis 6.0부터는 ThreadedIO가 추가되어 멀티 스레드를 지원하지만, I/O Socket read/write 시에 멀티 스레드로 동작하고 명령어를 실행하는 코어는 여전히 싱글 스레드로 동작한다.
Redis는 주요 기능들이 O(1)로 처리되고 상황에 맞춘 여러 Collection을 지원하기 때문에, atomic을 유지하는 목적으로 싱글 스레드에서 최고의 효율을 낼 수 있도록 설계한 것이다.
Redis 사용자는 싱글 스레드로 동작한다는 것을 항상 명심해서 사용해야한다. 수행이 오래 걸리는 명령을 피해야하며, 이런 long-term 명령이 수행되는 몇 가지 상황이 있다.
•
keys
◦
Redis에 존재하는 모든 key를 조회하기 때문에 시간이 오래 걸림
◦
scan을 통한 순회 탐색으로 해당 상황을 피할 수 있다.
•
smem
◦
set 자료구조에서 모든 member를 조회하기 때문에 시간이 오래 걸림
◦
sscan을 통한 순회 탐색으로 해당 상황을 피할 수 있다.
•
flushall
◦
전체 데이터를 지우는 명령어로, 키의 개수에 비례하여 시간이 소요된다.
분산 처리
저장된 데이터가 많아지면 전체적으로 성능이 저하되는 문제가 발생하기 때문에, 일정 규모를 넘어가면 대부분 시스템에서는 분산 처리를 사용한다. Redis에서도 데이터가 많아지는 상황을 대비해, Redis Cluster 기능을 제공하여 분산 처리를 용이하게 도와준다.
Redis Cluster를 통해 데이터를 여러 노드로 분산 시켜 사용하는 메모리를 분산할 수 있고, 장애 발생 시 다른 노드로 대체하여 운영하는 것이 가능하다. 기본적으로 Redis Cluster에 구성되어 있는 노드들은 hash slot이 16384개(0 ~ 16383)가 할당되어 있어, key를 해싱하여 맞는 hash slot을 찾아 분산하여 저장한다.
장애 복구
Redis는 Redis Sentinel을 통해 장애를 감지하고, Redis Replication을 통해 장애를 복구할 수 있도록 지원한다.
Master-Slave의 Replication을 구성하고, Sentinel이 master와 slave를 감시하다가 master에서 장애가 발생하면 slave를 master로 승격시켜 장애 상황을 복구한다.
Redis의 자료구조
Redis의 Key-Value 스토리지의 Value에는 단순한 Object가 아닌 Strings, Bitmaps, Lists, Hashes 등 다양한 자료구조를 가지고 있어, 적절한 자료구조를 사용해 필요에 맞게 효율적으로 데이터를 저장할 수 있다.
Strings
일반적인 문자열을 저장하는 자료구조로, 최대 512MB를 저장 가능하고 binary data나 JPEG 이미지를 저장 할 수 있고 HTML 매핑도 가능하다.
INCR과 DECR 등의 Value의 값을 1씩 증가하거나 감소하는 연산을 지원하기 때문에, 단순 증감연산이 잦은 데이터의 저장에 좋다.
Bitmaps
문자열을 저장하는 Strings 자료구조의 변형으로, Value에 bitmap을 저장하는 자료구조이다. Strings와 마찬가지로 512MB를 저장할 수 있기 때문에, bit를 2^32 bit까지 저장 가능하다.
Value의 값을 bit 연산이나 bit 개수 세기, 지정한 bit 위치 구하기 등의 연산을 지원하고, 저장 시에 저장 공간을 절약할 수 있다는 장점이 있다.
Lists
Linked-List 형태의 자료 구조로, 데이터를 순차적으로 저장한다. 메세지 queue로 사용하기에 적절하다.
Linked List의 특성을 따라 데이터의 맨 앞과 맨 뒤의 데이터 조작에는 O(1)의 속도를 가지지만, 중간의 임의의 index 값을 조회하거나 조작할 때는 O(N)의 속도를 가진다.
Hashes
key 하위에 field(subkey)-value 형태의 Hash table을 사용하는 자료구조로, 메모리가 허용하는한 제한 없이 field를 추가할 수 있다.
Hash의 특성을 따라 데이터의 추가, 조회, 삭제를 O(1)의 속도로 수행할 수 있다.
Sets
중복된 데이터를 담지 않는 집합(set) 자료 구조로, 유니크한 key 값만 저장하고 중복된 데이터를 저장하려하면 마지막에 저장된 값만 보관된다.
교집합, 합집합, 차집합, 여집합 등 여러 Set 간의 집합 연산을 빠른 속도로 수행할 수 있지만, 잘못된 명령어를 사용하면 set 내의 모든 데이터를 전부 가져오기 때문에 주의해야한다.
Sorted Sets
일반적으로 set은 정렬이 되어있지 않고 insert 한 순서대로 들어간다. 반면 sorted set은 set에 score라는 일종의 가중치 필드를 추가하여, member들의 순서를 score의 오름차순으로 관리한다.
특성은 set과 동일하게 여러 집합 연산을 빠르게 처리할 수 있다. 또한 value는 중복이 불가능하며 score의 경우에는 중복이 가능하고, score 값이 같으면 사전 순으로 정렬된다.
HyperLogLog
데이터의 저장과 원소 개수만 셀 수 있는 대용량 자료구조로, set과 비슷하게 동작하지만 저장되는 모든 데이터가 12KB로 고정되어 굉장히 많은 양의 데이터를 dump할 때 사용된다.
중복되지 않는 대용량의 데이터를 count 할 때 주로 사용되고, 저장 용량이 굉장히 작지만 저장된 데이터는 다시 확인할 수 없다. 이런 특성을 활용해 데이터를 보호하는데 사용되기도 한다.
Stream
append-only로 중간에 데이터가 바뀌지 않는 자료구조로, log를 저장하기에 가장 적합한 collection이다.
기존의 데이터 맨 뒤에 추가적으로 데이터를 저장하며, id 값을 기반으로 시간 범위를 검색하는 것이 가능하다.
Redis의 이벤트
Redis 2.8.0부터는 새로운 Key의 입력이나 변경 등의 이벤트가 발생할 때, 이를 알려주는 기능(Pub / Sub)이 있다.
이벤트의 종류
•
Key 입력 / 변경 : set 명령어 등으로 인해 Key나 Value의 값이 새로 추가되거나 수정될 때
•
Key 삭제 : del 명령어 등으로 Key가 삭제될 때
•
Key 만료 : 키 만료 시간 설정으로 Key가 삭제될 때
•
Key 퇴출 : MaxMemory 정책으로 인해 키가 퇴출(eviction)될 때
채널명 구성
•
키 중심: __keyspace@<dbid>__:key command
•
명령 중심: __keyevent@<dbid>__:command key
알림(Publish) 방식
•
키 중심: publish __keyspace@0__:key set
•
명령 중심: publish __keyevent@0__:set key
알림 받기
알림 받기는 psubscribe + 키 중심(keyspace) / 명령 중심(keyevent) 형태로 설정할 수 있다.
•
모든 Key 알림 받기 : psubscribe __keyspace@0__:*
•
tag로 시작하는 모든 Key 알림 받기 : psubscribe __keyspace@0__:tag*
•
특정 Key 알림 받기 : psubscribe __keyspace@0__:tag001
•
모든 쓰기 명령 알림 받기 : psubscribe __keyevent@0__:*
•
특정 명령 알림 받기 : psubscribe __keyevent@0__:set
이벤트 종류
•
K Keyspace events, publish prefix "__keyspace@<db>__:".
•
E Keyevent events, publish prefix "__keyevent@<db>__:".
•
g 공통 명령: del, expire, rename, ...
•
$ 스트링(String) 명령
•
l 리스트(List) 명령
•
s 셋(Set) 명령
•
h 해시(Hash) 명령
•
z 소트 셋(Sorted set) 명령
•
x 만료(Expired) 이벤트 (키가 만료될 때마다 생성되는 이벤트)
•
e 퇴출(Evicted) 이벤트 (최대메모리 정책으로 키가 삭제될 때 생성되는 이벤트)
•
A 모든 이벤트(g$lshzxe), "AKE"로 지정하면 모든 이벤트를 받는다.
이벤트 사용 예시
•
notify-keyspace-events "KEA": 모든 이벤트를 발생
•
notify-keyspace-events "Kg": 키 이벤트 + 공통 명령 발생
•
notify-keyspace-events "Kx": 키 이벤트 + 만료 이벤트 발생
•
notify-keyspace-events "": 이벤트 제거
Redis in Cabi
의존성 주입
@Autowired
public LentRedis(RedisTemplate<String, Object> valueHashRedisTemplate,
RedisTemplate<String, String> valueRedisTemplate,
RedisTemplate<String, String> shadowKeyRedisTemplate,
RedisTemplate<String, String> previousUserRedisTemplate,
CabinetProperties cabinetProperties) {
this.userRedisTemplate = valueRedisTemplate.opsForValue();
this.shareCabinetRedisTemplate = valueHashRedisTemplate.opsForHash();
this.shadowKeyRedisTemplate = shadowKeyRedisTemplate;
this.previousUserRedisTemplate = previousUserRedisTemplate.opsForValue();
this.cabinetProperties = cabinetProperties;
}
Java
복사
Java에서의 Redis Template
공유 사물함 참여 세션
공유 사물함마다 어떤 유저가 참여 시도를 했고, 참여를 시도한 유저 중에는 몇 번 시도했는지에 대한 정보를 저장하는 템플릿이다.
Hash collection을 사용하여, 공유 사물함의 cabinetId를 Key로 사용하여 사물함마다 subkey(userId) - value(string)의 Hash table을 생성한다.
초대 코드를 맞게 입력하여 유저가 해당 사물함에 참여한 경우에는 value로 저장하는 문자열에 entered를 저장하고, 참여 시도했지만 입장 코드를 틀린 유저는 해당 유저의 시도 횟수를 문자열로 저장한다.
유저 세션
유저가 사용하고 있는 사물함에 대한 정보를 저장하는 템플릿이다.
Strings collection을 사용하여, key(userId) - value(cabinetId) 구조로 유저마다 사용하고 있는 사물함의 cabinetId를 저장한다.
공유 사물함의 초대 세션이 유지되는 동안, 유저가 사물함에 참여할 때 데이터를 저장해두어 userId만으로 해당 유저가 어떤 공유 사물함에 참여하고 있는지 바로 찾을 수 있게 한다.
공유 사물함 초대 코드 세션
공유 사물함에 참여를 시도하는 유저가 있을 때, 1000~9999 사이의 랜덤하게 생성된 초대 코드를 저장해두는 템플릿이다.
Strings collection을 사용하여, key(cabinetId) - value(share code) 구조로 공유 사물함마다 초대 코드를 저장해둔다.
해당 공유 사물함에 참여하고자 하는 경우에는, 유저가 입력한 초대코드를 받아와 저장된 코드와 비교하여 일치 여부를 확인한다.
이전 대여자 캐싱
사물함의 바로 직전에 사용한 유저의 이름을 저장해두는 템플릿이다.
Strings collection을 사용하여, key(cabinetId) - value(username) 구조로 사물함마다 직전 사용자의 이름을 저장해둔다.
사물함의 직전 사용자 이름의 경우 필요한 정보는 간단하지만, 해당 정보를 DB에서 꺼내려면 아래와 같은 과정이 필요하다.
1.
LentHistory에서 cabinetId로 조회
2.
조회해온 LentHistory들 중 현재 대여하고 있는 대여 기록을 제외한, 가장 최근의 대여 기록 찾기
3.
찾은 대여기록의 userId를 통해 User에서 조회
4.
조회해온 user의 값에서 name 부분만 반환
이처럼 필요에 비해 성능적 부하나 절차가 복잡하기 때문에, Redis를 통해 사물함마다 직전 사용자의 이름만 캐싱해두고 사용한다.
공유 사물함 세션 만료
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
/**
* 내장 혹은 외부의 Redis를 연결
*/
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
/**
* RedisConnection에서 넘겨준 byte 값 객체 직렬화
*/
@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
Java
복사
이처럼 RedisMessageListenerContainer, RedisConnectionFactory, RedisTemplate을 빈으로 등록한 후
@Component
public class ExpirationListener extends KeyExpirationEventMessageListener {
private final LentService lentService;
public ExpirationListener(
@Qualifier("redisMessageListenerContainer")
RedisMessageListenerContainer listenerContainer,
LentService lentService) {
super(listenerContainer);
this.lentService = lentService;
}
@Override
public void onMessage(Message message, byte[] pattern) {
String cabinetIdString = message.toString().split(":")[0];
lentService.handleLentFromRedisExpired(cabinetIdString);
}
}
Java
복사
이와 같이 만료 이벤트에 대한 Listener를 등록해둔다. ExpirationListener가 구현하는 KeyExpirationEventMessageListener의 경우에는,
내부적으로 이와 같이 만료 이벤트에 대해 EventListener를 등록한다.
이와 같이 공유 사물함 초대 코드를 처음 생성할 때 expire를 통해 만료 시간을 설정하고,
Key가 시간에 따라 만료되면, 만료에 대한 로직을 처리한다.