나에게도 찾아온 동시성 문제

🧗🏼탐험가 시은·2023년 3월 17일
0

내 공부

목록 보기
1/2
post-thumbnail

상황 설명

커뮤니티에 참여하는 참여자 수를 관리한다.
커뮤니티에 참여한 사용자는 Fan 테이블에 새로 저장된다.

CommunityEntity증가 로직
- 커뮤니티 엔티티에서 참여자 수를 하나의 Column으로 관리한다.

- 참여자 수 Update는 Transactional 내 entity의 변화를 감지하는 Driry Checking 방법을 활용했다.

문제의 발견

기능 구현을 마무리하고, 성능 테스트를 진행하던 중, 저장된 참여자 값과 칼럼으로 관리하는 참여자 수가 맞지 않는 것을 발견했다.

초당 100건의 요청 10번 반복가입자 수는 1000명으로 일치하지만, 참여자수는 209로 정합성이 맞지 않음

드디어 동시성 문제를 만난것인가...!(내심 기대했다)

오해와 진실

동시성 문제를 처음 만나고 @Transactional로 해결이 가능하다고 생각했다.
Transactional은 신이야!
하지만 내 생각과는 달랐고 시행착오를 겪었던 2가지 상황을 공유해보려한다.

Try .1

MySQL에서는 기본 isolation Level을 2단계 (REPEATABLE READ)를 사용하고 있다.
여기서 동시성 문제를 해결하기 위해 단순히 최고단계인 SERIALIZABLE로 사용하면 되겠지 생각했다.

Serializable을 사용해도 동시에 여러 Thread에서의 읽기가 발생

MySQL의 Transaction 실행 조건을 보니 auto commit 여부에 따라 다르지만, Read 시 shared lock을 사용하고 있었다. (하위 사진)

따라서, 여러개의 스레드가 값을 수정하기 위해 읽기 요청을 보냈을 때, 같은 값을 읽어오는 동시성 문제가 발생하게 된다.

부가적으로, Serializable에선 write를 위해 Exclusive 락을 걸어야하는데, Read시 발생한 Shared 락 때문에 데드락이 발생할 확률이 매우 높다.

Try .2

두번째로는 해당 메서드에 단 하나의 Thread만 접근하도록 하는 synchronized를 붙여줬다.

synchronized를 활용한 시도
  • Transactional 안에서의 Synchronized
@Transactional
public synchronized methodA(){
	...
    updateLogic();
    ...
}

위처럼 Transactional어노테이션이 붙으면 Proxy에서 동작하기 때문에 synchronized가 원하는 대로 동작하지 않는다.

  • Synchronized의 Method level
public methodA(){
	...
    methodB
    ...
}
public synchronized methodB(){
	updateLogic();
}

위와 같은 하위 method에만 synchronized가 붙어있다면, 이미 상위 method에서 여러 thread가 동시접근 했기 때문에 동시성 문제를 해결할 수 없다.

문제 해결

위 상황들을 종합하여, Transactional 탈거, 최상위 method에 synchronized를 적용하여 동시성문제를 해결할 수 있었다.

추가 고려사항

하지만 scale out 환경에서도 원하는대로 동작할까?
아래 그림을 보자

멀티스레딩 환경에서의 동시성 문제synchronized된 서버의 scale out 환경에서의 동시성 문제

|

위처럼 동시성 문제가 그대로 발생하는 것을 생각해볼 수 있었고
해당 내용을 해결하기 위해 공통으로 관리해줄 수 있는 환경이 필요하다고 생각했다.
그리하야, redisson을 활용한 분산락을 적용하게 되었다.

Redisson 적용

Redis에서는 Redisson과 Lettuce 두가지 라이브러리를 지원한다.
Redisson : pub/sub 구조로 락을 가져오는 방식
Lettuce : 스핀락 형식으로 계속 락을 요청하는 방식 (레디스에 전달되는 부하가 높음)

따라서, 서로 다른 프로세스가 배타적인 방식으로 리소스를 가져오기엔 Redisson이 유리하다고 판단하여 적용하게 되었다.

하지만, 기존 Transactional이 적용되는 부분을 유지하고 싶어,
동시성이 보장되어야 하는 로직을 덮을 수 있는 FACADE class를 생성했다.

이를 통해 FACADE class안에선 Transactional역할도 유지하고, 동시성 문제로부터 자유로울 수 있도록 하였다.

profile
시은이의 살아남기 시리즈!

0개의 댓글