Redis @Transactional 적용

Seoyeon Kim·2024년 3월 27일
0

Redis Transaction 명령어를 @Transactional 을 사용하여 적용하고 싶을 때


설정 전 @Transactional 동작 확인하기

기존 leave()@Transactional 을 적용했을 때,

Redis monitor 결과는 다음과 같다.

트랜잭션을 시작하는 MULTI , 명령어를 실행하는 EXEC 이 수행되지 않았음을 확인할 수 있다.


설정하면서 @Transactional 동작 확인하기

RedisConfig.java

redisTemplate.setEnableTransactionSupport(true) 를 추가한다.

여기까지 설정하고 어플리케이션을 실행한 결과:

마찬가지로 트랜잭션 명령어가 수행되지 않는다.


RedisConfig.java

@EnableTransactionManagement 를 적용한다.

여기까지 설정하고 방 나가기 요청을 보낸 결과:

NoSuchBeanDefinitionException이 발생한다.


build.gradle / RedisConfig.java

Spring 공식 문서를 보면 'Transaction management는 PlatformTransactionManager를 필요로 하지만 Spring Data Redis는 PlatformTransactionManager의 구현체를 포함하고 있지 않다. 만약 어플리케이션이 JDBC를 사용하고 있다면 JDBC의 transaction manager를 사용할 수 있다.' 고 안내하고 있다.

즉, 다른 구현체를 빌려서 사용해야 한다는 의미이다.

Transaction management는 requires a PlatformTransactionManager. Spring Data Redis does not ship with a PlatformTransactionManager implementation. Assuming your application uses JDBC, Spring Data Redis can participate in transactions by using existing transaction managers.

JDBC를 의존성에 추가한다.

org.springframework.jdbc.datasource package 내의 DataSourceTransactionManager 인스턴스를 빈으로 등록한다.

여기까지 설정하고 어플리케이션을 실행한 결과:

BeanCreationException이 발생하며 dataSource 설정을 요구한다.


build.gradle / RedisConfig.java

임의의 데이터베이스 H2 의존성을 추가한다.

PlatformTransactionManager 빈에 DataSource를 주입한다.

여기까지 설정하고 방 나가기 요청을 보낸 결과:

MULTIEXEC 가 수행된 것을 확인할 수 있다.


명령어 알아보기

"HELLO" "3" "AUTH" "(redacted)" "(redacted)"
"CLIENT" "SETINFO" "lib-name" "Lettuce"
"CLIENT" "SETINFO" "lib-ver" "6.3.1.RELEASE/12e6995"

HELLO:

  • Redis 서버에 연결하고 버전 정보를 확인하는 명령어입니다.
  • "3"은 사용하는 Redis 프로토콜 버전을 나타냅니다.

AUTH:

  • Redis 서버에 비밀번호 인증을 수행하는 명령어입니다.
  • "(redacted)" 부분은 비밀번호 정보이므로 보안상의 이유로 가려져 있습니다.

CLIENT SETINFO:

  • Redis 클라이언트 정보를 설정하는 명령어입니다.
  • lib-namelib-ver 정보를 설정합니다.
  1. HELLO 명령어를 사용하여 Redis 서버에 연결하고 버전 정보를 확인합니다.
  2. AUTH 명령어를 사용하여 Redis 서버에 비밀번호 인증을 수행합니다.
  3. CLIENT SETINFO 명령어를 사용하여 Redis 클라이언트 정보를 설정합니다.
    • lib-name : Lettuce
    • lib-ver : 6.3.1.RELEASE/12e6995

위 명령어들은 Redis 서버에 연결하고 인증하며, Lettuce 클라이언트 라이브러리 정보를 설정하는 데 사용됩니다.

트랜잭션 명령어를 실행할 때만 HELLO 와 같이 연결을 설정하는 명령어가 출력된 이유:
대기 명령(blocking) (예, BLPOP), 트랜젹션 처리(MULTI/EXEC), 파이프라인(Pipeline) 등의 경우 새 연결을 맺어서 처리하고 종료(close)한다. 참고


