Home > General > Concurrency > 동시성 문제 해결 방법

동시성 문제 해결 방법
Multi-Thread Concurrency-Issue MySQL Redis

동시성 문제 해결방법

  • 멀티스레드 작업을 하다보면, 공유 자원에 대한 Race Condition으로 인해 동시성 이슈가 발생한다
  • 이에 대한 다양한 해결방법을 정리해보자
  • 최종 선택 기준
    • synchronized는 사용 X
    • 비용적 여유가 없적정한 트래픽이라면, MySQL로 처리
    • 비용적 여유가 있트래픽이 많다Redis로 처리
  • MySQL VS Redis
    • MySQL
      • 이미 사용 중이라면 별도 비용 없이 사용 가능
      • Redis 보다 성능이 떨어짐 (어느정도 트래픽까지는 문제 없이 사용 가능)
    • Redis
      • 사용 중인 Redis가 없다면, 인프라 구축 및 관리 비용 발생
      • MySQL보다 성능이 좋음

Synchronized (거의 사용 X)

  • 데이터에 1개의 스레드만 접근 가능하도록 하기
  • 문제점
    • 여러 프로세스 동작 시, 여전히 Race Condition 발생
      • synchronized는 하나의 프로세스 안에서만 1개의 스레드 접근 보장
      • 다른 프로세스의 스레드가 접근하면, 여전히 여러 스레드가 접근 가능해짐
        • 서버가 1대일 때는 괜찮지만, 2~3대부터는 데이터 접근을 여러 곳에서 할 수 있음
      • 실제 운영 중인 서비스대부분 2대 이상의 서버를 사용 -> synchronized는 거의 사용 X
    • 추가로, @Transactional 사용 시 synchronized 적용이 어려움
      • @Transactional은 스프링 AOP 사용으로 트랜잭션 프록시 객체를 생성
        • 내부 동작
          • startTransaction();
          • stockService.decrease(id, quantity); (target 객체 호출)
          • endTransaction();
      • 실제 DB 업데이트(endTransaction()) 전다른 스레드가 decrease() 메서드 호출할 수 있음
      • 이렇게 되면, 다른 스레드는 갱신되기 전 값을 가져가 여전히 동시성 문제 발생
      • 즉, 서비스 객체 메서드가 아닌, AOP 객체 메서드에 synchronized를 걸어야 하는데 어려움

MySQL이 지원해주는 방법

  • 선택 기준
    • 충돌이 빈번하게 일어날 것 같다면 Pessimistic Lock
    • 충돌이 별로 없을 것 같다면 Optimistic Lock
  • Pessimistic Lock
    • 실제 데이터에 을 걸어서 정합성을 맞추는 방법 (for update)
    • Exclusive Lock을 걸게되며, 다른 트랜잭션에서는 락이 해제되기전에 데이터를 가져갈 수 없음
      • e.g. 서버 1이 락을 가져가면, 다른 서버(2, 3, 4…)는 락 획득 대기
    • 데드락을 주의해야 함
    • 장점
      • 충돌이 빈번하게 발생한다면, Optimistic Lock 보다 성능이 좋을 수 있음
      • 락 덕분에 데이터 정합성 보장
    • 단점
      • 자체로 인한 성능 감소 발생
  • Optimistic Lock
    • 버전을 이용해 정합성을 맞추는 방법
    • 데이터를 읽은 후 update 쿼리를 수행할 때, 현재도 내가 읽은 버전이 맞는지 확인하며 업데이트
      • e.g.
        • 서버 1과 2가 버전 1인 데이터를 읽고, 업데이트 쿼리를 날림
        • 서버 1이 업데이트 쿼리를 수행하면 해당 데이터의 버전이 2가 됨
        • 서버 2는 읽은 버전이 1이므로, 현재 데이터와 버전(버전 2)이 달라 쿼리 실패
    • 내가 읽은 버전에서 수정사항이 생겼을 경우, application에서 다시 읽은 후에 작업을 수행
    • 장점
      • 별도의 락을 걸지 않으므로Pessimistic Lock 보다 성능이 좋음
    • 단점
      • 업데이트가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 함
  • Named Lock
    • 이름을 가진 Metadata Locking
    • 이름을 가진 락을 획득한 후 해제할 때까지 다른 세션은 이 락을 획득할 수 없음
      • MySQL: select get_lock('1', 1000), select release_lock(‘1’)
    • 락 해제별도의 명령어로 수행하거나 선점시간이 끝나야
      • 트랙잭션이 종료될 때 락이 자동으로 해제되지 않음
    • 장점
      • 분산 락 구현에 적합
      • Pessimistic Lock 보다 타임아웃을 쉽게 구현할 수 있음
      • 삽입 시 데이터 정합성 맞출 때도 좋음
    • 단점
      • 트랜잭션 종료 시 락 해제, 세션 관리를 잘해주어야 함
      • 실제 사용 시 구현 방법이 복잡
    • 참고: 실무에서는 데이터 소스를 분리해서 사용할 것 (커넥션 풀 고갈 예방)

Pessimistic Lock과의 차이점

  • Pessimistic Lock은 행, 테이블 단위로 락을 걸음 (e.g. Stock에 락을 걸음)
  • Named Lock은 메타 데이터에 락을 걸음 (e.g. Stock이 아닌 별도의 공간에 락을 걸음)

Redis를 이용한 방법

  • 선택 기준
    • 재시도가 필요하지 않은 락Lettuce 활용
    • 재시도가 필요한 락Redisson 활용
  • Lettuce
    • setnx 명령어(set if not exist)를 활용해 분산 락 구현
      • 데이터 set (=락 획득), 데이터 del (=락 해제)
      • MySQL의 Named Lock과 유사
    • 스핀락(Spin Lock) 방식
    • 장점
      • 구현이 간단
        • 세션 관리도 신경쓸 필요 X
      • 별도 라이브러리 필요 X
        • spring-data-redis를 사용하면 Lettuce가 기본
    • 단점
      • 재시도 로직을 개발자가 직접 작성
      • 스핀 락이므로 충돌이 잦으면 Redis 부하 상승
        • -> 실패 시 재시도 시간에 텀을 두어 보완 (Thread.sleep(100))
  • Redisson
    • Pub-Sub 기반으로 분산 락 제공
      • 채널을 하나 만들어 락 점유 중인 스레드가 해제를 알리면 락 획득 대기 스레드는 락 획득 시도
    • 장점
      • 락 획득 재시도기본으로 제공
      • Pub-Sub 기반이므로 Lettuce에 비해 Redis 부하 적음
        • 락 해제 후 알림으로 락 획득 시도는 1번 혹은 몇 번 정도만 진행함
    • 단점
      • 구현이 조금 복잡
      • 별도의 라이브러리 필요 O