📖
Aaron's TECH BOOK
  • Intro
    • About me
  • Lecture
    • Kubernetes
      • Begin Kubernetes
    • Kafka
      • Begin Kafka
    • Kotlin
      • TDD, Clean Code Preview
      • woowa Kotlin
    • Java
      • Multithread Concurrency
      • The Java
    • Toby
      • Toby Spring 6
      • Toby Spring Boot
    • MSA
      • 01.Micro Service
      • 02.DDD 설계
      • 03.DDD 구현
      • 04.EDA 구현
    • Spring Boot
    • Spring Batch
    • Spring Core Advanced
    • Spring DB Part II
    • Spring DB Part I
    • JPA API and Performance Optimization
    • JPA Web Application
    • JPA Programming Basic
    • Spring MVC Part 2
      • 01.Thymeleaf
      • 02.ETC
      • 03.Validation
      • 04.Login
      • 05.Exception
    • Spring MVC Part 1
      • 01.Servlet
      • 02.MVC
    • Http
      • 01.Basic
      • 02.Method
      • 03.Header
    • Spring Core
    • Study
      • Concurrency issues
      • First Come First Served
      • Performance Test
      • TDD
      • IntelliJ
  • Book
    • Kafka Streams in Action
      • 01.카프카 스트림즈
      • 02.카프카 스트림즈 개발
      • 03.카프카 스트림즈 관리
    • Effective Kotlin
      • 01.좋은 코드
      • 02.코드 설계
      • 03.효율성
    • 이벤트 소싱과 MSA
      • 01.도메인 주도 설계
      • 02.객체지향 설계 원칙
      • 03-04.이벤트 소싱
      • 05.마이크로서비스 협업
      • 06.결과적 일관성
      • 07.CQRS
      • 08.UI
      • 09.클라우드 환경
    • 몽고DB 완벽 가이드
      • I. 몽고DB 시작
      • II. 몽고DB 개발
    • Kotlin Cookbook
      • 코틀린 기초
      • 코틀린 기능
      • ETC
    • Kotlin in Action
      • 함수/클래스/객체/인터페이스
      • 람다와 타입
      • 오버로딩과 고차 함수
      • 제네릭스, 애노테이션, 리플렉션
    • Kent Beck Tidy First?
    • 대규모 시스템 설계 기초
      • 01.사용자 수에 따른 규모 확장성
      • 02.개략적인 규모 추정
      • 03.시스템 설계 공략법
      • 04.처리율 제한 장치 설계
      • 05.안정 해시 설계
      • 06.키-값 저장소 설계
      • 07.유일 ID 생성기 설계
      • 08.URL 단축기 설계
      • 09.웹 크롤러 설계
      • 10.알림 시스템 설계
      • 11.뉴스 피드 시스템 설계
      • 12.채팅 시스템 설계
      • 13.검색어 자동완성 시스템
      • 14.유튜브 설계
      • 15.구글 드라이브 설계
      • 16.배움은 계속된다
    • 실용주의 프로그래머📖
    • GoF Design Patterns
    • 도메인 주도 개발 시작하기
      • 01.도메인 모델 시작하기
      • 02.아키텍처 개요
      • 03.애그리거트
      • 04.리포지터리와 모델 구현
      • 05.Spring Data JPA를 이용한 조회 기능
      • 06.응용 서비스와 표현 영역
      • 07.도메인 서비스
      • 08.애그리거트 트랜잭션 관리
      • 09.도메인 모델과 바운디드 컨텍스트
      • 10.이벤트
      • 11.CQRS
    • Effective Java 3/E
      • 객체, 공통 메서드
      • 클래스, 인터페이스, 제네릭
    • 소프트웨어 장인
    • 함께 자라기
    • Modern Java In Action
      • 01.기초
      • 02.함수형 데이터 처리
      • 03.스트림과 람다를 이용한 효과적 프로그래밍
      • 04.매일 자바와 함께
    • Refactoring
      • 01.리펙터링 첫 번째 예시
      • 02.리펙터링 원칙
      • 03.코드에서 나는 악취
      • 06.기본적인 리펙터링
      • 07.캡슐화
      • 08.기능 이동
      • 09.데이터 조직화
      • 10.조건부 로직 간소화
      • 11.API 리팩터링
      • 12.상속 다루기
    • 객체지향의 사실과 오해
      • 01.협력하는 객체들의 공동체
      • 02.이상한 나라의 객체
      • 03.타입과 추상화
      • 04.역할, 책임, 협력
      • 05.책임과 메시지
      • 06.객체 지도
      • 07.함께 모으기
      • 부록.추상화 기법
    • Clean Code
    • 자바 ORM 표준 JPA 프로그래밍
