
기능을 구현하면서 이러한 패턴들을 많이봤다. 동기적으로 코드가 진행된다면 크게 문제가 되지않지만 회사에서 비동기 적으로 처리할 일이 생겨서 문제가 되는 패턴이다.
Member member= firstMemberRepository.findById();
If(member != null){
secondMemberRepository.save()
}
(위 에코드는 예시)
위와 같은 코드는 check then act 패턴 이라고도 하며 동시성이 일어나기 아주 좋은 패턴이다. 예를들어 transaction a,b가 있다고 가정하고 둘다 동시에 요청이오고 둘다 if문을 동시에 통과하게 된다며 중복된 데이터가 생성되는 문제가 생긴다.
https://velog.io/@wnsqud70/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4
여러가지 동시성 제어 방법이 있지만 이전에 동시성 제어 방법에 대해서는 글을 한번 올린적이 있기 때문에 여기서는 다루지 않겠다.
그래서 이번엔 몰랐던 uniqueKey로 동시성 제어 방법에 대해 조사해 봤다.
Unique옵션 은 단순히 중복만 방지하는 줄 알았는데 동시성도 제어가 가능하다. Unique옵션이 걸린 컬럼에 대해서는 내부적으로 인덱스를 사용한다.(Mysql은 B Tree 인덱스)
이 옵션이 중복을 막는다는건 알겠는데 그럼 어떻게 unique 옵션은 어떻게 동작하는 걸까??
unique 옵션을 걸면 실제로는 Lock을 걸어서 동시성을 제어한다. 트랜잭션 a,b가 있다고 가정을 해보면 먼저 들어온 요청에대해 Intention Lock 을 먼저 획득을 하고 데이터 삽입과 동시에 X-Lock 까지 동시에 획득을 하게 된다.
그렇게 되면 다른 동시에 요청이 온 트랜잭션에서도 똑같이 Intention Lock 을 획득하고 데이터를 삽입할때 중복을 발견 하고 X-LOCK를 시도하지만 기존에 X-Lock하고 충돌이나 대기를 하게 된다.
그리고 나서 첫번째 트랜잭션이 커밋을하고 두번째 트랜잭션이 insert를 시도하지만, 두번째 트랜잭션에서는 중복을 에러가 발생하게 된다!
UNIQUE 제약은 내부적으로 UNIQUE INDEX로 구현됨
따라서 “중복 여부”를 판단할 때, 인덱스 레벨에서 X-Lock이 걸린다.
CREATE TABLE member (
id BIGINT PRIMARY KEY,
email VARCHAR(255) UNIQUE
);
UNIQUE INDEX email_idx (email)
email_idx (UNIQUE)
-------------------------------------
| 'alice@example.com' → row id=1 |
| 'bob@example.com' → row id=2 |
| 'test@example.com' → row id=3 |
-------------------------------------
INSERT INTO member (email) VALUES ('test@example.com');
인덱스(email_idx)에서 'test@example.com' 위치를 찾는다.
같은 키가 없으면 그 위치에 새 인덱스 엔트리를 추가해야 하므로
→ 해당 key 영역(혹은 그 “gap”)에 X-lock을 건다
그 후 실제 데이터 row(id, email)를 테이블에 추가한다.
--> 이런 방식으로 x-lock는 엔트리 키 값에 건다!
대량 동시 요청이 몰리면 인덱스 락 경합으로 대기시간 증가
중복 발생 시 애플리케이션에서 예외를 처리해야 함
트랜잭션 격리 수준이 낮으면 Phantom Read 가능성 존재
Dirty Read: 다른 트랜잭션이 롤백(Rollback)할 데이터를 읽는 문제입니다. 데이터 정합성에 심각한 문제를 야기할 수 있어 거의 사용되지 않습니다.
Non-Repeatable Read: 한 트랜잭션 내에서 같은 데이터를 두 번 읽었을 때 결과가 다르게 나타나는 현상입니다.
Phantom Read: 한 트랜잭션 내에서 특정 범위의 데이터를 두 번 읽었을 때 없던 데이터가 보이거나 있던 데이터가 사라지는 현상입니다.
Dirty Read, Non-Repeatable Read 는 해결하지만 여전히 Phantom Read (단, MySQL의 InnoDB 엔진에서는 이 문제도 대부분 해결된다.
가장 엄격한 격리 수준으로, 트랜잭션을 순서대로 하나씩 실행하는 것과 같은 결과를 보장합니다. 여러 트랜잭션이 동일한 데이터에 동시에 접근할 수 없도록 한다. 하지만 그만큼 느림
동시 처리 성능이 가장 낮아 꼭 필요한 경우가 아니면 잘 사용하지 않습니다.
Unique : DB 차원에서 최종 결과의 무결성을 보장, 둘다 성공하는 것을 방지 하지만 Race Condition은 발생했을 수도 있다. 즉, 동시성 자체를 예방하지는 않는다
격리수준 : “트랜잭션 간 읽기/쓰기 충돌”을 얼마나 허용할지를 정의 , 즉, 경쟁 자체를 줄임
지금 만들고 있는 서버는 단일서버에 트래픽이 그렇게 높지 않을거라 예상이 되서 Unique + READ COMMITTED 로 간단하게 동시성을 제어 하고 있다. 하지만, 기능들이 더많아지며 서버가 나눠지고 트래픽이 더많아 지게 된다면, 대표적인 분산락 제어 Redisson이나 다른 방법을 고려해 봐야 겠다.