본문 바로가기
Lecture

Spring webflux - R2DBC, Redis

by Renechoi 2023. 10. 21.

R2DBC

DB 동기처리의 문제

비동기 어플리케이션에서는 처리 절차 중간에 동기가 포함되면 병목 -> 성능 저하

2번 부분까지 보았을 3번 db 커넥션 부분에서 jdbc 혹은 orm을 사용하면, 1번 2번이 비동기로 처리하더라도 3번을 동기로 처리하면 3번 응답이 올 때까지 해당 스레드는 4번까지 처리를 못하고 기다리게 된다.

즉, 처리 흐름에서 하나라도 동기가 있게 되면 전체적인 성능에 문제

이러한 문제를 해결하기 위해 나온 것이 R2DBC

R2DBC

  • 데이터베이스 async 처리
  • reactice stream
  • nonblocking
  • 라이브러리지만 일종의 오픈 스택 -> 벤더 사에서 제공하는 것이 -> 일반적 SPI(Service Provider Interface)
  • Spring Webflux 생태계와 어울림

implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
  • spring.io 에서 제공 안되는 것들도 있기 때문에 오픈소스 라이브러리를 체크해서 반영해야 함

Spring Mvc vs. Spring Webflux

호환이 되는 것을 찾아야 함

둘간의 전환 작업은 간단하지 않다 -> 바꾸게 되면 전부 바꿔야 함

공식 문서

엔티티 설정,
레포지토리 구현,
애노테이션을 사용하여 설정,

-> 스프링 repository와 유사함
-> 쿼리 메서드 예시

예시 코드

@Component
@Slf4j
@RequiredArgsConstructor
@EnableR2dbcRepositories
@EnableR2dbcAuditing
public class R2dbcConfig implements ApplicationListener<ApplicationReadyEvent> {
    private final DatabaseClient databaseClient;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // reactor: publisher, subscriber
        databaseClient.sql("SELECT 1").fetch().one()
                .subscribe(
                        success -> {
                            log.info("Initialize r2dbc database connection.");
                        },
                        error -> {
                            log.error("Failed to initialize r2dbc database connection.");
                            SpringApplication.exit(event.getApplicationContext(), () -> -110);
                        }
                );
    }
}
public interface UserR2dbcRepository extends ReactiveCrudRepository<User, Long> {
    Flux<User> findByName(String name);
    Flux<User> findByNameOrderByIdDesc(String name);

    @Modifying
    @Query("DELETE FROM users WHERE name = :name")
    Mono<Void> deleteByName(String name);
}

Redis

Reactvice Redis

-> 레딩스에 비동기, nonblocking 처리를 위한 라이브러리
-> 레디스 서버와 비동기적인 상호작용을 위해 Redis 서버와 비동기적으로 연결

  • 필요성?
  • R2DBC와 마찬가지로 전체 요청의 비동기처리 중에 Redis를 동기적으로 처리하면, 이 과정에서 전체 흐름의 병목이 가능하다.
  • 대용량 트래픽에서 Webflux를 사용하는 환경이라면 Reactive Redis를 사용할 가능성이 매우 많다.
  • Spring Data Reactive Redis -> Lettuce
  • Lettuce에서 Netty와 함께 비동기적 처리가 가능하다.
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

예시 코드


@Configuration
@RequiredArgsConstructor
@Slf4j
public class RedisConfig implements ApplicationListener<ApplicationReadyEvent> {
    private final ReactiveRedisTemplate<String, String> reactiveRedisTemplate;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        reactiveRedisTemplate.opsForValue().get("1")
                .doOnSuccess(i -> log.info("Initialize to redis connection"))
                .doOnError((err) -> log.error("Failed to initialize redis connection: {}", err.getMessage()))
                .subscribe();
    }

    @Bean
    public ReactiveRedisTemplate<String, User> reactiveRedisUserTemplate(ReactiveRedisConnectionFactory connectionFactory) {
        var objectMapper = new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .registerModule(new JavaTimeModule())
                .disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);

        Jackson2JsonRedisSerializer<User> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, User.class);

        RedisSerializationContext<String, User> serializationContext = RedisSerializationContext
                .<String, User>newSerializationContext()
                .key(RedisSerializer.string())
                .value(jsonSerializer)
                .hashKey(RedisSerializer.string())
                .hashValue(jsonSerializer)
                .build();

        return new ReactiveRedisTemplate<>(connectionFactory, serializationContext);
    }

}

Redis에서 먼저 조회하고 없으면 영속성 레벨에서 가져오는 코드

1. 레디스로부터 저장된 키 값 확인
2. 있으면 리턴
3. 없으면 안의 로직을 탐 -> db에서 가져오고 레디스에 세팅하고 다시 모노로 감싸서 전달 
    public Mono<User> findById(Long id) {
        return reactiveRedisTemplate.opsForValue()
                .get(getUserCacheKey(id))
                .switchIfEmpty(userR2dbcRepository.findById(id)
                        .flatMap(u -> reactiveRedisTemplate.opsForValue()
                                .set(getUserCacheKey(id), u, Duration.ofSeconds(30))
                                .then(Mono.just(u)))
                );
    }

delete와 update 로직도 레디스에 반영하기


    public Mono<Void> deleteById(Long id) {
        return userR2dbcRepository.deleteById(id)
                .then(reactiveRedisTemplate.unlink(getUserCacheKey(id)))
                .then(Mono.empty());
    }

    public Mono<User> update(Long id, String name, String email) {
        return userR2dbcRepository.findById(id)
                .flatMap(u -> {
                    u.setName(name);
                    u.setEmail(email);
                    return userR2dbcRepository.save(u);
                })
                .flatMap(u -> reactiveRedisTemplate.unlink(getUserCacheKey(id)).then(Mono.just(u)));
    }

reference :

반응형