[Spring] Redis 트랜잭션 적용하기

Hocaron·2024년 2월 6일
1

Spring

목록 보기
39/44

MySQL 에서도 트랜잭션을 사용하듯이 Redis 에서도 트랜잭션이 필요한 경우가 있을 것이다. Redis 에서는 어떻게 설정할 수 있는지 코드로 살펴보자.

Redis 트랜잭션 관리하는 명령어

트랜잭션 시작 및 실행

  • MULTI 명령을 사용하여 트랜잭션이 시작된다.
  • MULTI 다음에 실행되는 명령어가 바로 실행되는 대신 대기열에 들어간다.
  • EXEC 가 실행될 때, 한꺼번에 실행된다.
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

MULTI 가 실행되면 이후 명령어 실행결과는 "QUEUED"라는 문자열로 응답된다. 대기 중인 명령은 간단히 EXEC가 호출될 때 실행된다.

트랜잭션 시작 및 종료

  • DISCARD 가 실행되면 트랜잭션을 중단할 수 있다.
> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

DISCARD 가 실행되면 MULTI 이후에 쌓인 명령어가 실행되지 않고, 트랜잭션을 종료한다.

Redis 트랜잭션 중에 발생할 수 있는 2가지 Command 오류

1. EXEC 를 실행하기 이전에 Queue 에 적재하는 도중 실패하는 경우

  • Command 가 문법적으로 잘못된 경우 (wrong number of arguments, wrong command name) 또는 메모리 부족과 같은 경우에 발생할 수 있다
  • Command 의 응답 값으로 QUEUED 가 온 경우에는 성공적으로 처리되었다고 보면 된다. 그렇지 않은 경우에는 Error 를 응답 값으로 받게 된다.
  • Redis 2.6.5 버전부터는 Server 에서 명령이 누적되는 동안 오류가 있음을 기억한다. 이는 EXEC 실행시 Transaction 을 거부한다는 오류를 반환하고 자동으로 DISCARD 처리한다.
  • Redis 2.6.5 이전 버전은 오류 응답에 관계없이 EXEC 실행된 경우 Transaction 내의 성공적으로 처리할 수 있는 일부분의 Command 만 실행된다.

2. EXEC 를 실행한 이후에 실패하는 경우

  • 잘못된 명령어 (string value 에 list 에게만 실행할 수 있는 명령을 호출한 경우) 를 호출한 경우에 발생할 수 있다.
  • EXEC 이후 발생한 오류는 특별한 방법으로 처리되지 않습니다. 트랜잭션 중에 일부 명령이 실패하더라도 다른 모든 명령들이 실행됩니다.

Spring 에서 Redis 트랜잭션 적용하기

SessionCallback 사용

public ResponseEntity<?> transaction() {
    redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            operations.multi(); // transaction start
            operations.opsForValue().set("USER:1", "1");
            operations.opsForValue().set("USER:2", "2");
            return operations.exec(); // transaction end
        }
    });
    	return null;
}
  • 모든 트랜잭션이 필요한 로직에 mutli, exec 를 감싸는 방식으로 사용할 수는 없을 것이다.
  • 프록시 패턴, @Transactional 로직과 비슷하지 않은가?!

@Transactional 을 사용

@Configuration
public class RedisConfig {

  @Bean
  public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);
    redisTemplate.setEnableTransactionSupport(true); // redis Transaction On !
    return redisTemplate;
  }
@transactional
public ResponseEntity<?> transaction() {
    redisTemplate.set("USER:1", "1");
    redisTemplate.set("USER:2", "2");
    return null;
}
  • PlatformTransactionManager를 Bean으로 등록해야하는데, 이미 우리 프로젝트에서는 @Transactional 을 사용하고 있었기 때문에 추가로 등록해줄 필요없이, setEnableTransactionSupport 옵션만 추가하였다.

Redis 트랜잭션 적용시 주의점

  • EXEC 실행시에 큐에 넣어놓았던 명령어가 한번에 실행된다.
  • 즉, 트랜잭션 내부에서 get(key)를 통해 값을 가지고 오면, 데이터가 존재하지만 null 로 나온다.
  • 아래와 같이 트랜잭션 내부에서 값을 조회 후 사용하는 로직이 있고, 해당 결과값을 사용하는 로직이 있다면 트랜잭션 외부에서 값을 조회하는 것으로 로직 변경이 필요하다.
@transactional
public ResponseEntity<?> transaction() {
	var result =  (String) redisTemplate.opsForValue().get("USER:1"); 
	if (result != null) { // key 에 해당하는 데이터가 존재해도 결과값은 Null 로 나와서 이 로직은 타지 않는다.
		redisTemplate.opsForValue().set("USER:1", "2");
	}
	redisTemplate.opsForValue().set("USER:2", "2");
    return null;
}

번외

pipeline 과 mutli 차이점

pipeline 은 커맨드를 요청할 때마다, 서버로 전송하는 것이 아니라, 커맨드를 모아서 서버에 전송시킨다.
네트워크 오버해드를 줄이기 위한 목적으로 주로 사용된다. 하지만, 커맨드는 TCP 패킷으로 나뉘어 전송되어, 전부 전송되기 전에 오류가 발생한다면, 일부만 실행되고, 나머지는 실행되지 않는 현상이 발생한다.

정리

  • Redis 트랜잭션 내부에서 GET 하는 경우, Null 이 반환되므로 내부에서 결과값을 사용하는지 확인이 필요하다.

References

profile
기록을 통한 성장을

0개의 댓글