Spring

Spring Redis에 Transaction 적용하기

mysterious dev 2024. 2. 3. 21:17

Redis Transaction

레디스에서는 트랜잭션을 지원하기 위해 다음 명령어를 지원한다.

  • MULTI: 트랜잭선을 시작한다. 이후 들어오는 명령어는 곧바로 처리되지 않고 queue 쌓인다.
  • EXEC: 커밋과 같다. queue에 쌓인 명령어를 일괄적으로 실행한다.
  • DISCARD: 롤백과 같다. queue의 명령어를 일관적으로 버린다.
  • WATCH: Lock과 동일한 역할을 한다. (낙관적 락 방식)

 

 

 

 

 

 

프로젝트 설정

 

✔️ dependencies

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

 

 

 

✔️ application.yml

spring:
  data:
    redis:
      host: localhost
      port: 6379

 

 

 

 

 

 

Spring Redis에 트랜잭션 적용

아무런 설정을 하지 않는다면, 기본적으로는 트랜잭션 기능이 동작하지 않는다.

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}
@Getter
public class SampleObject {

    private Long id;
    private String name;

    public SampleObject(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}
@RequiredArgsConstructor
@Transactional
@Service
public class RedisTxService {

    private final RedisTemplate<String, SampleObject> redisTemplate;

    public void save(Long id, String name) {
        System.out.println("processing ...");
        redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
        if (name == "ex") {
            throw new RuntimeException("name is ex!!");
        }
    }
}

 

 

트랜잭션이 적용된다면 예외 발생 시 데이터가 모두 롤백되어야 한다.

@SpringBootTest
class RedisTxServiceTest {

    @Autowired
    private RedisTxService redisTxService;

    @Autowired
    private RedisTemplate<String, SampleObject> redisTemplate;

    @Test
    void 기본적으로_트랜잭션이_적용되지_않는다() {
        // when
        assertThatThrownBy(() -> {
            redisTxService.save(1L, "ex");
        }).hasMessage("name is ex!!");

        // then
        assertThat(redisTemplate.opsForValue().get("sample:"+1L))
                .isNotNull();
    }
}

 

위 테스트를 진행하면 성공하는 것을 알 수 있는데, 이를 통해 기본적으로 트랜잭션이 적용되지 않는다는 것을 알 수 있다.

 

 

 

 

 

 

✔️ 트랜잭션 설정

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

 

위와 같이 트랜잭션 옵션을 활성화시켜준 RedisTemplate을 등록하면 트랜잭션이 적용된다.

Redis 단독으로 사용하게 되면 @EnableTransactionManager를 추가로 달아주어야 하며,
PlatformTransactionMananer를 빈으로 등록해야 한다.

jpa를 사용중이라면 위 과정이 자동으로 진행되므로 RestTemplate만 등록하면 된다.

 

 

@SpringBootTest
class RedisTxServiceTest {

    @Autowired
    private RedisTxService redisTxService;

    @Autowired
    private RedisTemplate<String, SampleObject> redisTemplate;

    @Test
    void 기본적으로_트랜잭션이_적용되지_않는다() {
        // when
        assertThatThrownBy(() -> {
            redisTxService.save(1L, "ex");
        }).hasMessage("name is ex!!");

        // then
        assertThat(redisTemplate.opsForValue().get("sample:"+1L))
                .isNull();
    }
}

 

 

예외 발생 시 트랜잭션이 적용된다면 롤백되어야 하므로 isEmpty()로 바꿨다.

결과는 다음과 같이 성공하는 것을 알 수 있다.

 

 

 

 

 

 

 

✔️ @Transactional(readOnly = true) 사용 시

ReadOnly 트랜잭션에서도 데이터는 저장된다.

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class RedisTxService {

    private final RedisTemplate<String, SampleObject> redisTemplate;

    public void save(Long id, String name) {
        System.out.println("processing ...");
        redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
        if (name == "ex") {
            throw new RuntimeException("name is ex!!");
        }
    }
}
@SpringBootTest
class RedisTxServiceTest {

    @Autowired
    private RedisTxService redisTxService;

    @Autowired
    private RedisTemplate<String, SampleObject> redisTemplate;

    @Test
    void readOnly_여도_데이터는_저장된다() {
        // when
        redisTxService.save(1L, "name");

        // then
        assertThat(redisTemplate.opsForValue().get("sample:" + 1L))
                .isNotNull();
    }
}

 

트랜잭션이 readOnly인 경우 MULTI 자체를 사용하지 않는다.

 

 

 

 

 

 

 

✔️ TransactionTemplate 사용 시

TransactionTemplate을 사용해도 트랜잭션을 적용할 수 있다.

@RequiredArgsConstructor
@Service
public class RedisTxService {

    private final RedisTemplate<String, SampleObject> redisTemplate;
    private final TransactionTemplate transactionTemplate;

    public void save(Long id, String name) {
        transactionTemplate.executeWithoutResult((status -> {
            System.out.println("processing ...");
            redisTemplate.opsForValue().set("sample:" + id, new SampleObject(id, name));
            if (name == "ex") {
                throw new RuntimeException("name is ex!!");
            }
        }));
    }
}
@SpringBootTest
class RedisTxServiceTest {

    @Autowired
    private RedisTxService redisTxService;

    @Autowired
    private SampleObjectRepository sampleObjectRepository;

    @Test
    void transactionTemplate_을_통해서도_tx_적용_가능() {
		// when
        assertThatThrownBy(() -> {
            redisTxService.save(1L, "ex");
        }).hasMessage("name is ex!!");

        // then
        assertThat(redisTemplate.opsForValue().get("sample:"+1L))
                .isNull();
    }
}

 

 

 

 

 

 

 

주의점

Spring data redis에서 제공되는 RedisRepository에는 트랜잭션을 적용할 수 없다. 

(관련 이슈)

 

 

 

 

 

참고