Powered by GitBook
On this page
  • Concurrency issues
  • Race Condition
  • Java Synchronized
  • DataBase Lock
  • Redis
  • Finish
  • Reference
  1. Lecture
  2. Study

Concurrency issues

Last updated 11 months ago

Concurrency issues

강의를 듣고 요약한 내용입니다.

Race Condition

경쟁상태는 두 개 이상의 스레드가 공유 데이터에 액세스할 수 있고, 동시에 변경을 하려고 할 때 발생하는 문제입니다.

@Service
@RequiredArgsConstructor
public class StockService {

    private final StockRepository stockRepository;

    public synchronized void decrease(Long id, Long quantity) {
        final Stock stock = stockRepository.findById(id).orElseThrow();
        stock.decrease(quantity);

        stockRepository.saveAndFlush(stock);
    }
}

우리는 아래 테스트에서 0의 결과를 예상하지만, 동시에 들어오는 요청들이 갱신 전 값을 읽고 수정하면서 실제 갱신이 누락되는 현상이 발생하게 됩니다.

Thread-1
Stock
Thread-2

select

quantity:5

quantity:5

select

update

quantity:4

quantity:4

update

@Test
void decrease_inventory_concurrent_requests() throws InterruptedException {
    int threadCount = 100;
    ExecutorService executorService = Executors.newFixedThreadPool(32);
    CountDownLatch latch = new CountDownLatch(threadCount);

    for (int i = 0; i < threadCount; i++) {
        executorService.submit(() -> {
            try {
                stockService.decrease(1L, 1L);
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();

    Stock stock = stockRepository.findById(1L).orElseThrow();

    assertEquals(0, stock.getQuantity());
}

Java Synchronized

synchronized 를 메서드 선언부에 붙여주면 해당 메서드는 한 개의 스레드만 접근이 가능하게 됩니다.

@Transactional
public synchronized void decreases(Long id, Long quantity) {
    final Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);

    stockRepository.saveAndFlush(stock);
}

.

⚠️ 여기서 문제가 있습니다.

Spring Transactional 의 동작 방식은 아래와 같이 우리가 만든 클래스를 래핑한 클래스를 새로 만들어서 실행하게 됩니다.

public class TransactionService {
   private StockService stockService;

   ...

   public void decreases(Long id, Long quantity) {
       startTransaction();

       stockService.decreases(id, quantity);

       endTransaction();
   }

   ...
}

우리가 만든 클래스를 필드로 가지고 호출하게 되는데 트랜잭션 종료 전에 다른 스레드가 갱신된 전 값을 읽게 되면 결국 이전과 동일한 문제가 발생하게 됩니다.

여기서 단순하게 Transactional Annotation 을 제거하여 문제를 해결할 수는 있습니다.

.

⚠️ 하지만, Java Synchronized 의 문제는 또 존재합니다.

Java Synchronized 는 하나의 프로세스 안에서만 보장이 됩니다.

그러다보니 서버가 1대일 경우에는 괜찮겠지만, 서버가 2대 이상일 경우 결국 여러 스레드에서 동시에 데이터에 접근할 수 있게 되면서 레이스 컨디션이 발생하게 됩니다.

Time
Thread-1
Stock
Thread-2

10:00

select

quantity:5

quantity:5

select

10:05

update

quantity:4

quantity:4

update

대부분의 운영 서비스는 2대 이상의 서버로 운영되기 때문에 Java Synchronized 는 거의 사용되지 않는 방법입니다.

DataBase Lock

MySQL

Pessimistic Lock

비관적 락.

  • 실제로 데이터에 Lock을 걸어서 정합성을 맞추는 방법

  • Row or Table 단위로 Locking

  • Exclusive Lock을 걸게 되며, 다른 트랜잭션에서는 Lock 해제 전에 데이터를 가져갈 수 없음

  • 데드락(두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다는 상태) 주의 필요

  • 충돌이 빈번하게 일어나거나 예상된다면 추천하는 방식

장점.

  • 충돌이 빈번하게 일어난다면 Optimistic Lock 보다 성능이 좋을 수 있다.

  • 락을 통해 업데이터를 제어하므로 데이터 정합성이 보장된다.

단점.

  • 별도의 락을 잡기 때문에 성능 감소가 있을 수 있다.

/** Repository **/
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithPessimisticLock(@Param("id") Long id);

...

/** Service **/
@Transactional
public void decrease(Long id, Long quantity) {
    final Stock stock = stockRepository.findByIdWithPessimisticLock(id);
    stock.decrease(quantity);
    stockRepository.save(stock);
}

Optimistic Lock

낙관적 락.

  • 실제로 Lock 을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법

  • 먼저 데이터를 읽은 후 update 수행 시, 현재 내가 읽은 버전이 맞는지 확인하며 업데이트 수행

  • 내가 읽은 버전에서 수정사항이 생겼을 경우 애플리케이션에서 다시 읽은 후에 작업을 수행

  • 충돌이 빈번하게 일어나지 않을 경우 추천하는 방식

장점.

  • 별도의 락을 잡지 않으므로 Pessimistic Lock 보다 성능상 이점이 있다.

단점.

  • 업데이트 실패 시 재시도 로직을 개발자가 직접 작성해야 한다.

/** Entity **/
@Version
private Long version;

...

/** Repository **/
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select s from Stock s where s.id = :id")
Stock findByIdWithOptimisticLock(@Param("id") Long id);

...

/** Service **/
@Transactional
public void decrease(Long id, Long quantity) {
    final Stock stock = stockRepository.findByIdWithOptimisticLock(id);
    stock.decrease(quantity);
    stockRepository.save(stock);
}

...

/** Facade **/
@Component
@RequiredArgsConstructor
public class OptimisticLockStockFacade {

    private final OptimisticLockStockService optimisticLockStockService;

    public void decrease(Long id, Long quantity) throws InterruptedException {
        while (true) { // 업데이트 실패 시 재시도
            try {
                optimisticLockStockService.decrease(id, quantity);
                break;
            } catch (Exception e) {
                Thread.sleep(50);
            }
        }
    }
}

Named Lock

  • 이름을 가진 Lock 획득 후, 해제할 때까지 다른 세션은 이 Lock 을 획득할 수 없도록 함

  • Transaction 종료 시 Lock 이 자동으로 해제되지 않는 점 주의

    • 별도의 명령어로 해제를 수행해 주거나 선점 시간이 끝나야 해제

  • 비관적 락과 유사하지만 로우나 테이블이 아닌 메타데이터의 락킹을 하는 방식

  • 커넥션 풀 부족 현상을 막기 위해 데이터 소스를 분리해서 사용할 것을 권장

  • 주로 분산락 구현 시 사용

장점.

  • Timeout을 쉽게 구현 가능

단점.

  • 트랜잭션 종료 시 락 해제, 세션 관리 필요

  • 구현 방법이 복잡해질 수 있음

/** Repository **/
@Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
void getLock(@Param("key") String key);

@Query(value = "select release_lock(:key)", nativeQuery = true)
void releaseLock(@Param("key") String key);

...

/** Service **/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decrease(Long id, Long quantity) {
    final Stock stock = stockRepository.findById(id).orElseThrow();
    stock.decrease(quantity);

    stockRepository.saveAndFlush(stock);
}

...

/** Facade **/
@Component
@RequiredArgsConstructor
public class NamedLockStockFacade {

    private final LockRepository lockRepository;

    private final StockService stockService;

    @Transactional
    public void decrease(Long id, Long quantity) {
        try {
            lockRepository.getLock(id.toString());
            stockService.decrease(id, quantity);
        } finally {
            lockRepository.releaseLock(id.toString());
        }
    }
}

Reference.

Redis

MySQL의 Named Lock과 유사한 방식

Lettuce

  • setnx(set if not exist) 명령어를 활용하여 분산락 구현

  • Spin Lock 방식

    • 락을 획득하려는 스레드가 락을 사용할 수 있는지 반복적으로 확인하면서 락 획득을 시도하는 방식

    • 재시도 로직 개발 필요

장점.

  • 구현이 단순

  • 별도의 라이브러리가 불필요(spring data redis 사용 시 기본적으로 Lettuce 적용)

단점.

  • Spin Lock 방식이므로 동시에 많은 스레드가 락 획득 대기 상태라면 레디스에 부하가 갈 수 있음

    • 락 획득 재시도 간 대기 시간도 필요

# key 는 1 이고, value 는 lock 인 데이터 생성
> setnx 1 lock
(integer) 1

# 이미 1 이라는 키가 있으므로 실패
> setnx 1 lock
(integer) 0

# 키 삭제
> del 1
(integer) 1
/* Repository */
public Boolean lock(Long key) {
    return redisTemplate
            .opsForValue()
            .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
}

public Boolean unlock(Long key) {
    return redisTemplate.delete(generateKey(key));
}

...

/* Facade */
public void decrease(Long key, Long quantity) throws InterruptedException {
    while (!redisLockRepository.lock(key)) {
        Thread.sleep(100);
    }

    try {
        stockService.decrease(key, quantity);
    } finally {
        redisLockRepository.unlock(key);
    }
}

Redisson

  • pub-sub 기반으로 Lock 구현 제공

    • 채널을 만들고 락을 점유중인 스레드가 락 획득을 대기중인 스레드에게 락 해제라는 메시지를 전송하면, 메시지를 전달받은 스레드가 락 획득을 시도하는 방식

  • 락 획득을 위해 반복적으로 락 획득을 시도하는 Lettuce 대비 부하를 줄일 수 있음

Thread-1 ----------> Channel -------------------> Thread-2
          I'm done            Try to get a Lock
# ch1 채널 구독(thread-2)
> subscribe ch1
1) "subscribe"
2) "ch1"
3) (integer) 1