1 "MULTI"
2 "HGETALL" "room:004029686c"
3 "HGETALL" "user:57be7ef8c9ed4643b915"
4 "SMEMBERS" "user:57be7ef8c9ed4643b915:idx"
5 "HGETALL" "room:004029686c"
6 "SMEMBERS" "room:004029686c:idx"
7 "DEL" "user:57be7ef8c9ed4643b915"
8 "SREM" "user" "57be7ef8c9ed4643b915"
9 "DEL" "user:57be7ef8c9ed4643b915:idx"
10 "DEL" "room:004029686c"
11 "SREM" "room" "004029686c"
12 "DEL" "room:004029686c:idx"
13 "EXEC"

MULTI:
트랜잭션을 시작하는 명령어입니다. 이후 실행되는 명령어들은 하나의 단위로 처리됩니다.

HGETALL:
해시 키에 저장된 모든 필드와 값을 가져옵니다.

SMEMBERS:
세트 키에 저장된 모든 멤버를 가져옵니다.

SREM "user" "57be7ef8c9ed4643b915":
user 세트에서 "57be7ef8c9ed4643b915" 멤버를 제거합니다.

DEL "user:57be7ef8c9ed4643b915:idx":
user:57be7ef8c9ed4643b915:idx 세트 키를 삭제합니다.

EXEC:
트랜잭션을 실행합니다. 이전에 큐에 쌓인 명령어들이 일괄적으로 실행됩니다.

3~6 line은 큐에 명령어를 쌓기 전에 수행한 명령어로
HGETALL user:57be7ef8c9ed4643b915 을 통해 객체를, SMEMBERS user:57be7ef8c9ed4643b915:idx 를 통해 객체에 걸린 인덱스들을 가져와서 명령어의 오류 여부를 판단하는 것 같습니다.

7~12 line은 큐에 쌓이게 되고 13번째 줄에서 EXEC 를 호출하면 실행된다.


참고 1

deleteById() 를 수행하면, 내부적으로 다음과 같은 단계를 거치게 된다.

org.springframework.data.redis.core.RedisKeyValueAdapter

connection.del(keyToDelete);
connection.sRem(binKeyspace, binId);
new IndexWriter(connection, converter).removeKeyFromIndexes(keyspace, binId);

따라서 다음과 같은 단계로 명령어가 수행되는 것이다.

7 "DEL" "user:57be7ef8c9ed4643b915"  // del
8 "SREM" "user" "57be7ef8c9ed4643b915"  // sRem
9 "DEL" "user:57be7ef8c9ed4643b915:idx"  // removeKeyFromIndexes

참고 2

다음과 같은 설정을 application.yml 에 추가하면 콘솔에서 로그를 확인할 수 있다.

logging:
  level:
    root: debug

연결에 성공한 뒤 MULTI 명령어를 dispatching 한다.

명령어를 stack에 쌓았다가 decode한 다음 수행한다. 그 결과로 OK를 응답 받았음을 확인할 수 있다.

이후에 HGETALL 명령어를 수행한다.
"Invoke 'hGetAll' on unbound connection"

이때, 연결 포트 번호가 달라진 것을 확인할 수 있다.

127.0.0.1:61049 → 127.0.0.1:61044

반면, 이후에 실행되는 DEL 명령어는 다시 127.0.0.1:61049 연결에서 동작한다.

"Invoke 'del' on bound connection"

이는 SREM 명령어도 마찬가지이다.
"Invoke 'sRem' on bound connection"

127.0.0.1:61049 연결에서 동작한다.

이를 통해 읽기 명령어와 쓰기 명령어가 다른 연결을 통해 수행되고 있음을 유추할 수 있다.

org.springframework.data.redis.core.RedisConnectionUtils 내부에 ConnectionSplittingInterceptor가 read-only 명령어라면 새로운 연결을 통하여 요청하는 것 같다.

이 곳에서 위에서 확인한 "on unbound" 또는 "on bound" 형태의 로그를 발행한다.

isPotentiallyThreadBoundCommand(commandToExecute) 의 결과가 false인 경우, RedisConnection connection = factory.getConnection() 을 통해 사용 가능한 다른 연결을 가져와서 invoke(method, connection, args) 를 수행하는 것을 확인할 수 있다.

private boolean isPotentiallyThreadBoundCommand(RedisCommand command) {
	return RedisCommand.UNKNOWN.equals(command) || !command.isReadonly();
}

마찬가지로 redis-cli의 monitor를 통해 명령어마다 포트 번호가 달라지는 것을 확인할 수 있다.


Reference

stack overflow- Redis is single-threaded, then how does it do concurrent I/O?

0개의 댓글