Redis 트랜잭션으로 동시성 제어 내가 해냄

serotonins·2024년 5월 5일

✒️ Redis

✏️ 개요

단일 스레드로 동작........... 설명은 후첨하겠어요

✏️ 사용 배경

Redis 채택 이유
인메모리 DB로 속도가 빠르다 == 실시간성이 중요한 기능이어서 중요
키마다 TTL 설정이 가능해 응답이 일정 시간 오지 않는 경우 제어가 편함
watch, multi, exec, unwatch 같은 트랜잭션 기능이 잘 되어있어 동시성 제어하기 좋음

✏️ Lettuce vs Jedis vs Redisson

Redis 클라이언트 라이브러리
애플리케이션에서 Redis 데이터베이스와 통신(을 추상화하고 관리)하기 위해 사용되는 라이브러리.
스프링 부트에서 Redis를 사용하기 위해서는 이 Redis 클라이언트 라이브러리가 필요하다.

  • 연결 관리: 애플리케이션과 Redis 데이터베이스 간의 연결을 생성, 관리, 풀링
  • 명령 실행: Redis 데이터베이스에 대한 다양한 명령(GET, SET, INCR 등)을 실행
  • 데이터 직렬화/역직렬화: 애플리케이션 데이터를 Redis 데이터 형식으로 변환하고 반대로 변환
  • 고급 기능 지원: 트랜잭션, 파이프라이닝, 클러스터링 등의 기능을 제공

Redis의 Java 클라이언트 라이브러리 중에는 Lettuce, Jedis, Redisson이 있는데 가장 많이 추천되는 건 Lettuce였다.

📒 Lettuce

  • 스프링 부트기본적으로 제공하는 Redis 클라이언트 라이브러리
  • 비동기 방식도 지원하며, 멀티스레드 환경에서 안전하게 사용 가능
  • 연결 풀링, 클러스터 지원, 트랜잭션 등의 다양한 기능을 제공
  • spin lock 구조로 구현됨

📒 Jedis

  • Java용 Redis 클라이언트 라이브러리 중 하나
  • 동기 방식으로 작동 -> 멀티스레드 환경에서는 주의 필요
  • 사용하기 쉽고 가벼움

📒 Redisson

  • 고급 API와 기능을 제공하는 Redis 클라이언트 라이브러리
  • 분산 객체, 분산 컬렉션, 분산 락 등의 기능을 제공하여 분산 애플리케이션 개발에 유용
  • 비동기(RxJava API) 방식도 지원, 멀티스레드 환경에서도 안전
  • 스프링 부트와 잘 통합되며, 손쉽게 사용 가능
  • pub-sub 구조로 구현됨


분석 결과로, 스프링 부트가 기본적으로 제공하여 자동 구성 기능을 활용해 간단하게 설정이 가능하며 비동기 처리가 가능한 Lettuce가 가장 좋은 선택지였다.


✒️ 기초 세팅

✏️ Docker로 Redis 설치

📒 1. Redis 이미지 다운로드 및 컨테이너 실행

git bash에다가 아래 명령어를 실행합시다
docker run --name redis -p 6379:6379 -d redis
  • docker run: Docker 이미지를 기반으로 새로운 컨테이너를 생성하고 실행하는 명령어입니다.
  • -name redis: 생성될 컨테이너의 이름을 "redis"로 지정합니다. 이 이름을 사용하여 컨테이너를 관리할 수 있습니다.
  • p 6379:6379: 포트 매핑을 설정합니다. 호스트의 6379 포트와 컨테이너의 6379 포트를 연결합니다. 이를 통해 외부에서 컨테이너의 Redis 서버에 접근할 수 있습니다
  • d redis: "redis" Docker 이미지를 사용하여 컨테이너를 백그라운드 모드로 실행합니다. redis 이미지가 로컬 시스템에 없다면, Docker Hub에서 자동으로 다운로드 받습니다.

📒 2. Redis 컨테이너 확인

docker ps
  • docker ps: 현재 실행 중인 모든 Docker 컨테이너를 나열합니다. Redis 컨테이너가 정상적으로 실행되고 있는지 확인할 수 있습니다.

✏️ build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis:2.6.3'