# 메시지 전송(thread-1)
> publish ch1 hello
(integer) 1

# thread-2 메시지 수신
...
1) "message"
2) "ch1"
3) "hello"

장점.

  • pub-sub 기반 구현으로 lettuce 대비 레디스에 부하가 덜 감

  • 락 획득 재시도를 기본적으로 제공

단점.

  • 복잡한 구현

  • 별도의 라이브러리 필요

/* Facade */
public void decrease(Long key, Long quantity) {
    RLock lock = redissonClient.getLock(key.toString());

    try {
        boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS); // (락 획득 시도 시간, 점유 시간)

        if (!available) {
            System.out.println("lock 획득 실패");
            return;
        }

        stockService.decrease(key, quantity);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        lock.unlock();
    }
}

실무에서는 보통

재시도가 필요하지 않은 락은 Lettuce를 활용하고,

재시도가 필요한 경우 Redisson을 활용

Finish

Java Synchronized

  • 한 개의 스레드만 접근 가능하도록 제한

  • @Transactional 적용 불가

  • 2대 이상의 서버로 운영될 경우 레이스 컨디션은 또 다시 발생

DataBase Lock

  • Pessimistic Lock

    • 로우, 테이블 단위로 락킹

    • 락 해제 전까지 다른 스레드는 데이터를 가져갈 수 없음(데드락 주의)

    • 충돌이 빈번하게 일어날 경우 추천

  • Optimistic Lock

    • 버전을 이용

    • 업데이트 실패 시 재시도

    • 충돌이 빈번하게 일어나지 않을 경우 추천

    • 업데이터 실패 시 재시도 로직 개발 필요

  • Named Lock

    • Pessimistic Lock 과 유사하지만 이름들 가진 데이터에 락킹

    • 트랜잭션 종료 시 락 해제, 세션은 직접 관리 필요

    • 주로 분산락 구현 시 사용

Redis

  • Lettuce

    • Spin Lock 방식(레디스에 부하 가능성 존재)

    • 재시도 로직 개발이 필요

    • 재시도가 필요하지 않은 경우 권장

  • Redisson

    • pub-sub 기반

    • 별도의 라이브러리가 필요하고 구현이 복잡

    • 재시도가 필요한 경우 권장

MySql vs. Redis

Mysql
Redis

Mysql을 사용중이라면 별도 비용없이 사용 가능

활용중인 Redis가 없다면 별도의 구축 비용과 인프라 관리 비용이 발생

Redis 보다 성능이 좋지 않음(어느정도의 트래픽까지는 문제없이 활용 가능)

Mysql 보다 성능이 좋음

Reference

이름을 가진

재고시스템으로 알아보는 동시성이슈 해결방법
Race condition
@Transactional 과 PROXY
commit
commit
Metadata Locking
commit
Locking Functions
commit
Spin Lock
Redisson/Spring Boot Starter
commit
Repository