스프링 부트 Redis Key Event Notification 처리 방법
'redis keyspace notification'은 redis 2.8.0부터 지원된 기능인데요.
해당 포스팅은 스프링 부트에서 redis key expired event를 수신하는 방법에 대해 정리한 내용입니다.
우선 redis.conf 설정 파일을 살펴보면 notify-keyspace-events 옵션을 통해 redis에서 발생하는 이벤트에 대한 알림 설정을 할 수 있는데요.
redis 이벤트 알림 기능의 경우 성능적 오버헤드가 발생할 수 있기 때문에 기본적으로 사용되지 않으며, 사용하지 않을 때 다음과 같이 ""으로 설정되어 있습니다.
기본적으로 K 또는 E 중 하나를 설정해야 알림이 발송되며, 해당 부분에 대해 조금 더 자세한 내용은 포스팅 하단에 관련 자료 링크를 참고하시면 좋을 것 같습니다.
(K = Keyspace events, E = Keyevent events)
***
하지만 해당 기능을 구현해 보는 과정에서 notify-keyspace-events 값이 이벤트에 대한 알림을 사용하지 않는 ""로 설정되어 있어도 spring boot에서 이벤트 알림이 수신되는 것을 확인할 수 있었는데요.
이유는 이어지는 KeyExpirationEventMessageListener 관련 내용을 통해 살펴보겠습니다.
1. KeyExpirationEventMessageListener
첫 번째는 스프링 부트 자체적으로 redis key expired 이벤트에 대한 알림을 수신할 수 있게 구현된 'KeyExpirationEventMessageListener'를 상속한 클래스를 통한 방법입니다.
@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
/**
* Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
*
* @param listenerContainer must not be {@literal null}.
*/
public RedisKeyExpiredListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
*
* @param message redis key
* @param pattern __keyevent@*__:expired
*/
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("########## onMessage pattern " + new String(pattern) + " | " + message.toString());
}
}
(RedisKeyExpiredListener class)
@Configuration
public class RedisConfig {
... //RedisTemplate 관련 코드 생략
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
return redisMessageListenerContainer;
}
}
(RedisConfig class)
KeyExpirationEventMessageListener 클래스를 상속받은 RedisKeyExpiredListener라는 클래스를 구현하였는데요.
해당 클래스는 생성자에서 RedisMessageListenerContainer를 인자로 필요로 하기 때문에 RedisConfig 클래스에서 해당 클래스를 위 예시 코드와 같이 bean으로 등록해 주는 과정이 함께 필요합니다.
//testKey라는 key가 expired 되었을 때 onMessage() method를 통해 출력된 결과
########## onMessage pattern __keyevent@*__:expired | testKey
(테스트 결과)
그리고 testKey라는 key의 ttl이 만료되었을 때 onMessage() 메서드를 통해 다음과 같은 결과가 출력되는 것을 확인할 수 있었는데요.
***
notify-keyspace-event ""
현재 redis.conf 설정 파일의 해당 값은 이벤트 알림을 사용하지 않도록 설정되어 있습니다. 하지만 테스트에서는 redis event notification이 발생하고, 수신되는 것을 볼 수 있는데요.
이렇게 이벤트 알림이 발생하는 원리를 찾아보기 위해 코드를 조금 더 살펴보았습니다.
먼저 KeyExpirationEventMessageListener 클래스는 KeyspaceEventMessageListener 추상 클래스를 상속받는데요.
(위 이미지는 해당 클래스 코드 중 일부만 가져온 것입니다.)
KeyspaceEventMessageListener 클래스의 init() 메서드를 살펴보면, 연결된 redis에서 notify-keyspace-events properties를 가져와 hasText() 메서드를 통해 값을 확인한 뒤 notify-keyspace-events의 값을 "EA"로 set 해주는 것을 볼 수 있었습니다.
(E = Keyevent events로 설정되어 있기 때문에 Keyspace events에 대한 이벤트 알림은 발생하지 않고, 때문에 수신할 수도 없습니다.)
결론적으로 해당 클래스가 빈으로 등록되며 redis 이벤트 알림 기능을 활성화하는 것인데요. 때문에 redis를 종료했다가 다시 킬 경우 notify-keyspace-events 값은 다시 ""가 됩니다.
* KeyspaceEventMessageListener 클래스는 InitializingBean 인터페이스를 상속받기 때문에 afterPropertiesSet() 메서드를 구현하고 있으며, 해당 메서드를 통해 init() 메서드가 실행됩니다.
2. MessageListener
두 번째 방법은 'MessageListener Interface'를 상속하는 클래스를 통해 이벤트 알림을 수신하는 방법입니다.
@Component
public class EventListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
System.out.println("########## onMessage pattern " + new String(pattern) + " | " + message.toString());
}
}
(EventListener class)
@Configuration
public class RedisConfig {
... //RedisTemplate 관련 코드 생략
private final String EXPIRED_EVENT_PATTERN = "__keyevent@*__:expired";
private final String EXPIRED_SPACE_PATTERN = "__keyspace@*__:expired";
private final String SET_EVENT_PATTERN = "__keyevent@*__:set";
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
EventListener eventListener) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(eventListener, new PatternTopic(SET_EVENT_PATTERN));
redisMessageListenerContainer.addMessageListener(eventListener, new PatternTopic(EXPIRED_EVENT_PATTERN));
redisMessageListenerContainer.addMessageListener(eventListener, new PatternTopic(EXPIRED_SPACE_PATTERN));
return redisMessageListenerContainer;
}
}
(RedisConfig class)
MessageListener Interface를 상속하는 EventListener라는 클래스를 구현하였으며, onMessage() method의 내용은 위에서 했던 테스트때와 같습니다.
이어서 RedisConfig 클래스에서 다음과 같이 RedisMessageListenerContainer에 Pattern이 포함된 MessageListener를 등록하게 되는데요.
//testKey라는 Key를 set하고 expired 되었을 때 onMessage() method를 통해 출력된 결과
########## onMessage pattern __keyevent@*__:set | testKey
########## onMessage pattern __keyevent@*__:expired | testKey
(테스트 결과)
testKey라는 key를 가진 값을 redis에 set 하며 출력된 메시지와 해당 데이터의 ttl이 만료되며 출력된 메시지입니다.
* 위 예시 코드에서 EXPIRED_SPACE_PATTERN에 대한 이벤트가 수신이 안된 이유는 현재 redis에 설정된 이벤트 알림 옵션 값이 "EA", 즉 keyevent에 대한 알림만 발송하도록 되어 있기 때문입니다. keyspace에 대한 알림을 수신하고 싶은 경우 redis의 해당 옵션 값에 K도 함께 추가하면 됩니다.
***
이 방식에서의 주의할 점으로는 KeyExpirationEventMessageListener 방식의 경우 내부적으로 레디스 설정에 notify-keyspace-events 값을 지정해 주는 과정이 있었지만, 여기서는 없다는 것인데요.
때문에 이 방식으로 이벤트 알림을 수신하고자 하는 경우 레디스 설정(redis.conf) 파일에 수동으로 notify-keyspace-events 값을 지정해 주는 과정이 필요합니다.
***
레디스 key expired events를 수신하는 기능 구현 시(1번, 2번 방법 동일) 꼭 알아두어야 하는 부분으로는 expired 이벤트는 redis 서버가 키를 삭제할 때 생성되기 때문에 TTL(time to live)가 0에 도달하는 시간과 완전히 동일하지 않다는 것인데요.
ttl이 설정된 키가 많은 경우 ttl이 0에 도달하는 시간과 이벤트가 생성되는 시간 사이에 상당한 지연이 있을 수 있다고 합니다.
(ttl이 설정된 키가 삭제되는 프로세스는 백그라운드에서 순차적으로 만료된 키를 찾는 시스템을 통해 삭제되거나, 특정 키에 대해 접근이 발생하고 만료된 사실이 발견된 경우 삭제됩니다.)
< 관련 자료 >
2023.06.22 - [Programming/Error] - @RedisHash keyspace 사용 시 주의할 점 (RedisKeyValueAdapter)
< 참고 자료 >
https://redis.io/docs/manual/keyspace-notifications/
https://moonsiri.tistory.com/87
https://backtony.github.io/spring/redis/2021-09-25-spring-redis-3/
'Programming > Spring Boot' 카테고리의 다른 글
spring boot 로그 파일 남기는 방법 (logback-spring.xml 설정) (0) | 2023.08.20 |
---|---|
(spring boot) XSS 공격 개념과 대응 방법 정리 (0) | 2023.08.12 |
@AutoConfiguration을 사용하여 라이브러리 만들어보기 (0) | 2023.07.27 |
Spring Boot 자동 구성 AutoConfiguration 동작 원리 파헤치기 (0) | 2023.07.21 |
@Conditional 어노테이션의 기능과 확장(@ConditionalOnXxx) (0) | 2023.07.18 |