2024.08.13.화.TIL 내일배움캠프 83일차 <최종프로젝트Day19>

김기남·2024년 8월 13일
post-thumbnail

안녕하세요, 오늘은 최종프로젝트를 진행하면서 동시성제어에 대해 학습한 내용을 정리해보았습니다.

분산 락 (Distributed Lock)

  1. 분산락이란 여러 서버가(프로세스) 공유 데이터를 제어하기 위한 기술 입니다.
  2. 락을 획득한 프로세스 혹은 스레드만이 공유 자원 혹은 Critical Section 에 접근할 수 있도록 하는 것 입니다.
  3. 분산락의 장점은 서버 분산 환경에서도 프로세스들의 원자적 연산이 가능한 것 입니다.(아래 그림처럼요.)

즉, 공유자원 자체에 락을 거는 것이 아니라, 어떤 행위가 발생하는 임계구역(Critical Section)에 락을 거는 방식이고 주로 Redis를 활용하기에 그냥 분산 락 → Redis 쓰겠다 라고 생각해도 무방합니다.

(물론 분산 락을 MySQL로도 구현이 가능하나 Redis에 비해 성능이 크게 떨어집니다.)

  1. Redis 락이 가능한 이유
    1. Memory DB
      1. 디스크를 사용하는 RDBMS 등의 DB보다 최소 수만배, 혹은 수십만배 이상 빠릅니다.
    2. 싱글 스레드 (Atomic)
      1. 일반적으로 DB는 멀티 스레드!

종류

  • Lettuce
    • spring-data-redis의 기본 구현체
    • 기본적으로 Spin Lock을 사용한다.
      • 이는 Lock을 대기하는 상황에서, Lock을 획득할 수 있는지 계속 요청을 보낸다.
      • 따라서 Lock을 획득하려는 스레드가 많을 경우 Redis에 부하가 집중된다.
    • Lock에 대한 타임아웃이 없어, Unlock(잠금 해제) 호출을 하지 못한 경우 Dead Lock을 유발할 수 있다.
  • Redisson
    • pub/sub 방식을 사용한다.
      • Lock을 당장 획득할 수 없으면 대기한다.
      • Lock이 획득 가능할 경우 Redis에서 클라이언트로 획득 가능함을 알린다.
    • Lock의 lease time 설정이 가능하다.
      • 즉, 설정된 lease time이 지난 경우 자동으로 Lock의 소유권을 회수하여 Dead Lock을 방지한다.

번외) RedisTemplate

  • Lock을 사용하지 않고, redis 의 싱글 스레드 특성을 이용한 동시성 제어

분산 락 예제 (Redisson)

build.gradle


// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.27.1'

Counter Service

package org.example.concurrency;

import lombok.RequiredArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class CounterService {

    private final CounterRepository counterRepository;
    private final RedissonClient redissonClient;

    private static final String LOCK_KEY = "counterLock";

    @Transactional
    public void save(Counter counter) {
        counterRepository.save(counter);
    }

    @Transactional
    public void reset() {
        Counter counter = counterRepository.findById(1L).orElseThrow();
        counter.setCount(100);
        counterRepository.save(counter);
    }

    @Transactional
    public void decreaseCount() {
        Counter counter = counterRepository.findById(1L).orElseThrow();
        counter.setCount(counter.getCount() - 1);
        counterRepository.save(counter);
    }

    public void decreaseCountUsingLock() {
        RLock lock = redissonClient.getFairLock(LOCK_KEY);
        try {
            boolean isLocked = lock.tryLock(10, 60, TimeUnit.SECONDS);
            if (isLocked) {
                try {
                    Counter counter = counterRepository.findById(1L).orElseThrow();
                    counter.setCount(counter.getCount() - 1);
                    counterRepository.save(counter);
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public void printCount() {
        System.out.println("\n\n\n====================\n");
        System.out.println(counterRepository.findById(1L).orElseThrow().getCount());
        System.out.println("\n====================\n\n\n");
    }
}

Redis Config

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;
    @Value("${spring.data.redis.port}")
    private int port;
    @Value("${spring.data.redis.password}")
    private String password;
    private static final String REDISSON_PREFIX = "redis://";

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
            .setAddress(REDISSON_PREFIX + host + ":" + port)
            .setPassword(password);

        return Redisson.create(config);
    }
}

application.yml

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 1234

Test

package org.example.concurrency;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.stream.IntStream;

@SpringBootTest
public class CounterServiceTest {

    @Autowired
    private CounterService counterService;

    @BeforeEach
    void setUp() {
        counterService.save(new Counter(100));
    }

    @AfterEach
    void tearDown() {
        counterService.reset();
    }

    @Test
    void concurrencyTest() {
        IntStream.range(0, 100).parallel().forEach(i -> counterService.decreaseCount());
        System.out.println("\n\n\n\n[concurrencyTest]");
        counterService.printCount();
    }

    @Test
    void concurrencyTestUsingLock() {
        System.out.println("\n\n\n\n[concurrencyTestUsingLock]");
        IntStream.range(0, 100).parallel().forEach(i -> counterService.decreaseCountUsingLock());
        counterService.printCount();
    }
}

Redis의 싱글 스레드 특성을 이용한 동시성 제어

public void initializeCounter() {
    ValueOperations<String, String> ops = this.stringRedisTemplate.opsForValue();
    ops.set("counter", "100");
}
public void decrementCounter() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    ops.decrement("counter");
}
public void getCounter() {
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    String value = ops.get("counter");
    System.out.println(value);
} 
@Test @DisplayName("Redis의 싱글 스레드 특성을 이용한 동시성 제어")
void concurrencyTestWithRedis() {
    System.out.println("\n\n\n\n[concurrencyTestNoLockWithRedis]");
    IntStream.range(0, 100).parallel().forEach(i -> counterService.decrementCounter());
    counterService.getCounter();
}
profile
새로운 시작~!

0개의 댓글