-
동시성 문제를 해결하자프로젝트/미디어 스트리밍 서버 프로젝트 2022. 9. 30. 00:00728x90
개요
val findVideo = videoRepository.findByIdOrNull(hitId) ?: throw IllegalArgumentException() findVideo.updateHit() videoRepository.save(findVideo)
조회수를 증가시키기 위해 read and write 하는 작업을 수행하였습니다.
이때 트랜잭션 내 데드락 혹은 데이터 불일치 등의 발생 위험이 존재했습니다.
이를 해결하기 위한 방법은 어떤 것들이 있는지 그리고 어떤 방법을 적용하는 게 좋을지 찾아보는 시간을 가지려고 합니다.
문제가 발생하는 이유는?
위 상태는 3가지 작업으로 분리할 수 있습니다.
1. hit 변수의 값을 가져온다.
2. hit 변수의 값을 증가한다.
3. 변경된 hit 변수를 저장한다.
이때 스레드 1과 2가 동시에 작업을 수행할 경우 문제가 발생할 수 있습니다.
현재 hit = 1이라고 가정
스레드 1이 hit 변수의 값을 가져옵니다. (hit = 1)
스레드 1이 hit 변수의 값을 증가합니다. (hit = 2)
스레드 2가 hit 변수의 값을 가져옵니다. (hit = 1) *DB에는 아직 1 임
스레드 1이 hit 변수의 값을 저장합니다. *DB에 2로 저장
스레드 2가 hit 변수의 값을 증가합니다. (hit = 2)
스레드 2가 hit 변수의 값을 저장합니다. *DB에 2로 저장
3을 기대하였지만 우리는 2라는 결과를 마주하게 되는 문제가 발생합니다.
그림으로 도식화하면 다음과 같습니다.
애플리케이션단에서 동시성을 제어하는 방법으로는 다음과 같은 방법들이 존재합니다.
AtomicInteger(CAS 알고리즘), volatile, synchronization
- CAS 알고리즘 : 현재 스레드에 저장된 값과 메인 메모리에 저장된 값을 비교 → 일치 시 새로운 값으로 교체, 불일치 시 실패하고 재시도
1. Isolation Level 변경하기
예를 들어 트랜잭션의 격리 수준을 SERIALIZABLE로 설정되면 읽기 작업도 공유 잠금(읽기 잠금)을 획득해야 하며, 동시에 다른 트랜잭션은 그러한 레코드를 변경할 수 없습니다.
즉, 한 트랜잭션에서 읽고 쓰는 레코드를 다른 트랜잭션에서는 절대 접근할 수 없습니다.
2. Lock 사용하기(SELECT ... FOR UPDATE)
SELECT gold FROM players WHERE id = 1 FOR UPDATE;
FOR UPDATE를 SELECT를 가져온 이후로 해당 ROW에 대해 다른 세션의 SELECT, UPDATE, DELETE 등의 쿼리가 모두 잠김 상태가 됩니다.
즉, FOR UPDATE를 한 세션 외에 다른 세션들은 모두 해당 ROW에 접근을 할 수 없게 되고, 모두 대기 상태가 된다.
트랜잭션이 끝나는 시점에서 풀립니다.
3. JPA versioning 사용하기 (낙관적 락)
도식도를 글로 나타내면 아래와 같습니다.
- A가 table의 Id 2번을 읽음 ( name = Karol, version = 1 )
- B가 table의 Id 2번을 읽음 ( name = Karol, version = 1 )
- B가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol2, version = 2 ) 성공
- A가 table의 Id 2번, version 1인 row의 값 갱신 ( name = Karol1, version = 2 ) 실패
- Id 2번은 이미 version이 2로 업데이트되었기 때문에 A는 해당 row를 갱신하지 못함
낙관적 락은 CAS알고리즘과 같다는 생각이 듭니다.
4. DB atomic operation
update [테이블이름] set count = count + 1 where id=[pk id]
update만 하게 돼도 동시성 보장이 된다고?라는 생각이 들었습니다.
단일 UPDATE/DELETE문은 원자적이어서 작업이 진행 중인 동안 다른 보류 중인 트랜잭션은 행을 수행할 수 없습니다.
다음은 stackoverflow에 대한 링크입니다.
https://stackoverflow.com/questions/4358732/is-incrementing-a-field-in-mysql-atomic
그래서 뭘 써야 할까?
2번(비관적 락) vs 3번(낙관적 락)을 비교했을 때는 낙관적 락은 확실히 락을 실제로 걸지 않는 만큼 성능 쪽에서 유리할 수 있습니다.
하지만 외부 API를 호출하는 경우가 로직에 포함되어 rollback이 일어나는 경우에는 외부 API는 rollback이 일어나지 않을 수 있기 때문에 비관적 락을 통해 API가 호출되지 않는 방향으로 선택해야 합니다.
2번(비관적 락) vs 4번(DB atomic operation)을 비교했을 때는 사실 로직상으로 lock을 건다고 생각해서 동일하다고 생각합니다.
하지만 복잡한 비즈니스 로직이 첨부된다면 DB atomic operation에는 쓰기가 힘들어진다고 생각이 듭니다.
따라서 복잡한 비즈니스로직이 존재하면 select... for update로 값을 읽어오고 비즈니스 로직을 수행하고 update 하는 방식을 사용하면 될 것 같습니다.
하지만 간단한 작업은 4번으로 해결하면 좋을 것 같습니다.
1번(isolation level) vs 4번(DB atomic operation)을 비교했을 때는 Serializable을 고립 레벨로 설정하면 동시성이 너무 떨어질 거라 생각이 들었기 때문에 4번으로 해결하면 좋을 것 같습니다.
결론
현재 조회수를 증가시키는 간단한 로직을 사용하고 있으니 4번(DB atomic operation)을 통해 해결하면 좋을 것 같습니다.
더 나아가서 여러 개의 서버가 락을 공유하는 분산 락에 대해서도 알아보고 싶으신 분들은 다음 글을 읽어보시면 좋을 것 같습니다.
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
출처
https://mygumi.tistory.com/111
https://dev-monkey-dugi.tistory.com/151
https://sabarada.tistory.com/175
https://velog.io/@been/자바Multi-Thread환경에서-동시성-제어를-하는-방법
https://forums.mysql.com/read.php?25,401854,401992#msg-401992
'프로젝트 > 미디어 스트리밍 서버 프로젝트' 카테고리의 다른 글
@Transactional 롤백과 @TransactionalEventListener (0) 2022.10.04 @Embedded vs @OneToOne (0) 2022.10.03 배포 스크립트 작성하기 (0) 2022.09.29 kotlinDSL + RestDocs 적용하기 (0) 2022.09.28 MultipartFile 컨트롤러 단위 테스트(MockMvc) (0) 2022.08.24