의존성을 추가해주자.
위는 Redis 클라이언트 라이브러리(Lettuce 또는 Jedis)를 추가하고, 스프링 데이터 Redis 모듈을 사용할 수 있게 하는 것
아래는 스프링 세션 관리 기능을 제공하여, 세션 데이터를 Redis에 저장하여 분산 환경에서도 세션 정보를 공유할 수 있게 하는 것(분산 환경에서 세션 관리 효과적으로 할 수 있게 함)

✏️ RedisConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration // 이 파일이 configuration 담당한다는 걸 알려주기용
public class RedisConfig {

	// 2. RedisConnectionFactory 빈을 정의해 Lettuce 클라이언트를 탑재시킨 뒤
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }


	// 1. 외부에서 얘를 호출하면
    // 3. 키-밸류 값에 맞는 직렬화 전략을 탑재시킨 RedisTemplate를 반환
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return redisTemplate;
    }
}

✏️ service에서 사용

@Service
@RequiredArgsConstructor
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    public void setData(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public Object getData(String key) {
        return redisTemplate.opsForValue().get(key);
    }

}

✒️ 동시성 제어

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.ssafy.fluffitbattle.entity.Battle;
import com.ssafy.fluffitbattle.service.RedisKeyExpirationListener;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;

import java.time.LocalDateTime;


@Configuration // 얘도 나름 Config니까 달아주기
@RequiredArgsConstructor // final 어쩌구들에 생성자 수동으로 타이핑하고 싶지 않다면 달아주기
public class RedisConfig {

    // Spring Cloud Config 사용하면 여기서 받아온다. (그냥 Environment Variable들을 git에 올렸다고 생각하면 편하다.)
    // git에서 계속 변경되는 프로퍼티를 동적으로 받아오려면 이렇게!
    private final Environment env; 
    
/*    프로퍼티 값을 동적으로 받아올 필요가 없으면 이렇게!
//
//    @Value("${spring.data.redis.host}")
//    private String host;
//
//    @Value("${spring.data.redis.port}")
//    private String port;
//
//    @Value("${spring.data.redis.password}")
//    private String password;
*/
    
    private final int BASIC_DATABASE = 0;
    
    @Bean
    @Primary
    public LettuceConnectionFactory redisConnectionFactory() {
        return createLettuceConnectionFactory(BASIC_DATABASE);  // Default connection factory uses database 0
    }
    
    private LettuceConnectionFactory createLettuceConnectionFactory(int database) {
        String host = env.getProperty("spring.data.redis.host");
        String port = env.getProperty("spring.data.redis.port");
        String password = env.getProperty("spring.data.redis.password");

        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(host);
        config.setPort(Integer.parseInt(port));
        config.setPassword(password);
        config.setDatabase(database);
        return new LettuceConnectionFactory(config);
    }
    
    @Primary
    @Bean(name = "stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate() {
        return new StringRedisTemplate(redisConnectionFactory());
    }
}

✏️ 데이터베이스 여러개

✏️ Redis Template

RedisTemplate은 Spring Data Redis에서 제공하는 클래스로, Redis 서버와 상호작용하기 위한 고수준의 추상화를 제공한다. 주로 데이터의 시리얼라이제이션, 연결 설정, Redis 명령어 실행 등의 작업을 수행한다.

주요 역할

  • 시리얼라이제이션: Redis에 저장하거나 Redis에서 읽어올 때 데이터를 시리얼라이즈(직렬화)하고 디시리얼라이즈(역직렬화)하는 기능을 제공
  • 연결 관리: Redis 서버와의 연결을 관리합니다.
  • 명령어 실행: Redis 명령어를 실행할 수 있는 다양한 메서드를 제공합니다.

RedisOperations

RedisTemplate이 구현하는 인터페이스로, Redis와 상호작용하는 다양한 메서드를 정의하고 있으며, 트랜잭션과 관련된 메서드(watch, multi, exec, discard)도 포함된다.

왜인지 모르겠는데 <String, String> 으로 여러개 짜놓으면 빈 주입이 제대로 안 됐어요
그때 무슨 오류가 났었는지 지금 나에게 알 방법이 있을까요?

✏️ 동시성 제어

0개의 댓글